设计模式之禅——6大设计原则

6个原则

  • 单一职责:有且仅有一个原因引起类的变更
  • 里氏替换:所有使用基类的地方都能无缝的使用其子类
  • 依赖倒置:所有依赖都应该依赖于抽象,细节依赖于抽象,抽象依赖于抽象,面向契约编程就是它的实现
  • 最少知识:一个对象应该对其他对象有最少了解,又名迪米特法则
  • 接口隔离:客户端不应该依赖他不需要的接口
  • 开闭原则:应该对修改关闭,对扩展开放

单一职责

我的看法

单一职责的定义是 >= 并且<= 一个原则引起类的变更,也就是多于一个职责不行,少于一个职责也不行。

我的例子

以前为了方便,我总是把Model和Gson 的解析的模板类写在一起,比如,要解析

{
    "name": "lisa",
    "age": 23,
    "sex": 0
}

我会定义一个这样的类

public class User{
    private String name;
    private int age;
    private int sex;
    
    ...Getter Setter...
}

为了方便,我会同时把这个类当做SQLite的一个Model保存数据。因为大部分时候他们两个是一致的。

这时候,User 这个类就会有两个职责,解析Json模板和保存数据的Model。
这样做就会带来问题,例如,我往Json数据中加入一个token的字段,但是这个字段是不需要保存下来的,这样你就需要加入一个判断使token不保存下来,会无端端导致这个类变得复杂,复杂就容易出错,同时因为Json的变动,导致Model那边的测试也要重新跑一边,这是一个典型的反例了。

正确的做法应该是把两个分开,分别负责不同的职能。

里氏替换

我的看法

就是子类中必须完全并且正确实现父类的方法,对,少了也不行,改了意思也不行。

我的例子

一时没想到好例子,继续沿用书中的栗子吧。
书中讲述的是一个枪的父类,具体调用流程如下

重点看枪这个父类,如果我要实现一个玩具枪的时候应该怎么办?玩具枪也是枪,是不是直接继承枪这个父类就可以了?我以前的做法会是直接继承AbstractGun,然后把shoot方法置空,也就是变成如下这样

但是这样做是不对的,毕竟这里的枪是用来杀人的,当ToyGun继承于AbstractGun的时候,就是表示他是一只杀人的枪,当实际上它并不是,所以应该声明一个玩具枪的父类,让玩具枪基于这个父类去实现,这样就不会导致士兵拿到这个枪去杀人。

里氏替换要求子类必须忠实的实现父类的功能,不能少,不能改。毕竟,在Java中,能使用父类的地方就能替换成任意一个子类,如果子类没有父类的功能,调用者就会出错。

总的来说就是,子类必须在忠实的实现父类的功能后这个前提下享有自己的个人

依赖倒置

我的看法

基于抽象编程,基于契约编程。Java中有个很出名的做法,不管干啥,先定义接口,使用的都是抽象的类或者接口,具体实现不管,就是基于接口编程。这样做好处是很明显的,抽象只是定义了一种行为模式,表示我可以做这个事情,具体你怎么做不管,这样给了实现者很大的空间。比如排序这个事情,我定义了排序这个功能,你只管把排序好的数据给我,具体实现我不管。当你发现一个更好的排序算法的时候,你就可以通过替换具体实例轻松完成升级。

我的例子

我在项目GaiaLibrary 曾经定义了一个通道GLink,如下图

有一个写入接口,负责把数据写入。调用者是GRequestDispatcher

本来实现类只有一个GBLELink,也就是BLE通道,但是某一天,上头要求支持传统蓝牙通道,这时候基于抽象编程的优越性就发挥出来了,调用的代码不需要改动一行,我只需要继承GLink,实现一个基于传统蓝牙GTranditionLink就可以了。对于调用者来说,这个完全是无缝的。

接口隔离

我的理解

接口隔离其实就是要求接口要尽可能小,接口代表的是一种能力,能够完成完整的描述一种能力就足够了。

我的例子

我曾经对Volley 的网络请求进行封装,我对网络请求的回掉并不是像一般的网络请求那样直接封装成一个,而是三个,如图

这样做有什么好处呢,很多网络请求回掉其实是有共性的,比如,错误之后,根据不同的指令码,弹出不同的提示。再比如,成功的时候固定获取某部分数据,或者存入到数据库中。当我把这几个接口全部分开的时候,可以轻松的通过不同的组合来达到各种各样的目的,提高了代码的复用性。但是缺点也是有的,这是接口分细之后共有的缺点,就是会更麻烦,如果是一个回掉接口,就可以通过一次设定就可以完成了。这个需要设定三次。

最少知识

定义解析

这个定义其实有两个方面内容,我对你了解最少,还有我让你了解我最少。
我对你了解最少,其实就是,调用其他类的属性和方法越少越好,这样就算其他类有更改,你内部的更改风险也能降到最低。
我让你了解最少,就是公开的属性和方法最少,这样就能保证修改内部实现的时候没有更多的束缚,毕竟私有的都是别人不能调用的。

我的例子

这个在SpringMVC中有个很好的东西就是为了解决知道类怎么构建这个问题的。就是注入,我在安卓中的使用Dagger,一个依赖注入框架。为什么说依赖注入框架是遵循最少知识原则,因为本来创建一个对象,就必须调用构造方法,调用构造方法就是在了解这个类,把这个过程省去了,就减少了需要对这个类的了解了。

开闭原则

定义解析

开闭原则可以提高以下几点,测试效率,提高复用,高维护性。测试效率,不需要重新测试修改过的代码,已经引用了这段修改过代码的代码,只需要测试配置是否正确即可。提高复用,使用已有功能通过配置达到目的。

个人案例

在安卓开发中,在网络回调中以前我是这样写的。

public void callback(int result){
    switch(result){
    case 200:
        .... doSomething
    case 400:
        .... doSomething
}

这样做有个很大的坏处,因为如果后台增加了一个码,你又需要重新加多一个case,假设这里有50个case,添加一个case,这个功能代码就需要所有测试重新再跑一趟,因为你修改了这个方法里的代码,正确的做法应该使用一种配置的方法。

//声明接口
interface CodeCallback{
    void execute();
}

Map<Integer,CodeCallback> map = new HashMap<>();

//添加callback到 map中
map.put(200,callback200);
map.put(400,callback400);


public void callback(int result){
    if(map.contains(result)){
        map.get(result).execute();
    }else{
        throw new IllegalArgumentException(result);
    }
}

通过这种方法,很好的符合开闭原则,当有新的code时候,只需要重新实现一个CodeCallback,然后添加map中就可以了,对于测试来说,只需要测试新的CodeCallback就可以了,无需重新跑一次所有的测试。

总结

  • 单一职责

    当一个类遵守单一职责的时候,对比多职责,涉及到的功能肯定比较少。实现起来容易,代码清晰,也更容易维护。职责的减少,导致类的调用也变少,使用到地方也相应的减少,对应修改这个类的时候涉及到的范围也减少了。

  • 里氏替换

    当一个类遵守里氏替换原则的时候,我就可以放心的使用父类,而不管子类是怎么实现的,毕竟功能肯定是实现了父类功能的。也不用一个个看子类的实现,毕竟你只要继承了这个类,你就应该有这个类的功能。关心的底层越少,编程越轻松。

  • 依赖倒置

    所有都依赖于抽象,抽象这个词其实不好理解。我们换一个词,契约,依赖于契约编程。契约这个东西在生活中很常见,比如,上淘宝买一公斤大米,你不需要知道大米是怎么生产的,怎么到商家手中的,你只需要你要的是一公斤大米,大米就会送上门,买一公斤大米的时候就定义了一个契约,契约让你不需要关心任何细节,只要得到结果就可以了。依赖倒置带来的也是这样的效果,契约其实定义你需要的结果,不要去关心细节的实现。

  • 最少知识

    最少知识在生活中其实是处处体现的,比如,我知道用这个菜刀可以切菜就行了,我不需要了解这个菜刀是怎么锻造的,用的是什么材料,我只需要知道这个菜刀可以切菜就够了。在类中的提现就是,我只需要调用这个类的公有方法去实现我需要的功能就可以了。对于功能提供类来说,我只需要把我需要提供的功能暴露出去就可以了,不需要把怎么实现也让第三方知道,毕竟,菜刀只需要让用户知道我真的切菜很好用就好,不需要告诉用户我是怎么锻造的。

  • 接口隔离

    接口,在现实生活中的其实就是代表着功能的意思。想象一下你在买一款微波炉,你只需要微波功能,但是因为微波炉没有做好接口隔离,都是带烧烤功能的,你不得不为了买个微波炉,多付了烧烤功能的费用。编程也是这样,接口实现是有成本的,无端依赖不需要的接口,之后造成实现成本的攀高。

  • 开闭原则

    为什么支持对扩展开放和对修改关闭呢?想象一下你有一台电脑,某一天你需要一个摄像头进行视频聊天,如果是对扩展关闭对修改开放的话,你的电脑估计要回厂,然后让厂商加一个摄像头了。但是对扩展开放就不一样了,直接淘宝买个USB摄像头,插上就能用。在编程中的体现就是,提供接口去获得新的功能。