单一职责原则(SRP)
应该有且仅有一个原因引起类的变更,换句话说,类的职责应尽量单一。
为什么?如果类的职责单一,那么只有它负责的部分出现了变化,它才会被修改。这个类被修改的次数越少,那么其他模块依赖它的部分修改的次数也就少了。这可以提高系统的可维护性。
该原则试用于接口、类、方法。
实际使用时,因为对“职责”划分的标准不同,所以很难实现真正的单一职责。一般建议是,接口职责单一(接口尽量少修改),类不一定单一,一个类可以实现多个接口,但类尽量保证只有一个原因需要修改。
里氏替换原则(LSP)
父类能出现的地方,子类就能出现。相反,子类出现的地方,父类不一定能出现。即,子类能替换父类,但父类不能替换子类。
通俗地讲,代码里使用了父类的地方,把父类类型直接替换成子类后,对运行结果无影响。但如果反过来,使用了子类的地方,不能替换成父类,要么编译不通过要么运行结果变了。
做到这个原则,要遵守4个条件:
- 子类必须全部实现父类方法
- 子类可以有自己的“个性”,即有自己的 public 方法或自己的子类
- 子类重载父类方法时,输入参数的范围比父类方法的大。如父类方法参数是 HashMap 类型,子类的应该是 Map 类型。注意这里是重载不是重写
- 子类重写父类方法时,返回参数的范围要么与父类一致,要么缩小,总之不能比父类返回的范围大。注意这里是重写不是重载
这4个条件是针对场景制定的,若子类不需要增加 public 方法,也不需要重载或重写父类方法,就只需遵守第一条就够。
对于第二条,若子类拥有了“个性”,那么使用这个子类的模块就与一个具体的类强耦合,这个模块不能通过依赖父类来依赖子类(因为父类没有子类的个性方法)。除非有这种需求场景,否则应该避免子类的“个性”,才能完美替换父类和子类。
子类在重载或重写时做到了第三、第四点,可以保证,用子类替换父类时,既不会编译报错,也不会影响运行结果。
平时用到里氏替换的场景:做设计时,经常先定义一个接口或抽象类,然后编码实现,声明这个类的变量时,是写接口或抽象类类型,但实际传入的对象类型是子类类型,这里就已经使用了里氏替换原则。
方法重写是子类 Override 父类方法,方法重载是方法名相同但参数不同
依赖倒置原则(DIP)
定义:
- 模块间的依赖是通过接口或抽象类产生,实现类之间没有直接的依赖关系
- 接口或抽象类不依赖于实现类
- 实现类依赖接口或抽象类
更加精简的定义是:“面向接口编程”。
这个原则可以结合里氏替换原则使用。因为里氏替换原则使得父类可以被替换为子类,所以,如果把依赖的模块声明为接口或抽象类,那么就能通过“注入”的方式直接换成它们的子类。
Spring 的依赖注入就体现了这个原则。我们声明注入的对象都是以接口类型声明,由 IoC 容器注入实现类。若有多个实现类,我们可以控制注入哪个,注入哪个实现类都不会影响原依赖关系和逻辑。
接口隔离原则(ISP)
客户端不应该依赖它不需要的接口。即,提供给外部的接口最好是“定制化”的,外部模块需要什么就只给什么,其他的都不开放。
如果一个接口有10个方法,提供给多个外部模块使用,一个模块可能只使用其中一两个,这就不符合接口隔离原则。接口隔离原则希望接口尽量“小”,即只有少量的 public 方法。
给外部模块提供太多方法会导致,也许口头约定只使用一两个接口,但因为外部模块开发组的疏忽使用了不该使用的方法且你不知情,增加风险。
要做到接口隔离,首先要决定接口的粒度,接口粒度太小,会导致接口数量太多;粒度太大,灵活性降低(因为一个接口同时被多个模块使用,修改时有影响多个模块的风险)。
接口粒度如何决定,只能靠实践、经验和感悟。
迪米特法则
类中方法不应该引入“朋友类”之外的类对象。“朋友类”的定义是,出现在成员变量、方法的输入输出参数声明中的类是朋友类,而出现在方法内部的类则是“非朋友类”。这个原则规定,方法内部不应该出现“非朋友类”,应该只有朋友类,这样做是为了降低类之间的耦合性。
同时,这个原则也强调,接口对外提供的方法要尽量少,即朋友类之间的依赖也要尽量少。这点和接口隔离原则一致。
遇到问题:一个方法既可以放在本类,又可以放在其他类,到底应该放在哪儿?这个原则告诉我们,如果这个方法放在本类中,即不增加类间耦合,也不对本类产生负面影响,那就放在本类中。这也是为了本类在使用这个方法时不用依赖其他类。
这个原则的核心观念就是类间解耦,类的弱耦合。但结果可能是一个类要访问另一个类时,中间有很多跳转类。一般来说,一个类访问到另一个类的跳转次数最多不超过2次,否则系统复杂性太大,可维护性小。
开闭原则(OCP)
对扩展开放,对修改关闭。换句话说,一个软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化。“软件实体”指的是,一个接口、类、方法,或一部分逻辑模块。
开闭原则是最基础的原则,以上5个原则是它的具体形态。
实现方法:
- 抽象约束
- 通过抽象限定扩展范围
- 方法参数类型、成员变量尽量使用接口或抽象类,避免使用实现类
- 抽象层尽量稳定,一旦确定就不允许修改
- 元数据控制模块行为。“元数据”就是配置参数(从配置文件或数据库取),通过配置就可以实现大部分的功能,不需要修改代码。举例:通过配置IP黑名单,就可以控制不允许某个IP访问接口。
- 封装变化。若预测到将来可能出现的变化,尽量把它封装在一个接口中,这样这个变化只影响一个接口。23种设计模式就是用于封装变化的。
- 制定项目章程。要求成员按照项目组指定的开发规范编写,如要求做到上面提到的几点,使得代码不杂乱,将来可维护性强。