SOLID
五原则
- Single responsibility principle
- open/closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
S.O.L.I.D 五大原则首写字母,每个字母代表一个原则,是面象对象开发的重要原则。遵循这五个原则,使我们的代码更加高内聚低偶合,可读可维护性强。使我们的代码对需求变化更加友好。
Single responsibility principle

单个类应该只承担一种职责,做好一件事,把负责不同功能职责的代码分离到不同的类中。
当需求变化时,反映在功能职责上的变化,一种职责的变化代表一个维度上的变化。如果多种职责的代码偶合到一个类里,使得代码在单个维度上的变化会有多个维度的影响,使变化带来的修改变得复杂,不可维护。现实中,偶合度高的变化是我们良好的预先设计崩溃的一个重要原因,高偶合使得我们的设计变得十分脆弱。
举个例子: 类:Rectangle有两个方法:draw()画图方法;area()计算面积方法,应用程序即可以调用Rectangle类完成界面画图功能,也可以调用Rctangle类计算面积。如下图所示:

两个不同的需求职责都由Rectangle类完成,这就违反了Single responsibility principle,画图和计算面积两种不同的需求职责的变化频率可以预计是不一样的,最终会引入高偶合的代码。好的设计应该把两种职责分离开来.

计算面积的职责由Rectangle完成,画图的职职由GeometricRectangle类完成。
怎样么定义职责?

在单一职责原则里,职责的定义是“变化的原因”。如果你有多于一个动机去修改一个类,那么这个类就是多于一个职责。
再看一个例子
interface Modem
{
public void dial(String pno);
public void hangup();
public void send(char c);
public char recv();
}
上面的Modem接口违反了单一职责原则,拨号和挂钩断应该是针对连接的管理,而发送和接收方式应该是针对内容的管理.
让我们重构成如下:

我们两部分职责分拆到两个不同的接口,但是Modem接口还是实现了两部分职责,那么是否需要据Modem的实现也拆开,我们需要这样的衡量:变化导致两部分职责代码修改是否总是同时发生,如果这两部分职责总是同时变化,则我们不需要拆分,拆分的同时也会引入复杂度。
因此,仅当变化发生时,变化的轴线才有实际意义,如果没有征兆,则不需要重构,因为拆分的同时也会引入复杂性。
do one thing and do it well
单一职责的核心思想是:做好并只做一件事,无论是方法还是类。
单一职责是最简单的原则之一,但是也是只难实践的原则之一。 关联不同的职责在一起是我们的天性,找到并分离这些职责就是软件计真正要做的。
Open/Closed Prininple

开闭原则,代码对扩展开放,对修改关闭。已经发布的代码,已经有其他客员端,或者模块依赖的情况下,不应该再修改其代码改变其特性,除非是修复BUG或者重构。因为修改已经发布代码的特性,会影响依赖这部分代码的客户端或者其他模块。
程序员都会有这样的经历,当新增特性或者需求变化时,我们常常通过新增参数,修改旧代码来添加新特性。随着项目的演化,我们在旧的方法里的参数越来越多,方法越来越长,类越来越大。代码的偶合度越来越大,越来越难以应对后续的需求变化。
Add functionality by adding new code, not by modify old code

理想的情况下,我们应该追求只通过新增代码来添加新的特性,但在实践中,这是一个逐步演化的过程。一开始你可能很好的识别出哪些代码将会变化,但是当变化或者添加新特性时并需要改动旧代码时,我们应该重构他,把变化部分抽象出来。抽象出一组行为,成为接口或者类(一组或者一个)。请记住,仅抽象频繁变化部分,不成熟的抽象同样会引入复杂度。同样需要注意的是,接口应该是在演化过程中产生的,不应该是被设计出来的。
看下一个例子:
public void doSomething(int type){
if(type == 0){
//business 1
}else if(type == 1){
//business 2
}else if(type ==2){
//buiness 3
}
}
上面的例子中,如果业务中,type代表的业务类型总是有变化,那么每次变化或者新增业务,则需要频繁修改这代码,带来风险,好的办法是把变化部分抽象出来,使用多态的方式实现。
在实践当中,我们追求open/closed 原则,但我们很难做出完美的提前设计,准确提前预计到变化,过多的提前设计带来过度设计的坏味道。
所以在实践当中,我们往往通过重构演化我们的代码,使我们的代码具有良好的open/closed原则
Liskov Substitution priciple
定义:subtypes must be substitutable for their base types
把使用父类的引用替换成子类,都不会出现正常的问题。
例子1:

如上图所示,正方形是一种特殊长方形,但是当正方形继承长方形就会违反Liskov substisution原则。长方形有长和宽两个属性,正方形的长和宽相等。如果正方形继承长方形,每当正方形修改长的同时还要修改宽,则在使用父类的地方,则不可能使用子类来替换。
另一个典型的例子是:removal of features

如上面的代码片段,有一个标准的实现CURD的接口,但在种场景下你可能需要一个只读的实现,实现了标准接口,在修改,删除,更新方法上直接抛出异常,实际上是移除了多个方法,这种设计违反了LSP。
在我们实际写代码过程中有一个常见的现象,当你的接口成员越多,越容易违反LSP。
危害性
违反LSP的危害
- 破坏开闭原则,如果在使用父类的场景中还需要关心哪种子类的实现,那就没有办法关闭对于子类实现的修改。
- 代码变的脆弱,在使用父类的地方,期望某种子实的实现,使得代码对于变化十分脆弱,不能很好的应对变化。
LSP是我们在设计抽象对象过程中的重要原则,保证我在抽象过程中的正确性。对于某个接口的实现,都不应该会景响系统的正确性,这种正确性是基于上下文相当的,系统相关的。
Interface Segregation Principle
Many client-specific interfaces are better than one general-purpose interface . No client should be forced to depend on methods it does not use.
接口隔离原则,为多个客户端按照职责不同定义不同的接口,比使用一个通用接口要好,客户端或者说调用者应该只关心他所关心的接口,与当前调用无关的接口不应该偶合在一起。接口隔离可以解决LSP与SRP遇到的问题。
接口隔离味意着组件更专注某件事情,更小。小的组件可以组合成更加复杂的系统,不同职责的接口偶合在一起使系统应对变化时更加脆弱。同时当你的接口拥有太多的接口方法,会产生过于庞大的类,导致接口与调用者之间产生有害的依赖。当调用者的需求有变化时,会影响到其他的调用者,应当把过于复杂的接口按不同的功能职责拆分成多个单位一职责的接口。
在设计接口时考虑两个问题:
- Who owns the interface?
- Who defines the interface?
客户端(调用者)不应该依赖不需要的接口,谁拥有接口就应该谁来定义接口,客户端拥有接口,就应该由客户端来定义接口,不应该因为设计需要或者实现类需要或者服务端的原因而定义接口,
Dependency Inversion Principle
- High-level modules should not depend on low-level modules . Both should depend on abstractions.
- Abstractions should not depend on details . Details should depend on abstractions.
高层次的模块和低层次的模块代码之间都不应该直接相互依赖,他们应该都依赖抽象;
抽象不应该依赖实现,应该实现依赖抽象。
传统的依赖关系
高层直接依赖底层,这种直接依赖关系是紧耦合的,底层的变动会直接影响高层的功能。

依赖倒置

设计良好的面象对象的程序,其依赖关系应该象上图所示,这使得高层与底层之间的耦合度更低,这是框架设计的核心原则,框架应该有非常清晰的层次定义,每层通过一个定义清晰,受控的接口向上提供高内聚的一组服务。
接口所有权的倒置
倒置的不仅是依赖关系,也是接口所有权的倒置,客户端拥有抽象接口的所有权,接口应该由客户端来定义,服务提供者则是接口的实现与派生。如下图所示,接口由上层定义,由下层实现

总结

- SOLID的顺序不代表重要性,所有这些原则有很强的关联性。如果你只应用其中一种原则,你很难有大的收获。
- 只有当你同时使用这些原则时,才能发挥出真正作用。所以你不能说我这周应用SRP,下周应用OCP。而是应该一小块一小块代码地修改,每一小块代码都朝着SOLID的方向努力。
下面几点是需要注意的:
- 使用设计原则来去除代码中的坏味道,如果没有坏味道那就没有必要使用设计原则,不能因为是设计原则就无条件地应用。
- 有需要时才进行抽象。
- 不要重复自己。不要被同招打倒。事不过三。
- 我们有相对简单和有效的方式去接近理想的情况。
- 大设计原则都是理想的,那么我们是不是必须完全遵守他们,其实不是一定的。要结合实际情况,适合才是最好的。
推荐阅读
