什么是设计模式
每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动
在历史性著作《设计模式:可复用面向对象软件的基础》一书中描述了23种经典面向对象的设计模式,创立了模式在软件设计中的地位。由于《设计模式》一书确定了设计模式的地位,所以通常所说的设计模式隐含地表示"面向对象设计模式"。但这并不意味"设计模式"就等于"面向对象设计模式",在Swift
中,我们编程会更倾向于面向协议的设计模式。
设计模式六大原则
虽然各种编程语言因为各自语言的特性,会偏好不同的设计模式,但是设计模式的原则是不会改变的,目前有六大原则:
- 单一职责原则(Single Responsibility Principle)
- 里式替换原则(Liskov Substitution Principle)
- 依赖倒置原则(Dependence Inversion Principle)
- 接口隔离原则(Interface Segregation Principle)
- 迪米特法则(Law Of Demeter)
- 开闭原则(Open Closed Principle)
直接解释原则有点抽象,我们先看一个案例,看完案例后再解释原则会容易理解很多
设计案例
我们做一个简单一点的需求,做一个画板,上面画不同的形状,然后做两种方案对比下:
方案一:
swift
struct Point {
var x = 0
var y = 0
}
struct Line {
var start: Point
var end: Point
}
struct Rect {
var leftUp: Point
var width = 0
var height = 0
}
class MainForm {
var sharps: [Any] = []
func draw(sharp: Any) {
sharps.append(sharp)
}
func OnPaint() {
sharps.forEach { sharp in
if let point = sharp as? Point {
// draw point
} else if let line = sharp as? Line {
// draw line
} else if let rect = sharp as? Rect {
// draw rect
}
}
}
}
let form = MainForm()
form.draw(sharp: Point(x: 12, y: 13))
form.draw(sharp: Rect(leftUp: Point(x: 0, y: 0), width: 10, height: 15))
form.draw(sharp: Line(start: Point(x: 0, y: 1), end: Point(x: 10, y: 11)))
form.OnPaint()
方案二:
swift
protocol Sharp {
func draw()
}
struct Point: Sharp {
var x = 0
var y = 0
func draw() {
// draw point
}
}
struct Line: Sharp {
var start: Point
var end: Point
func draw() {
// draw Line
}
}
struct Rect: Sharp {
var leftUp: Point
var width = 0
var height = 0
func draw() {
// draw Rect
}
}
class MainForm {
var sharps: [Sharp] = []
func draw(sharp: Sharp) {
sharps.append(sharp)
}
func OnPaint() {
sharps.forEach { sharp in
sharp.draw()
}
}
}
let form = MainForm()
form.draw(sharp: Point(x: 12, y: 13))
form.draw(sharp: Rect(leftUp: Point(x: 0, y: 0), width: 10, height: 15))
form.draw(sharp: Line(start: Point(x: 0, y: 1), end: Point(x: 10, y: 11)))
form.OnPaint()
我们画了3个形状,最后让图案显示出来。上面两种方案明显第二种会好一点,第二种更符合设计模式原则,我们通过第二种案例讲解下设计原则
六大原则详解
单一职责原则(Single Responsibility Principle)
单一职责原则,简称SRP。其定义是应该有且仅有一个类引起类的变更,这话的意思就是一个类只担负一个职责。就如同上面的案例里,原本各个形状的draw
实现都是在MainForm
中实现的,现在各自形状负责各自的draw
实现。现在逻辑比较简单,所以从MainForm
拆分出来没感觉出什么优势,但是随着逻辑的增多,职责越单一,被修改的原因就越少。
单一职责原则的优点:
- 类的复杂性降低,实现什么职责都有明确的定义;
- 逻辑变得简单,类的可读性提高了,而且,因为逻辑简单,代码的可维护性也提高了;
- 变更的风险降低,因为只会在单一的类中的修改。
当然,如果类一味追求单一职责,有时会造成类的大爆炸,单一职责原则提出了一个编写程序的标准,用"职责"或"变化原因"来衡量接口或类设计得是否优良,但是"职责"和"变化原因"都是不可以度量的,因项目和环境而异。
里式替换原则(Liskov Substitution Principle)
先看下它的定义:
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2的时候,程序P的行为都没有发生变化,那么类型T2是类型T1的子类型。
看起来有点绕口,它还有一个简单的定义:
所有引用基类的地方必须能够透明地使用其子类的对象。
通俗来讲,只要有父类出现的地方,都可以使用子类来替代,而且不会出现任何错误或者异常,所以使用继承的时候需要谨慎。如果可以,优先使用对象组合,而不是类继承。
继承的优点就不多说了,代码共享,减少创建类的工作量,提高代码的重用性等。但继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法,增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改。
所以在设计父类的时候往往定义成抽象类(Java
)或者虚类(C++
),然后定义抽象方法或者虚函数,显式的让子类实现抽象方法或者虚函数。当然,在Swift
中没有这样的概念,与之对应的就是协议(protocol
)了。使用了该种设计,父类本身就没有具体的实现,或者实现的内容比较少,很少会违背里式替换原则。
依赖倒置原则(Dependence Inversion Principle)
先看下定义:
高层模块不应该依赖底层模块,二者都该依赖其抽象;
抽象不应该依赖细节;
细节应该依赖抽象;
高层模块就是调用端,低层模块就是具体实现类。抽象就是指接口或抽象类。细节就是实现类。
在我们上面的案例里,MainForm
就是高层模块,各形状的实现就是底层模块。在案例一中,我们MainForm
在实现OnPaint
的方法的时候,既依赖了底层模块也依赖了实现细节,完全违背了依赖倒置原则。
而在案例二中,MainForm
没有依赖各形状的实现,而是MainForm
和各形状都依赖于各形状的抽象protocol Sharp
,抽象protocol Sharp
不依赖于各形状draw
的实现细节,反而实现细节因为protocol Sharp
的声明而存在,这样完美遵循了依赖倒置原则
接口隔离原则(Interface Segregation Principle)
不应该强迫客户程序依赖它们不需要的方法
接口应该小而完备
首先说下什么是接口,一个模块对外暴露给外界使用的函数调用就是接口,它大多数呈现形式是一个类,当然也包含抽象类,在Swift
中对应的就是协议。对于上面案例二中的MainForm
来说,Sharp
就是接口。那什么是"不应该强迫客户程序依赖它们不需要的方法"呢?
举个例子,假如我现在有个功能,需要得到图形的面积,那么我们是否应该在Sharp
里面添加一个获取面积acquireArea
的接口呢?答案是否定的,因为对于MainForm
来说,acquireArea
这个接口是不需要的,而我们每一个形状都要为了这个接口而实现这个功能,但对于那些不封闭的图形(比如点、线、曲线等)acquireArea
是没有意义的,当然你可以返回为0,但这个是不合理的。正确的做法应该是在新增一个接口(协议),比如叫AreaSharp
,然后在那些有封闭区域的形状中遵守协议并实现acquireArea
方法:
swift
protocol AreaSharp {
func acquireArea() -> CGFloat;
}
struct Rect: Sharp, AreaSharp {
var leftUp: Point
var width = 0
var height = 0
func draw() {
// draw Rect
}
func acquireArea() -> CGFloat {
return CGFloat(width * height)
}
}
这样,MainForm
使用Sharp
接口,而刚才想新增得到图形面积的功能用AreaSharp
接口
那什么是"接口应该小而完备"呢,这个和单一职责原则那边有点像,不能过度设计,举个极端点例子,你不能为每一个需求的方法来设计一个接口类吧,所以接口虽然尽可能小,但是方法能整合在一起的,也尽可能要放一个接口类里。
迪米特法则(Law Of Demeter)
简称LoD,也被称为最少知识原则,它描述的规则是:
一个对象应该对其他对象有最少的了解
法则强调了以下两点:
- 从被依赖者的角度来说:只暴露应该暴露的方法或者属性
- 从依赖者的角度来说:只依赖应该依赖的对象
这个应该比较好理解,一个类不要暴露多余的功能函数给外界,防止外界乱调用出现异常。
开闭原则(Open Closed Principle)
软件实体应该对扩展开放,对修改封闭。
"扩展"就是新增的意思,我们还是看我们的那个案例,比如说,我们现在想增加一个圆的功能,对于方案一来说,除了必须要定义一个Cycle
的结构体外,那么还需要在MainForm
的OnPaint
方法里实现圆的绘制,这个就属于修改了,所以方案一不是一个好的设计。
反观方案二,我们只要新增如下定义就能完美解决问题:
swift
struct Cycle: Sharp {
var dot: Point
var radius: CGFloat = 0
func draw() {
// draw Cycle
}
}
方案二只有扩展而没有修改
总结
一款程序的实现是复杂,而在敏捷开发的现在,需求变化又是迅速的,所以我们设计模式的初衷是抵御变化。
我们需要在代码中分清哪些是会"变化"的,哪些是"不变"的,我们尽可能把变化的部分关在一个笼子里,不要四散在各处代码中,也就是所谓的高内聚低耦合。
当然,我所说的"变化"和"不变"是相对的,没有完全不变的代码,如果有个逻辑三个月会变,另外一个逻辑三年才会变,那么相对来说,前者是"变化"的,后者是"不变"的。
后续,我可能会讲一些经典的设计模式。