

超易懂!原来SOLID原则要这么理解!
source link: https://segmentfault.com/a/1190000039201899
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

说到 SOLID 原则,相信有过几年工作经验的朋友都有个大概印象,但就是不知道它具体是什么。甚至有些工作了十几年的朋友,它们对 SOLID 原则的理解也停留在表面。今天我们就来聊聊 SOLID 原则以及它们之间的关系。
什么是SOLID原则
SOLID 原则其实是用来指导软件设计的,它一共分为五条设计原则,分别是:
- 单一职责原则(SRP)
- 开闭原则(OCP)
- 里氏替换原则(LSP)
- 接口隔离原则(ISP)
- 依赖倒置原则(DIP)
单一职责原则(SRP)
单一职责原则(Single Responsibility Principle),它的定义是:应该有且仅有一个原因引起类的变更。 简单地说:接口职责应该单一,不要承担过多的职责。 用生活中肯德基的例子来举例:负责前台收银的服务员,就不要去餐厅收盘子。负责餐厅收盘子的就不要去做汉堡。
单一职责适用于接口、类,同时也适用于方法。例如我们需要修改用户密码,有两种方式可以实现,一种是用「修改用户信息接口」实现修改密码,一种是新起一个接口来实现修改密码功能。在单一职责原则的指导下,一个方法只承担一个职能,所以我们应该新起一个接口来实现修改密码的功能。
单一职责原则的重点在于职责的划分,很多时候并不是一成不变的,需要根据实际情况而定。单一职责能够使得类复杂性降低、类之间职责清晰、代码可读性提高、更加容易维护。但它的缺点也很明显,就是对技术人员要求高,有些时候职责难以区分。
我们在设计一个类的时候,可以先从粗粒度的类开始设计,等到业务发展到一定规模,我们发现这个粗粒度的类方法和属性太多,且经常修改的时候,我们就可以对这个类进行重构了,将这个类拆分成粒度更细的类,这就是所谓的持续重构。
开闭原则(OCP)
开闭原则(Open Closed Principle),它的定义是:一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。 简单地说:就是当别人要修改软件功能的时候,使得他不能修改我们原有代码,只能新增代码实现软件功能修改的目的。
这听着有点玄乎,我来举个例子吧。
这段代码模拟的是对于水果剥皮的处理程序。如果是苹果,那么是一种拨皮方法;如果是香蕉,则是另一种剥皮方法。如果以后还需要处理其他水果,那么就会在后面加上很多 if else 语句,最终会让整个方法变得又臭又长。如果恰好这个水果中的不同品种有不同的剥皮方法,那么这里面又会有很多层嵌套。
if(type == apple){ //deal with apple } else if (type == banana){ //deal with banana } else if (type == ......){ //...... }
可以看得出来,上面这样的代码并没有满足「对拓展开放,对修改封闭」的原则。每次需要新增一种水果,都可以直接在原来的代码上进行修改。久而久之,整个代码块就会变得又臭又长。
如果我们对剥水果皮这件事情做一个抽象,剥苹果皮是一个具体的实现,剥香蕉皮是一个具体的实现,那么写出的代码会是这样的:
public interface PeelOff { void peelOff(); } public class ApplePeelOff implement PeelOff{ void peelOff(){ //deal with apple } } public class BananaPeelOff implement PeelOff{ void peelOff(){ //deal with banan } } public class PeelOffFactory{ private Map<String, PeelOff> map = new HashMap(); private init(){ //init all the Class that implements PeelOff interface } } ..... public static void main(){ String type = "apple"; PeelOff peelOff = PeelOffFactory.getPeelOff(type); //get ApplePeelOff Class Instance. peelOff.pealOff(); }
上面这种实现方式使得别人无法修改我们的代码,为什么?
因为当需要对西瓜剥皮的时候,他会发现他只能新增一个类实现 PeelOff 接口,而无法再原来的代码上修改。这样就实现了「对拓展开放,对修改封闭」的原则。
里氏替换原则(LSP)
里氏替换原则(LSP)的定义是:所有引用基类的地方必须能透明地使用其子类的对象。 简单地说:所有父类能出现的地方,子类就可以出现,并且替换了也不会出现任何错误。 例如下面 Parent 类出现的地方,可以替换成 Son 类,其中 Son 是 Parent 的子类。
Parent obj = new Son(); 等价于 Son son = new Son();
这样的例子在 Java 语言中是非常常见的,但其核心要点是:替换了也不会出现任何的错误。 这就要求子类的所有相同方法,都必须遵循父类的约定,否则当父类替换为子类时就会出错。 这样说可能还是有点抽象,我举个例子。
public class Parent{ // 定义只能扔出空指针异常 public void hello throw NullPointerException(){ } } public class Son extends Parent{ public void hello throw NullPointerException(){ // 子类实现时却扔出所有异常 throw Exception; } }
上面的代码中,父类对于 hello 方法的定义是只能扔出空指针异常,但子类覆盖父类的方法时,却扔出了其他异常,违背了父类的约定。那么当父类出现的地方,换成了子类,那么必然会出错。
其实这个例子举得不是很好,因为这个在编译层面可能就有错误。但表达的意思应该是到位了。
而这里的父类的约定,不仅仅指的是语法层面上的约定,还包括实现上的约定。有时候父类会在类注释、方法注释里做了相关约定的说明,当你要覆写父类的方法时,需要弄懂这些约定,否则可能会出现问题。例如子类违背父类声明要实现的功能。比如父类某个排序方法是从小到大来排序,你子类的方法竟然写成了从大到小来排序。
里氏替换原则 LSP 重点强调:对使用者来说,能够使用父类的地方,一定可以使用其子类,并且预期结果是一致的。
接口隔离原则(ISP)
接口隔离原则(Interface Segregation Principle)的定义是:类间的依赖关系应该建立在最小的接口上。 简单地说:接口的内容一定要尽可能地小,能有多小就多小。
举个例子来说,我们经常会给别人提供服务,而服务调用方可能有很多个。很多时候我们会提供一个统一的接口给不同的调用方,但有些时候调用方 A 只使用 1、2、3 这三个方法,其他方法根本不用。调用方 B 只使用 4、5 两个方法,其他都不用。接口隔离原则的意思是,你应该把 1、2、3 抽离出来作为一个接口,4、5 抽离出来作为一个接口,这样接口之间就隔离开来了。
那么为什么要这么做呢? 我想这是为了隔离变化吧! 想想看,如果我们把 1、2、3、4、5 放在一起,那么当我们修改了 A 调用方才用到 的 1 方法,此时虽然 B 调用方根本没用到 1 方法,但是调用方 B 也会有发生问题的风险。而如果我们把 1、2、3 和 4、5 隔离成两个接口了,我修改 1 方法,绝对不会影响到 4、5 方法。
除了改动导致的变化风险之外,其实还会有其他问题,例如:调用方 A 抱怨,为什么我只用 1、2、3 方法,你还要写上 4、5 方法,增加我的理解成本。调用方 B 同样会有这样的困惑。
在软件设计中,ISP 提倡不要将一个大而全的接口扔给使用者,而是将每个使用者关注的接口进行隔离。
依赖倒置原则(DIP)
依赖倒置原则(Dependence Inversion Principle)的定义是:高层模块不应该依赖底层模块,两者都应该依赖其抽象。抽象不应该依赖细节,即接口或抽象类不依赖于实现类。细节应该依赖抽象,即实现类不应该依赖于接口或抽象类。 简单地说,就是说我们应该面向接口编程。通过抽象成接口,使各个类的实现彼此独立,实现类之间的松耦合。
如果我们每个人都能通过接口编程,那么我们只需要约定好接口定义,我们就可以很好地合作了。 软件设计的 DIP 提倡使用者依赖一个抽象的服务接口,而不是去依赖一个具体的服务执行者,从依赖具体实现转向到依赖抽象接口,倒置过来。
SOLID 原则的本质
我们总算把 SOLID 原则中的五个原则说完了。 但说了这么一通,好像是懂了,但是好像什么都没记住。 那么我们就来盘一盘他们之间的关系。ThoughtWorks 上有一篇文章说得挺不错,它说:
- 单一职责是所有设计原则的基础,开闭原则是设计的终极目标。
- 里氏替换原则强调的是子类替换父类后程序运行时的正确性,它用来帮助实现开闭原则。
- 而接口隔离原则用来帮助实现里氏替换原则,同时它也体现了单一职责。
- 依赖倒置原则是过程式编程与面向对象编程的分水岭,同时它也被用来指导接口隔离原则。
简单地说:依赖倒置原则告诉我们要面向接口编程。当我们面向接口编程之后,接口隔离原则和单一职责原则又告诉我们要注意职责的划分,不要什么东西都塞在一起。当我们职责捋得差不多的时候,里氏替换原则告诉我们在使用继承的时候,要注意遵守父类的约定。而上面说的这四个原则,它们的最终目标都是为了实现开闭原则。
参考资料
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK