什么是“设计模式”
什么是“设计模式”
众所周知,算法、数据结构 和 设计模式一直都是软件开发中讨论话题非常多的几个领域。
如果说 数据结构 是一种
对数据的组织形式
(例如,可以把它看作一棵长满果实的果树)。那么 算法 则给出了几种
摘果子的方式
(例如,深度优先摘取
和广度优先摘取
)。而设计模式则是针对整个果园的一种
采摘管理方式
,它类似于蓝图
,指明了低成本与高效
的摘取方法。
如果从成本和效率方面来衡量的话,算法和数据结构 在某种程度上,也算是一种处理数据的高效 模式。
模式的概念最初是由克里斯托佛·亚历山大在其著作《建筑模式语言》中首次提出的。例如哥特式风格、巴洛克式建筑、中国古代园林风格等不同风格的建筑,都是不同的建筑模式
。
埃里希·伽玛
、约翰·弗利赛德斯
、拉尔夫·约翰逊
和理查德·赫尔姆
这四位作者受到了模式
概念的启发,在1994年共同出版了《设计模式:可复用面向对象软件的基础》一书,将设计模式的概念应用到程序开发领域中。该书提出23个
模式,以此来解决面向对象程序设计中的各种问题。
由于原书名太长,人们将其简称为四人组(Gang of Four,GoF)的书,并且很快进一步简化为GoF书
。
设计模式所遵循的原则
代码复用原则
代码复用可以说是站在巨人的肩膀上
前行,这个巨人
可以是别人,也可以是自己。
例如,当需要实现一个操作文件的功能时,如果已经有了现成的代码,大多数人都会毫不犹豫地去捡便宜。既然都已经有了,何必去费那个力气呢?时光宝贵,生命不应该浪费在重复的、毫无意义的事情上,不是吗?
但即使是想偷懒,也要知道组件间是否耦合?是否存在硬编码?是否需要满足特定的前提?......等诸如此类的问题。
就像GoF的作者之一埃里希·伽玛所说,复用 有三个层次。
在最底层,可以
复用
类、类库、容器及其他。在中间层,这就是
模式
,一组类的关系及其互动的方式的描述,这类似于一个针对具体问题的微型解决方案。
扩展性原则
变化是唯一不变的事情
。
流程、功能、界面,甚至操作平台都会发生改变,例如从PC端迁移到移动端。
如果变化一次就要全部重写一次代码,那会把人逼疯。
不过这也有好的一面:至少还有人在关心这个破代码。
封装变化原则
为了将变更造成的影响最小化,就要想办法将这些可能发生变化的地方放给封装
起来——例如,潜艇或军舰都被设计成一小段一小段的隔间,这些隔间之间都有防水闸门。潜艇或军舰遭受打击或爆破时,只要关闭隔间的闸门,就能立即阻隔涌进来的海水,将损失减到最小。

对于软件开发也是一样。
例如一个电子商务网站,如果采用这种类似于潜艇结构的设计,那么就可以把会员、商铺、商品、购物车、支付、营销、统计等功能看作是一个个不同的舱室
,即使支付功能暂时不可用,也不影响会员浏览商品与放入购物车。
面向接口开发原则
在很多不同的编程语言中,都对行为进行了抽象,这种抽象是独立于语言具体的实现机制而存在的。这些行为,对于语言来说,可以称之为接口
。
以动物的继承结构来说,不同的物种其进食的方式是不一样的。哺乳类一般是咀嚼,大部分爬行类和几乎所有的鸟类都是生吞。
如果针对每个物种单独开发吃东西
这一功能,恐怕工作量不小。
如果按照面向接口的开发原则,只需要将吃东西
这一行为抽象成一个接口,然后针对不同物种执行不同的具体动作
即可,这种多态
机制大部分面向对象的编程语言都已具备——这至少免去了开发时每次都要重新创建食物
这一重复性工作——这也符合上面所说的 封装变化原则。
组合优于继承原则
面向对象编程的便利性之一就是可以用继承来偷懒
——子类直接复用父类的属性和方法而不必从头开始。
只是这种便利也是有代价的。
子类无法增加或减少父类的方法,如果确实要这么做的话。
反过来,只要父类的方法有变化(方法名称、方法参数、方法数量等),子类都必须要跟着变——谁让你贪便宜的?
如果继承结构复杂庞大,会让人无从下手:因为哪怕改一个地方,就可能会有很多地方报异常。
组合 是代替 继承 的另一种选择。这有点类似于 所有权 与 经营权 的关系。
继承代表 is-a(是一个) 的关系。例如,李四
是
张三的一个
儿子。组合代表 has-a(有一个) 的关系。例如,李四
有一个
叔叔叫王五。
这种既能利用对方功能而又不必担心对方死活
的方式,是非常利于开发的。
单一职责原则
这条原则的主要目的是减少复杂度,让不同的类和不同的方法各司其责:与其用300行代码来实现一个复杂的功能,不如用十几个职责清晰的接口(或方法)来拆分它。
这也是一种 分而治之 的策略,因为如果一个类或方法做了太多的事,那么当其中任何一点发生改变时,整个类或方法都必须跟着修改,这肯定是一个非常痛苦的过程。
开闭原则
开闭原则 的意思是对扩展开放,对修改关闭
,它的本意是在实现新功能的开发时,保持现有代码不变,至少是不会面临大规模地修改。
例如,如果某个电商平台有一个会员体系评判功能,改评价方法通过硬编码方式得出每个会员的等级。现在如果该电商平台接入了第三方授权,那么这个会员评价体系将不得不增加if-else
分支条件,会对之前的源代码造成破坏。
如果按照开闭原则
,那么只需要将评判方法抽象成为接口,新增加一个实现该接口的第三方会员评判类即可,原评判方法一行代码都不用改。
这就是所谓的对扩展开放,对修改关闭
。
里氏代换原则
所谓 里氏代换原则 指的是类之间的兼容性问题,是否能够像操作(或传递)父类那样操作(或传递)子类。
这说的有点抽象,还是看看代码。
public class LiskovSubstitutionPrinciple {
class Animal {};
class Bird extends Animal{};
public Animal eat(Animal animal) {
// TODO......
}
}
如果在上面代码中,public Animal eat(Animal animal)
方法可以变成public Bird eat(Bird bird)
,而功能不受任何影响,那么Bird
类就符合 里氏代换原则。
之所有会有这么奇怪的原则存在,是因为在开发插件、类库或框架组件时,开发者无法预料开发出来的插件、类库或框架组件会被哪些客户端调用,即使知道也无法修改。
所以,必须遵守事先规定好的开发准则或技术规约,才能避免调用异常或失败。
这些开发准则或技术规约在某种程度上也扮演了父类的角色。
接口隔离原则
这个原则和 单一职责原则 说的是一体两面。
单一职责原则 是从开发者的角度来说的,尽量减少接口的功能复杂度,最好让接口一次只做一件事。
接口隔离原则 则是从维护者的角度来说的,让接口只实现其必须具备的功能,它不需要的就剥离掉。
例如,以共享单车来说,如果像下面那样设计功能结构,会给非电动的共享单车
增加不必要的充电
功能。

如果按照 接口隔离原则 设计,那么它就应该是这个样子的。

当然,这种设计也不是越细越好,太细了也会增加复杂度,需要折衷权衡。
依赖倒置原则
有的地方叫它 控制反转原则,但其实它们并不完全是一回事。
依赖倒置原则 的核心意义是高层不依赖底层,而底层依赖高层的抽象
。
这其实就等同于:
(高层的)业务逻辑 决定
(中层的)技术路线 决定
(底层的)技术实现方式。
业务逻辑 > 技术路线 > 技术实现
至于 控制反转原则,那只不过是框架的一种行为模式。
方向盘 控制
传动轴 控制
车轮子。
可以仔细体会一下 控制反转原则 和 依赖倒置原则 的这种差别。
包含的内容
所有关于模式的描述都会包括下面这些部分。
模式概述
:简明扼要地说明该模式的作用。问题
:面临的现实问题。方案
:模式会提供什么样的解决方案,这样做的好处在哪。结构
:模式的各个组成部分和它们之间的协作关系。适用场景
:哪些场合比较适用该模式,或哪些场合不适用。优缺点
:模式的优缺点。相关性
:该模式和其他模式的关系。
关注公众号后回复 模式
即可获得设计模式
栏目剩余文章的访问密码。
