最近在读 Clean Architecture (豆瓣) ,正好说到了设计模式的5大原则。简写成 Solid
,也就是靠谱、牛逼的意思。
Solid
是由如下的5种原则组成:
- SRP - Single Responsibility Principle (单一职责)
- OCP - Open-Closed Principle (开放,封闭)
- LSP - Liskov Substitution Principle (李氏替换)
- ISP - Interface Segregation Principle (接口隔离)
- DIP - Dependency Inversion Principle (依赖倒置)
SRP
简单来说,每一个东西都只有一个职责。这里的 东西 的粒度,可以是一个类,一个文件,一个模块,一个框架等等。
这个原则让我想起了 Unix的哲学:每一个命令只做一件事,并把这事做到极致。所以,经常看到Unix的命令的input/output都是text,这样方便组成pipeline,可以让各种命令相互协作。
对于这个原则的解释还有其他的一些版本,例如:
一个模块,有且只有一个原因令其发生改变。
一个模块只对唯一的用户或模块负责。
为什么要让一个模块只有保持一个责任呢?我觉得有一下的一些原因。
- 简单。简单的东西总是容易维护和理解的。Keep it simple 是让整个架构灵活、可扩展的前提。
- 减少错误。当只有一个责任时,错误的几率总是会最小化。当有更多的责任,那这个模块就会和更多的模块耦合。所以会有更多的原因让这个模块变化,也就增加了bug的概率。
- 高内聚 (cohesion) 。我们希望一个模块是高内聚,低耦合的。
OCP
对扩展开放,对改变封闭
当设计一个类或者一个模块的时候,我们希望遵循能够容易地让使用者扩展,同时不希望让别人来改变类本身的各种功能。
其实,这个原则也好理解。当我们去改变类本身的行为时,势必会改变那些继承和使用这个类的其他用户。这是一个引入bug的危险行为。所以对于改变,我们应该谨慎。但每个使用者又有各自的特殊需求,这时我就应该让类更容易被扩展,让每个使用者自己扩展需求。
以 swift
为例:
final - 不想让其他的类来继承了
open/public - 对于继承是开放的
extension - 可以对类进行扩展
LSP
单从名字上毫无线索。这个原则源自 Barbara Liskov,一位拿了图领奖的女性计算机牛人。
这个原则的定义如下:
What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
实在难理解,说人话就是:
派生类(子类)对象能够替换其基类(超类)对象被使用。
这是继承和复用的基石。
其中包含了4个含义:
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
这其中第1条有时候是很难做到的,我们一般覆盖父类方法的同时会调用父类方法并且写上一些子类特有的操作。另外的3和4可以参看这里:设计模式六大原则(2):里氏替换原则 - OpenFire_ - 博客园
看个例子:
class Bird {
func fly() {
print("Bird is flying")
}
}
class Kingfisher: Bird {
override func fly() {
print("kingfisher is flying")
}
}
class Penguin: Bird {
override func fly() {
print("Penguin cannot fly! :(")
}
}
var bird: Bird = Bird()
bird.fly()
bird = Penguin()
bird.fly()
bird = Kingfisher()
bird.fly()
这里的 bird
相当于一个指针,它只知道指向的是一个 Bird
。这里,Kingfisher
和 Penguin
都是 Bird
的子类,它们可以无缝地替换掉 Bird
,对于 bird
这个指针,它并不需要知道这些细节。
ISP
我不需要的东西,别硬塞给我
其实,这个原则和前面的 SRP 有异曲同工之妙。就是要保持接口的简单,不要把各种职责都塞过来。看看能不能把不要的分离出去。这个原则拆分庞大臃肿的接口成为更小和更具体的接口,从而解耦合。
这里一篇文章 - 接口分离原则(Interface Segregation Principle) - 匠心十年 - 博客园 提到了多继承的问题来解决ISP的问题。这里提一下在Swift中是没有多继承的,它用 protocol
来部分解决了这个问题。同时,我觉得这样也更优雅。因为这是对 抽象 在编程,而非 实现 。
DIP
这个原则可以归纳为两点:
- 高层模块不应当依赖于低层模块。两者都应该依赖于抽象。
- 抽象不应依赖于实现,实现应该依赖于抽象。
看个例子:

比如我们现在有个自己的App (A),然后使用了 SDWebImage (B),现在我们想要用 Kingfisher (C) 来替换B。这时就变得异常痛苦,因为C和B的接口并不相同。而A已经非常依赖于B的具体接口。
如果我们刚开始就让A和B都依赖于抽象接口 (Interface A),那么当要使用C时,只需让C实现 Interface A,或者实现一个Adapter把C接入然后转换成它的接口就行了。这就是对抽象而不是具体编程的好处。
网友评论