说到面向对象编程,有一个原则几乎每个程序员都知道,那就是 SOLID 原则。关于它的资料介绍也非常丰富,实践例子也很多。但实际上你很可能把 SOLID 原则都用错了,并且还无意识地一直在滥用它。
之所以这么说,一方面是因为很多时候你都将每一个原则分开使用,容易造成过度解读。比如,在使用接口隔离原则时容易只关心接口,而忽略不同实现,或者不关心接口之间的关系以及和整体系统之间的关系。另一方面是因为它总是能让你无意识地将简单的问题复杂化。比如,明明只需要写一个一次性同步数据的方法,然后写完即扔,但是突然想到 SOLID 原则,于是又搞出来十几个多余的类。有了锤子,总是容易想去找钉子,殊不知有时就完全不需要锤子,只需要一把小刀即可解决问题。
那么 SOLID 原则到底长什么样子?各原则之间有什么联系和区别?该如何正确理解呢?今天,我们就来一起学习下这五大面向对象设计原则,也就是我们所说的 SOLID 原则。
一、五大设计原则概览
2000 年,Robert C. Martin 在他的《设计原理和设计模式》这一论文中首次提出 SOLID 原则的概念。然后,在过去的 20 年中,这 5 条原则彻底改变了面向对象编程的世界,改变了我们编写软件的方式。
SOLID 原则的核心理念是帮助我们构建可维护和可扩展的软件。因为随着软件规模的扩大,一个人维护所有的代码越来越困难,这时就需要更多的人来维护代码,而多人协作的关键在于相互通信与协作,恰好 SOLID 原则提供了这样一个框架。
“SOILD”是由五大原则的英文首字母拼写而成,具体对应情况如下。
- S(Single Responsibility Principle,简称 SRP):单一职责原则,意思是对象应该仅具有一种单一的功能。
- O(Open–Closed Principle,简称 OCP):开闭原则,也就是程序对于扩展开放,对于修改封闭。
- L(Liskov Substitution Principle,简称 LSP):里氏替换原则,程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的。
- I(Interface Segregation Principle,简称 ISP):接口隔离原则,多个特定客户端接口要好于一个宽泛用途的接口。
- D(Dependency Inversion Principle,简称 DIP):依赖反转原则,该原则认为一个方法应该遵从“依赖于抽象而不是一个实例”。
1、单一职责原则
单一职责原则(SRP)的原意是:对一个类而言,应该仅有一个引起它变化的原因。
但对此,我们通常更容易这么来理解:
- 只有一个类或方法;
- 写好就不能修改的类或方法;
- 一个接口对应唯一一个实现。
上面这些理解不能说完全错,但是只抓住了单一职责原则(SRP)本质上重要的两点中的一点——单一,而忘记了另一个也很重要的点——职责。在上一讲中,我们有介绍过“职责”可以定义为 “变化的原因” 。下面我们就来讲讲为什么那三种理解方式不够准确。
- 职责不一定只有一个类或方法,还可能有多个类或方法,比如,上传文件是单一职责,而上传方法、增删改查 URL 方法、校验方法都服务于上传文件。
- 不能修改的类或方法本质上有很多影响因素,比如,代码长时间没有维护、设计时没有预留扩展接口,等等。
- 一个接口对应一个实现并不能说职责是单一的,因为一个接口中可能会存在没有划分清楚的职责。
2、开闭原则(OCP)
开闭原则最初是由 Bertrand Meyer 在 20 世纪 80 年代提出的,被称为“面向对象设计中的最重要原理”。
开闭原则是指软件组件(类、方法、模块等)应该对扩展开发,对修改关闭。这就意味着当你在设计或修改程序代码时,应该尽量去扩展原有程序,而不是修改原有程序。
不过在我看来,开闭原则更像是一个框架的设计原则,而不是具体的业务编码技巧。因为在实际业务编码实现中,需求变化总是快于技术更新,直接修改业务代码的时间成本有时会比扩展的时间成本低很多,所以说,在非常细节的业务编码实现中,只扩展而不修改原始的代码几乎很难做到,反倒是在框架、类库或架构设计中常常更容易实现开闭原则。即便强行在编码实现中这样做,也会导致过多的冗余类产生,并导致最终系统整体调用关系复杂。
3、里氏替换原则(LSP)
里氏替换原则(LSP)这个名字看上去虽然有点奇怪,但是在面向对象编程中,它却是使用频率非常高的一个原则。
里氏替换原则(LSP)的原意是:子类应该能够完全替换掉它的基类。换句话说,在进行代码设计时,应该尽量保持子类和父类方法行为的一致性。这样做的好处在于,即便是扩展子类,也不会丢失父类的特性。同时,里氏替换原则(LSP)也是针对接口编程的最佳实践原则之一,因为某一个接口定义的功能不改变,那么就可以使用很多不同算法的代码来替换同一个接口的功能。
4、接口隔离原则(ISP)
如果说单一职责原则(SRP)是适用于类的设计原则,那么接口隔离原则(ISP)就是适合接口的设计原则。
接口隔离原则(ISP)的原意是:不应该强迫用户依赖于他们不用的方法。那么什么情况下会造成“被强迫”呢?答案就是:当你在接口中有多余的定义时。
5、依赖反转原则(DIP)
依赖反转原则(DIP)的原意是:
- 高层模块不应该依赖底层模块,二者都应该依赖于抽象;
- 抽象不应该依赖于细节,细节应该依赖于抽象。
简单来说,就是我们应该在编程时寻找好的抽象。这里的抽象不是简单地指 Java 中的 interface,而是指可以创建出固定却能够描述一组任意个可能行为的抽象体。
而好的抽象就是指具备一些共性规律并能经得起实践检验的抽象。比如,关系型数据库(RDMS)就是对数据存储与查询的一种正确抽象。再比如,我们非常熟悉的 JDBC 协议就是一种对数据库增删改查使用的正确抽象,还有我们课程后面模块要讲的设计模式也是某种场景下的正确抽象。
当然,好的抽象并不容易找到,更多的时候你还是得做很多定制化的开发。
总之,依赖反转原则(DIP)给我们的启示是:要尽量通过寻找好的抽象来解决大量重复工作的效率问题。
二、五大设计原则之间的关系
虽然 SOLID 原则在面向对象编程中应用广泛,但是你可能更多时候还是在死记硬背式地使用。在我看来,SOLID 原则之间其实是有一定联系的,搞清楚这些联系,不仅能帮助你理解记忆 SOLID 原则,而且还能更好地应用它们。
首先,开闭原则是 SOLID 原则追求的最终目标。为什么这么说呢?因为修改代码非常容易引入 Bug,即便是很小的改动都有可能引起未知的 Bug。而一旦系统因为 Bug 出现故障,担责的一定是我们。没有人愿意担责,所以,我们都更喜欢写新代码而不是修改旧代码。除此之外,在设计之初就尽量以实现开闭原则为目标,它能为你在未来的实际开发中提供更高的代码扩展性。不过,这里需要注意一下,开闭原则不是封闭原则,千万不要把遵守开闭原则当作必要条件,如果代码需要适应现实的需求变化而必须要修改的话,那么这时就应该违反原则。当然尽量还是要做到开闭。
其次,单一职责原则是重要的基础原则,它帮助实现了里氏替换原则、接口隔离原则和开闭原则。你只要仔细分析各个原则的含义就能发现,它们都涉及了两个关键动作:分离和替换。那么是逻辑揉在一起、接口定义模糊的代码容易分离和替换,还是职责单一、接口抽象清晰的代码容易分离和替换呢?答案很明显是后者。之所以说单一职责原则是基础,就是因为要想实现代码的灵活扩展性需要更容易理解的模块。而职责单一的模块,更容易被组合起来用于更大的职责,也能进行快速替换和修改。
最后,依赖反转原则是一种指导原则,同样是用来分离和替换代码的。
文章(专栏)将持续更新,欢迎关注公众号:服务端技术精选。欢迎点赞、关注、转发。