如何使用设计模式
Iterator模式 (迭代器)
一个一个遍历
一个集合类可以遵守 Iterator 协议,并实现一个 Iterator,一般包含 next() 方法来获取下一个值,借此来实现数据的遍历。我们来实现一个简单的数组的迭代器例子:
1 | protocol IteratorProtocol { |
可以将遍历和实现分离开来,如果以后集合类型发生改变,只要同样为其实现了 Iteraltor协议的方法,就无需大批量的修改代码。
Adapter模式 (适配器)
加个适配器以便复用
就像要将一个220v的电源,转换为10v的输出一样,我们需要一个适配器来进行输入和输出的转换。如果一个类 Number
有 createNumber() 的方法,而有一个协议 Print
需要实现一个 printNumber() 的方法,我们可以通过一个中间类 NumberPrint
继承至 Number
并实现 Print
协议,这样的话我们就可以使用 createNumber() 的方法,来实现 printNumber() 方法。这无疑是提高了代码的复用,并且降低了代码的耦合度,我们不需要修改原来的类型就能够进行新的扩展。这种方法叫做类适配器模式
。
1 | class Number { |
还有一种模式是对象适配器模式
,如果需要转换的不是协议 Print
,而是类 Print
。对于无法进行多继承的语言来说,无法创建一个中间类 NumberPrint
同时继承两个类。
这是我们需要进行一些转变,我们可以创建一个继承于 Print
的类 NumberPrint
,而 NumberPrint
中有一个属性是 Number
的实例变量,我们可以通过调用实例变量的对象方法来实现 Print
中的 printNumber() 方法。
1 | // 此时的 Print 是一个类 |
当需要扩展类的功能时,无需对现有的类进行修改。有时一个类已经被多地方复用,并确认可靠,如果冒然进行修改可能会出现意想不到的错误,而且又要进行一轮新的测试,所以可以使用适配器进行处理。
还有当进行版本更新的时候,可能会有版本适配的要求,这个时候如何对旧的代码进行适配就很重要了,我们可以通过适配器模式,对新的功能进行转换,而保留旧的接口。
交给子类
Template Method模式(模板模式)
将具体的实现交给子类
如果一个类的逻辑代码在父类中,而其具体的方法需要子类来实现,我们就可以称之为模板模式。其实父类就是一个抽象类,比如我们定义一个类 Person
,它实现了 eat()、run() 和 sleep() 方法。我们可以在父类中定义它的执行顺序。但是具体的方法是如何吃、如何跑、如何睡觉的我们要交给子类来实现,毕竟 Child
(小孩)和 Adult
(成人)的习惯是不同的,不是吗?
使用模板模式,当我们遇到了执行逻辑的改变时,我们不需要去修改各个子类,我们只需要修改抽象类就行了。并且无论是任何子类都可以被父类执行。
1 | class Person { |
上面的例子中我们使用一个抽象类来描述 Person
。你也可以使用协议 Protocol
来达到同样的目的,不同的操作实现,相同的操作步骤。
我们这里将逻辑代码放到了父类中,把具体实现放到了子类中,但是实际使用时,如何分配父类和子类之间的代码的处理级别就需要大家们自己斟酌了,如果父类中实现的太多,就失去了模板的意义,降低了父类的灵活性。但是父类实现的太少也会导致子类中的代码重复,所以一切看大家的感觉了。
Factory Method模式(工厂模式)
将实例的生成交给子类
什么是工厂模式呢?通过一个Factory 生成一个 Product, 而 Factory和 Product 的实现是由子类来实现的,使用了模板模式。所以你可以定制自己的工厂生产出你想要的实例。 举个例子来说,我们把一种面点机器
作为 Factory
,而生成的面点是类 Product
。面点机器生成面点用方法 create()
,而面点可以被吃掉有方法 eat()
。
1 | protocol Factory { // 定义了一个工厂 |
但是我们并没有定义机器如何生产面点以及面点该如何被吃掉,这个时候我们创建类 Dumpling Machine
(饺子机器)继承于 Factory
并实现方法 create()
.
接下来我们实现类 Dumpling
继承至 Product
并实现了方法 eat()
。然后我们就可以通过 DumplingMachine
的 create()
来生成 Dumpling
的实例了。
1 | class DumplingMachine: Factory { |
这就是工厂模式的使用,那我们什么时候使用工厂模式呢。它又有什么好处呢?
工厂模式将框架
和具体实现
分离开来了,当我们实现自己的框架(Factory
,Product
)时,我们直接定义相应的方法逻辑和属性结构,抽象的描述框架的行为。而开发者可以通过框架来实现自己的工厂和产品。当我们使用工厂方法生成实例时,我们不需要考虑框架内部的实现,只需要实现预先约定的方法和属性就OK了。
例如Object-C
中的 NSNumber
就使用了 工厂模式
。 我们可以通过 NSNumber
生成不同的数值类型。
使用工厂模式的时候,生成实例我们有几个实现的方法。<1> 指定其为抽象方法,这样如何子类不实现的话,编译器就会报错。如果语法不支持定义抽象方法,这种方法就无法使用了。**<2>为其实现默认的实现,当子类没有继承父类的方法时。我们可以默认实现此过程,不过我不推荐使用这种方法,因为往往默认的实现都是不符合实际需要的,如果是忘了子类实现也无法通过编译器来提醒。<3>**在父类的实现里,抛出异常。并提示用户必须实现子类的方法。这样如果用户忘记在子类中实现这个方法,就会抛出异样,防止进一步的错误方法,也能够提示用户的错误发生在哪里。
生成实例
Singleton 模式(单例模式)
只有一个实例
当你想要保证在任何情况下都只有一个实例,程序对外也表现出只有一个实例的时候,你就可以选择使用单例模式来实现你的类。
单例模式,顾名思义就是这个类,只会生成一个实例变量。当你想要新实例化一个类的时候,它要不返回给你唯一的实例,要不就抛出异常。通常一个单例模式的类,都只有一个类似于 shareInstance()
的类方法
用来获取唯一的实例。具体的实现方法,根据各个语言的不同而做改变。
基本上,就是在类中创建一个实例变量,这个实例变量一旦被初始化就无法被改变和销毁。而获取实例的方法,总是返回这是实例就 ok 了。
在某些情况下,类似于程序的窗口,一定是只有一个的。这个时候,为了方便管理这个窗口,我们就可以实现一个单例来处理具体的事物。亦或是在程序启动以后需要,一个始终纯在的类实例,来进行公共数据的处理和传递。
在 GUI 上也有可以用到的地方,比如一个界面上,只能同时出现一个的弹窗。你不必在显示一个的时候,去关闭另一个,你只需要在这个地方显示一个弹窗,另一个就会消失,毕竟只有一个实例。(你可以不必在其他的类中持有此实例,在想用的时候,直接获取单例就 OK 了)
Prototype 模式(原型模式)
通过复制生成实例
一般情况下,我们在生成一个实例的时候。都是使用初始化方法,根据一个类来生成一个实例。当时在某些情况下,我们可能并不想根据一个类来实例化一个对象,这个时候我们可以通过一个实例来生成另一个实例,这种复制的方式,我们就称之为 Prototype 模式
。
什么情况下我们可以选择使用 prototype 模式
呢?大概有以下三种情况。
- **当对象功能相近而种类又太多的时候。**如果使用类的话,会创建很多的类。如果是在一个类中,创建不同功能的实例,然后通过实例复制来进行后续的对象生成。(有一种把实例当类用的感觉…)
- **太过复杂,无法通过类来生成实例。**比如一个画板上笔的运动轨迹,通过一个对象来记录。在另一个地方要生成一个和这个笔的运动轨迹完全一样的对象时,你很难通过一个类来实例化出这个对象。而对这个对象的复制就能够很容易的做到。
- **写框架时,想要将类和实例解耦。**这个在实现的过程中,你就可要体会到,它的复用性很高,类之间是没有耦合的。
实现的过程大概如此: 我们通过创建一个类 Manager
,并实现 register()
和 createClone()
方法,用来注册和生成实例。然后是协议 Product
,它定义了方法 use()
和 copySelf()
。继承此协议的类需要实现 use()
来实现如何使用执行,copySelf()
则是复制自己以生成新的实例。
另外则是是具体的类了,我们举个例子是类 State
,它用来描述一个人身体的状况,State
需要遵守协议 Product
并实现方法 use()
和 copySelf()
。我们生成一个一个 state
实例,并使用 Manager
来注册此实例,然后可以通过 createClone()
来进行复制,下面是简化的伪代码
这里我们可以把 Manager
想象为一个库房,因为通过实例来生成实例,毕竟要有一个 母体
。而这个母体不能像是一个类一样随时可以调用,所以我们需要把它放在一个地方,在我们想用的时候,随时可以使用,并且防止母体
被意外修改,Manager
只是提供了复制的方法,你不能获取和修改母体
。
1 | protocol Product { |
Builder 模式 (构建模式)
组装复杂的实例
有时候,当我们在构建一个复杂的模块的时候,我们需要将其拆分出来。形成一个个小的组件,然后通过一个管理类,进行重新组合。这样的话我们可以最大限度的提供代码的复用性及可替代性。
Builder 的思路十分的简洁,主要分为 Director(管理者), **Builder(构建器)**和 Buinder 的子类。Director 通过一个 Builder 的实例来生成所需要的数据,而数据的具体实现方式,则是通过子类来实现的。Builder 中应当涵盖构建数据所需要的所有必要方法,但是不应当含有特别的方法。这样 Director 可以通过一个 Builder 来实现功能。
而具体的实现方式,它就不知道了,它只是知道其调用了一个 Builder,但是 Builder 有很多,它并不知道调用的是哪一个 Builder。这种不知道则提高了模式的灵活,只有不知道,才能够被替换。
1 | protocol Builder { |
到现在为止,大家们对抽象这个概念应该都很了解了,抽象可以是
抽象类
也可以是接口
,抽象的目的就是隐藏实现
而突出逻辑
。将逻辑和实现分开是实现代码复用和提高维护性减少耦合常用的方法。以后如果提到抽象,希望大家都能理解它的含义。
Abstract Factory 模式 (抽象工厂模式)
将关联的零件组装成产品
抽象工厂
的作用就是将抽象零件
加工成抽象产品
。直接理解的话可能不是特别容易懂,我们直接举一个例子,就大概明白它的意思了。
我们的抽象工厂
就是一个生产抽象产品
电子板的机器,这个电子板上有很多的电容、电阻和各种各样的元件(抽象元件
)。这里我们并不知道电子板的大小和所需元件的参数和数量。那就意味着我们可以通过实现不同的电子板子类**(抽象产品的子类)来生产不同的产品
。而抽象元件
只要符合对应的参数,我们可以使用任意厂商的元件(抽象零件的子类)**来使用。产品模型有了,元件也有了,那么实现一个具体的工厂来生产特定的产品是很重要的,不可能一个工厂可以生产任何产品吧,我们也可以通过修改工厂实例来优化生产的工艺和流程。
这样我们就实现了一个可以生产各式各样产品的生产线。当需要修改的时候,我们不用替换很多的数据,只要将特定的子类替换掉就可以实现产品线的跟新,是不是和现在的代工厂一模一样。
你会发现大部分的设计模式都要牵扯到抽象概念(接口)。这是很多模式优化的基础。如果你知道面对对象编程和函数响应式编程等等,那你肯定对面对接口编程也有所耳闻,Swfit 相比 OC 就大量的使用了面向接口编程。这种编程方式的灵活性很高,如果大家感兴趣,可以去多了解一下
分开考虑
Bridge 模式 (桥接模式)
将类的功能层次结构和实现层次结构分类开来
为了了解我们是为了桥接谁和谁,我们需要先来了解一下什么是类的功能层次和类的实现层次:
- 类的功能层次: 功能层次其实就是实现一个类的子类,当你需要给一个类添加新的功能的时候,我们可以通过实现一个子类来完成。随着功能的增多,我们可以实现一个又一个子类,并不断的加深这个结构,这就是类的功能层次。**(类的功能层次过多是不好的设计)**就行下面这样:
- Person
- Men
- Boy
- 类的实现层次: 类的实现层次则是抽象类的实现,当我们需要改变一个类的方法实现方式的时候,我们只需一个继承抽象类的子类就行了,我们并不是为了给父类中添加其没有的新功能,我们只是为原功能提供了不同的实现而已。
- Display
- StringDisplay
- HtmlDisplay
在实际的使用中,我们往往要根据实际的需求,灵活的运用这两种结构。如果仅仅是将它们混合在一起使用的话,当应用变得更为复杂的时候,你就很难清楚的认识到,你到底应该继承哪个类。
所以我们需要将功能层次和实现层次分离开来。但前面说了,我们要灵活的运用两种层次,那就要让他们之间有联系,这时我们就需要在它们之间建一条桥梁。
大概的实现就是下图这样,Display 类是功能层次的最高层级,它持有一个 DisplayImp1 的实例,这个实例中有与 Display 相对应的功能。DisplayImp1
是一个抽象类, 而 StringDisplayImp1
继承了 DisplayImp1
,实现了所有的方法。
这时我们可以创建一个 stringDisplayImp1
的实例,通过这个实例来创建一个 Display
或是 PhotoDisplay
来使用。
Strategy 模式 (策略模式)
整体地替换算法
**策略在程序中也可以被称作算法。**我们在处理程序中一些复杂的关系时,所使用的算法可能会根据软件的系统、时间的需求、错误率及系统硬件机能等进行相应的调整。这是我们要同时完备几种算法以便在系统中进行替换,不加设计的话,替换算法本身也将是一个麻烦的事情。 Strategy 模式
就可以方便的完整替换整个算法。
例如我们想要实现一个棋类应用,单机模式下我们将会有一个AI来和玩家对战。我们定义一个 Player
类作为AI的类。创建一个Player
需要提供一个策略,而这个策略 Strategy
是一个抽象类,它定义了一系列的方法,可以通过现在棋局的数据推算出下一步该往哪走。我们根据游戏的算法来制定算法,这个时候我们就可以通过不同的子类策略实现设备的适配和 AI 难度的调节。
无论策略发生了什么改变,我们无需修改任何的接口,我们只需要替换一个策略的类,就可以完成整个算法的替换。
Strategy 模式常用在棋牌类游戏中,而且确实很实用。我感觉 Strategy 模式不太像一个正经的设计模式,它的概念很简单,甚至就是抽象类或接口模式的基础应用而已。我们平时写代码的时候,多多少少会用过或见过这类用法。
一致性
Composite 模式 (复合模式)
容器与内容一致性
我们平时使用的电脑、ipad和手机等电子设备都有自己的文件管理系统。他们的基本结构就是有一个根目录,下属很多的文件夹和文件。文件夹下面又是文件夹或是文件。我们所看的这种树状结构看起来是由两种数据类型组成的。其实我们完全可以把它们统一看做为一种目录条目
。
这种目录条目
拥有通用的属性和方法,它们拥有一致的行为。能够使容器和内容具有一致性,创造出递归的结构的模式就是 Composite 模式。
Entry 是一个抽象类,它定义了一个
条目
。它通过getName()
来获取条目名字、getSize()
获取条目大小、printList()
是用来打印内容列表,add()
则是提供给子类Directory
来添加新的File
和Directory
。
File
是文件类,它可以返回文件名、大小和报告自己的目录。Directory
是文件夹类,它有名字name
, 还有directories
用来存储自己内部的文件和文件夹列表,但是它没有自己的大小,它的大小是通过内容的getSize()
方法相加获取。
通过这样的方式,我们就构建了一个递归的文件结构,这种结构将内部的内容和外部的容器统一起来,使对象的调用变得更易理解和简洁。
Decorator 模式 (装饰器模式)
装饰边框与被装饰物的一致性
一提到装饰器,大家肯定都知道装饰的概念。装饰器就像你照片的相框,水果蛋糕上的点心一样,通过装饰物使主体**[被装饰物]**(相片和蛋糕)变得与众不同。这个模式的作用也是如此,但是如果只是这样的话,你很容易把装饰器看做和主体不同的东西。你的想法大概是这样的:
你可能以为它们是一被一个一个放到被装饰物上的。这样的话,你就无法装饰装饰物本身了,整个模式的扩展性就被降低了。我们需要装饰物和被装饰物具备一致性
,这样的话接口就变得透明了起来,无论我们如何对被装饰物进行装饰,我们最后所看到的被装饰物所体现的接口和行为还是和最初是一样的。而这样的形式才是真正的装饰器模式
,它就像一个俄罗斯套娃,一层嵌套一层,每层都可以看着一个包装或装饰,直到最后一个套娃出现。
那我们如何实现这个结构呢,它看起来和 Composite 模式
有些像。我们举一个显示程序的例子,它可以为显示内容添加+、-、* 等字符边框。
我们需要一个抽象类 Dispaly
来描述显示的流程,通过一个子类 StringDisplay
可以显示一行字符串。接下来我们定义一个装饰器的抽象类 Decorator
,它是继承于Display 的子抽象类。Decorator
中有一个类型为 Display
的成员变量 display,它表示被装饰物。
这样我们就将装饰物和被装饰物统一起来了,使他们满足一致性
。它的优缺点大概有以下几点
- 接口的透明性 无论我们进行多少次装饰,被装饰物的接口都没有被隐藏起来,还是和当初一样。
- 可以在不改变装饰物的前提下去添加功能 当我们添加功能的时候,只需要实现不同的装饰器就 OK 了。
- 需要实现太多的小类 我们需要些很多不同功能的装饰器,这些类的功能通常不多,但是却数量巨大。
访问数据结构
Visitor 模式 (访问者模式)
访问数据结构并处理数据
我们大家都学过数据结构,数据结构的重要性就不言而喻了。但是此设计模式的重点不是如何设计数据结构,而是如何访问数据结构。
一般我们在实现了一个数据结构后,都会将数据的操作方法和数据结构本身绑定在同一个类中。使它们成为一个整体。这样做在以后使用的时候会方便很多,也不用那么多的类进行操作。
但是当你想要扩张数据结构的处理方法的时候,你就需要直接修改数据结构的类。这样做很不方便,既麻烦又不利于项目的稳定。
如何解决呢?以我们学习了以上那么多设计模式的经验,当然是将数据结构和访问操作分离开来喽。
在 Visitor 模式
中,Visitor
是一个访问者的形象。它是一个抽象类,内部定义了一个 visit()
方法。,以我们在 Composite 模式中使用的文件系统为例,这里的 Entry
类同样代表了数据结构的抽象形式,它的具体实现是由 File
和 Direcotry
实现的。
不一样的地方是,我们这里要定义一个新的接口 Element
,这个接口定义了一个 accpet(Visiter)
方法,Entry
遵守这个接口,而它的子类需要实现这个接口的方法。
accept()
方法接受一个 Visitor
实例,并在 accept()
的方法内部调用 Visitor
的 visit()
方法,同时把自己【也就是Entry 本身】传给这个方法。这样在 visit()
的内部就可以进行数据的处理了,而细节则是由 Visitor
的子类所实现的。
此时,我们就可以通过实现不同的 Visitor
子类进行数据访问方式的扩展。
整个模式的结构就是如此,但是你可能会为里面 visit()和 accpet()的调用感到困惑在处理 Directory
的过程中,我们需要遍历里面的所以对象,并一个个调用他们的 accpet()
方法,同时把Visitor
自己也给传过去,然后在各个 Entry
中再次调用 visit()
方法,进行同样的操作,直到最后一个文件是 File
,递归就结束了。
这里用到了两个类的两个方法来进行嵌套递归,着实很难让人理解。一个递归就已经让人头痛了,这样的递归也主要是为了实现数据处理
和数据结构
的分离,并简化数据的处理过程。
当你实现了一个新的 Visitor
并通过一句调用就可以直接处理一个数据类型,而不用关心具体的类型时,你就会感受到它的好处了。
一切的辛苦都是有价值的。
Chain of Responsibility 模式 (责任链模式)
推卸责任
看到这个模式的描述推卸责任
,是不是感觉有些奇葩。我们生活中,推卸责任看起来像是一个低效的处理问题的方式,那是如何在程序中发生正向的作用呢?
我们先看一下责任链模式的结构:
- 问题: 既然要处理问题,那问题本身就很重要了。我们需要一个抽象类或是接口来定义问题。
- 处理问题的抽象类: 问题需要交给一个类来处理,而这个类定义了处理问题的流程,比如判断自己能否解决,如果能解决就返回结果。如果不能解决,就自动交给下一个类来处理,要是没有下一个类就返回错误。
- 具体处理问题的类: 抽象类我们已经定义了,但是具体的解决问题的方法,需要不同的子类来实现。
比如我们有多种不同等级的预警处理方案来处理一个警报。警报分为蓝、黄、橙和红四个级别。我们定义一个警报类
Alert
,它通过初始化方法init(int)
传入一个等级来创建。
接下来我们定义一个抽象类Handle
来实现处理警报的流程,它有属性next
(Handle 的实例)表示要将责任推给那个对象。以及一个让子类实现的抽象方法resolve()
,用来表示解决问题的具体实现。
它通过方法targetr(Alert)
来处理警报,target
内会调用resolve()
方法来处理问题,如果能处理就返回成功,如果无法处理,就将问题交给next
,接下来会调用next
的target()
方法。直到有方案能处理警报,或是没有办法处理,报告错误。
现在想一想我们开头的问题,责任链模式有什么好处呢。
- 我们可以简化处理问题的流程,如果不踢皮球的话,我们需要为每个问题指明对应的处理方法。那
Alert
本身就需要知道自己能被哪个类处理,这就像你想要解决一个问题,你未必就一定能找到一个对的人。你只是纯粹的想解决问题而已。让问题知道自己该被谁解决,就会让问题本身变得更加复杂。 - 可以动态的修改流程,我们的处理顺序是链式的,上一个类决定下一个要处理的类。我们只需要需改一下
next
就能够轻松的改变处理问题的顺序。
使用责任链模式的一个问题是,会增加处理问题的时间,因为是一个一个去判断能不能解决的。如果问题没有固定的解决方案,使用
责任链模式
是没有任何问题的。如果能够确定问题的处理方式就没必要这样了。
简单化
Facade 模式 (窗口模式)
简单窗口
随着时间的脚步,我们的程序会越来越完善,同时也会变得更加复杂和冗余。当我们实现新的功能时,我们需要在众多的类中,找到需要的类,并组织逻辑和顺序。特别是在大型程序中,每次调用都要注意众多的类之间错中复杂的关系。难道我们就不能使用一个统一的窗口,只需要调用这个窗口的方法,我们就可以实现这个操作,这个思想就是Facade 模式
。
例如要实现一个发布模块,要发布的内容有文字、视频和图片。原来的操作是我们要分别上传图片 >> 上传视频 >> 处理文字 >> 整理json 数据 >> 上传服务器。 这样的操作做一次还好,如果有多个地方需要使用到发布的功能,这样就显得太过复杂了,也不利于整合模块的功能。
我们现在使用 Facade 模式
进行改进,实现一个 PublishManager
类就是 Facade模式
中的窗口,它有一个方法 publish(text, image, video)
可以直接接受文字、图片和视频,在PublishManager
内部,它可以把文字交给 TextHandle
[处理文字的类] 来处理,把图片和视频的上传交给 UploadManager
[进行上传的类],拿到 url 后通过 JSONSerialization
进行 JSON 处理。最后通过 HTTPManager
将数据传递给后台。
这样以后,我们无论在任何地方需要使用到发布功能的时候,我们只需要调用 PublishManager
的发布方法,就可以直接进行发布,这里我们就实现了一个窗口,进行发布的窗口,而复杂的内部调用,就被我们隐藏起来了,我们无需关心它的内部调用,如果以后需要进行修改我们可以直接修改PublishManager
而不用再调整其他的地方,使得发布的功能变得更加纯粹。
Mediator 模式 (中介模式、仲裁者模式
只有一个仲裁者
如果你要编写一个联机的棋类游戏,同时有4名玩家进行对战,每人一步,通过某个规则可以吃掉别人棋子。我们该如何同步各个玩家的棋盘和管理各个玩家的状态呢。
如果我们每个玩家的终端,各自控制自己的状态而后将数据发送到其他的终端。那每个终端都要处理其他终端发送过来的数据,而后同步自己的状态。
这时每个终端都有一份自己的数据,处理的逻辑随着玩家的个数增加也会变得更加复杂。并且一旦一个玩家的数据出错,他会把错误的数据发送给其他的终端,这时双方的数据会发生冲突而产生致命错误。
而今天我们将通过 Mediator 模式
来解决这个问题。我们通过一个仲裁者,你可以把它作为游戏的一个中间服务器。玩家的每个终端都只是接收仲裁者发来的属于自己的数据并进行状态的更新,而自己的每一步操作就只是传递给仲裁者。仲裁者进行数据的处理后,再通知所有的终端分别更新状态,这样一来各个终端的操作实时汇集到仲裁者,而仲裁者再实时进行数据分发。
这样做就不会出现数据不同步的状况了,而数据的处理集中到了一点,降低了出现 bug 的概率。即使出现了问题也容易排查 bug 发生在哪里。
除了上述的使用情景以外,我们在项目当中处理 GUI 的点击、界面和操作逻辑管理时,也可以使用 Mediator 模式
。 我们创建一个抽象类类 Manager
作为 Mediator,再创建一个接口 Colleague
, 表示和 Manager
连接的各个控件。Manager
定义了各种各样设置 Colleague
的方法和方法 didChange( Colleague )
来告知 Manager
哪个控件发生了改变。我们实例化 Manager
的一个子类,将其传递给各个控件[实现接口 Colleague],当控件发生状态变更时就传递给这个仲裁者,而后仲裁者进行处理后,通过各个设置 Colleague
的方法进行控件状态的更新。
开到这里我们就能发现,
Mediator 模式
是一种双向绑定机制。只不过是各个对象都绑定同一个仲裁者,而后通过与它进行通信借以实现与其他的对象进行通信的目的。
管理状态
Observer 模式 [观察者模式]
发送状态变化通知
说到观察者模式,我想大家都应该有所了解。很多语言中都有 Observer 模式
的设计,虽然各种各样的实现各有区别,但都是以Observer 观察被观察者,当被观察者发生改变时,通知 Observer 发生了什么改变为目的。
我们现在来实现一个简单化的观察者模式,我们创建一个抽象类 NumberGenerator
, 再创建一个 RandomNumberGenerator
继承自NumberGenerator
,
1 | class NumberGenerator { |
接下来就是创建一个接口 Observer
,只有实现了此接口的类才能成为 NumberGenerator
的观察者。它只有一个 update
方法 [在 swfit 中接口相当于协议]
1 |
|
我们通过 Dispaly
的实现,将每次订阅到的值显示出来。下面是一个简单的使用
1 | let generator = RandomNumberGenerator() |
以上就是一个简单化的 Observer 模式
的使用,如果细心的话你会看到,我们直接将被观察者本身返回给了观察者。一个对象可以同时被很多观察者观察,但是观察者想要获取的信息可能各有不同,所以直接将自身传递,让观察者自己去查找。
当然了,这是由于我们的设计过于简陋。在 Objective-C 中,我们可以直接监听各个对象的属性。
其实,观察者模式,我们也可以称为订阅模式。观察者并不是去主动观察,而是被观察者通知观察者的。如果理解为发布和订阅就更加契合了,你可以订阅一个对象,如果他发布了新的内容,你就会得到通知。
到这里,如果你上面的各种模式都了解了一遍的话,你就会发现,在很多模式中已经出现了很多的这种可替换性设计了。通常进行替换性设计,可以提高系统的灵活性和降低耦合性。一般我们通过以下两者方式进行替换性设计。
- 利用抽象类和接口从具体类中提取出抽象方法
- 在将实例作为参数传递至类中,或是在类的字段中保存实例时,不使用具体的类型,而是使用抽象类和接口
使用这种设计我们可以轻松的替换项目中的具体类。
Momento 模式
保存对象状态
我们平时使用的文本编辑器、PS 等等,都有一系列十分重要的功能,就是撤销(undo)、 重做(redo) 和历史快照(history)。像是撤销这样的操作我每天要使用几百次,那如何记录每个操作节点的状态就十分重要了。
而 Momento 模式
就十分善于处理这种情况,Momento 有纪念品的意思,我们也可以想象着把一个对象每个时间点的状态拍上一张照片作为纪念品。
当我们需要的时候,我们可以通过每个时间点的快照来恢复对象的状态。比如我们要记录一个棋局,类 ChessGame
表示一局正在进行的棋盘。里面有方法 createMomento()
通过当前棋子的数据存储快照。我们是创建一个类 Momento
来存储棋局数据的。生成的快照被存入棋局的数组 history
中,当调用 undo()
方法时,我们就取出最后一个棋局状态进行棋局的复原,这就是 Momento 模式
。
1 | class ChessGame { |
这里的 Momento 模式
和以前的 Prototype 模式
在存储状态上也一点点相似,但是这里的 Momento
只是存储恢复状态所需要的必要数据,而 Prototype 模式
中,实例复制成的则是完完全全相同的另一个实例,所以它们的区别还是很明显的。
State 模式 (状态模式)
用类表示状态
有些时候我们在项目当中会遇到各种各样的状态,比如应用的夜间模式和白天模式,再或者是一个警报系统的各个预警状态。使用夜间、白天模式是一些阅读软件常备的功能,切换不同的模式,整个应用的界面会发生色调的转变。而警报系统在不同的预警状态下,对同一事件的处理方式也是不同的。
针对这种需要根据状态判断的例子,我们通常使用的方法,就是通过 if
或是 switch
来判断不同的状态,而执行不同的实现方法。比如应用的夜间和白天模式:
1 |
|
这个就是我们一般的实现方式,这样的实现方式在简单的状态切换时到没有什么。但是像是以上这样的白天和黑夜模式的界面颜色获取,可能有几十个方法,一个类中满满的都是 if
看起来就眼花。如果这个时候你需要添加另一个模式,你就需要在每个方法下面添加一个 else if
,重要的是,编译器并不会因为你忘记写一个,而通知你, 所以,在添加新的模式时,我们很容易出错,接下来就是用到 State 模式
的时候了。
通过一个类来表示一个状态,就是状态模式。 在 State 模式
中我们通过创建一个类来表示一个新状态。像以前一样,我们需要创建一个抽象类 State
来定义状态中需要实现的方法。接下来我们分别定义 NightState
和 DayState
来表示白天和黑夜的状态,通过以下的代码我们来看看有什么区别。
1 |
|
通过上面的例子,我想你一定明白了它们的区别。在这样的 State 模式
下,UIManager
是用来控制界面的颜色显示的。它负责切换和控制状态,所以它需要知道所有状态的条件。
除了让 UIManager
控制状态的切换外,我们还可以让每个状态本身去控制现在的状态,这里就像是 Chain of Responsibility 模式
(责任模式)。我们扩展一下这个协议:
1 |
|
可以看出来,UIManager
只需要默认一个状态,然后再调用方法前,告知当前模式时间,它就可以通过自己的判断来寻找正确的状态。这里的状态只有两种,如果有很多种的话,自己不是此状态,就传递给下一个状态,直到找到一个正确的状态。
使用第一种方法,manager 就需要知道所有的状态关系。但是耦合度很低,各个状态不需要知道其他的状态。
而第二种方法,每个状态或多或少的需要知道其他的状态,这样增加了耦合度。不过 Manager 不用再管理所有的状态了,它只需要处理方法就行了。
- 我们可以方便的添加各种各样的状态我们只需要实现
State
的方法就行了,可能还需要处理一下切换到其他状态的情况,不过这是你使用第二种 Manager 管理的时候。 - 添加依赖于状态的处理十分的麻烦当我们对状态添加一个新的处理方法的时候,我们需要修改每一个状态,这十分的麻烦。所幸的是,我们不会忘了给其中的一个状态添加新的处理方法,因为编译器会提示我们,如果我们忘记了给任意一个状态添加方法。如果不使用
State 模式
就不会得到编译器的帮助,可想而知,一旦大意,就会引发不可知的 bug。
避免浪费
Flyweight 模式 (轻量级模式)
共享对象,避免浪费
我们都知道在应用当中使用的对象都占用了一定的系统内存,当我们的对象占用内存过大时,就会降低系统的运行速度和稳定性,甚至引发崩溃。 如果有些对象可以被共同使用,就可以减少创建新对象的开销,也可以降低内存的占用。所以 Flyweight 模式
就是 通过尽量共享实例来避免 new 出新的实例来大大降低系统的内存消耗。
这里我们举一个例子,比如我们要打印一张图片,而这张图片是又几种不同的素材图片拼出来的。当我们在在打印图片的时候,我们需要先将对应的素材按照顺序排列好,才能进行打印。
1 | class Image { |
我们创建 Image
当做是素材,ImageManager
是用来排版素材的类,它通过传入一个包含素材 id 的数组来打印出对应的图片。 在排列过程中,我们每种素材的信息其实是不变的,所以它是可以共享的,我们使用一个字典把 id 当做 key来 实现缓存素材数据。当通过 id 排列素材时,我们直接获取缓存中的素材数据,如果重复使用了一个素材,也不会再次创建,而是共享一个对象。通过这样的方式,我们就能够减少一大部分的内存消耗。
不过共享同一个对象也有问题,就是改变了这个对象,那么所有共享它的也会发生改变。这有时候是好事,有时候是坏事,具体要看应用的场景。
但是大概可以这样判断是否该共享该对象。
- 代表本质的,不依赖于状态和位置的对象可以共享它是一个
intrinsic 信息
。 - 外在的,依赖于状态和位置的对象不能共享 它是一个
extrinsic 信息
。
一般的对象都适用于这两个规则。根据项目的实现目的,灵活的运用Flyweight 模式
可以优化你的应用内存占用。
Proxy 模式 (代理人模式)
只在必要时生成实例
当读到代理人模式
的时候,希望你不会把它和 OC
中的 delegate
弄混淆了。OC中的 delegate 其实是接口 interface
或者说是 protocol
的使用,而我们今天要了解的 Proxy 模式
中的代理人
指的是替原本的对象来执行操作。
在哪些情况下,我们需要使用Proxy 模式
呢? 通常是当一个对象的创建需要消耗大量的性能,而它的重要操作又可以延后的时候。在这种情况下,如果需要使用此对象,就立刻创建,可能会占用过高的性能,而后又没有使用到这个对象的重要功能,那岂不是白白浪费了大量的系统算力。
所以我们需要使用一个代理人来替代这个本人。它实现这个本人的基本属性和方法,而将耗时的工作交给真正的本人
去做,那样只有在真正需要本人
去做得事情才会去创建本人
,而其他的不耗时操作将交给代理人
去做。
这里我们举一个例子,比如有一个打印图片的类 ImagePrint
,它通过一个 url
来初始化实例,调用 Print
方法就可以打印出这张图片,这就是本人。又有一个类 ImagePrintProxy
表示它的代理人
。接口 ImagePrintable
规定了本人
和代理人
都应该具备的方法和属性。下面我们通过伪代码来具体了解一下整个过程:
1 |
|
看过这个例子以后,就很容易理解什么是 Proxy 模式
了,使用 Proxy 模式
的时候,调用者并不关心是谁实现了里面的方法,它只是调用了符合 ImagePrintable
的类。而实际的执行者 ImagePrint
也不关心自己是被直接调用还是间接调用。对问题的处理就交给了 ImagePrintProxy
这个代理人身上。这样的话,代理人就可以根据实际的情况来替本人
完成一些简单的工作,而尽量将本人
的创建延后,只在真正需要使用的时候,才会创建本人
。
这样的设计,对外显示出了一致性,在不影响调用关系的情况下。节省了系统的性能消耗,能提高应用的流畅性。
用类来表示
Command 模式 (命令模式)
命令也是类
通常我们所说的命令都是实例的方法,虽然调用的结果会在实例的状态中得到反馈,但是却无法留下调用的历史记录。当我们想要把每一次调用都记录下来时,我们可以把类当作命令来看待,使用类来表示要做的操作。这样我们管理一系列操作时就是直接管理这些命令类的实例,而不是通过方法进行动态操作了。
那我们该如何进行设计以实现 Command 模式
呢,一样,我们举一个例子。比如我们实现一个和Flyweight 模式
(上上个模式)一样的功能,通过素材打印图片,这里我们再为它添加一些新的功能,并进行优化。
- 如果素材进行排列的时候,不是按照顺序,而是有各自的坐标
- 并且每添加一个素材我们就立即打印出来。
现在我们把每次添加一个素材的操作不在看做是一个方法里面的循环执行,而是一个个命令。我们需要一个接口 Command (interface)
表示什么是命令,命令很简单,只需要能执行就 OK 了。 每次绘制素材的操作用 DrawCommand
来表示,它继承于 Command
。
有时我们可能需要执行一系列的操作,所以我们需要一个表示操作集合的类 MacroCommand
,它同样也继承于 Command
,在 MacroCommand
中有添加和移除 Command
的命令,同样有保存所有操作的属性 commands
。
有了命令,但是命令本身不执行具体的绘制操作,它仅仅是提供操作的具体数据。我们还需要一个绘制类,这个绘制类我们不具体创建,而是通过一个接口 Drawable
来表示, Drawable
需要实现绘制方法draw()
。为什么这样设计,如果你看了以上的设计模式,我想你应该已经很清楚了。使用接口,能方便的替换绘制实现,也为你要绘制不同的东西提供了扩展的可能性,并且不影响其他代码的结构,这就是代码的可替换性。
这里我们用 ImageDrawCanvas
来表示一个简单的绘制图片的图层。下面是伪代码的实现
1 | // 命令接口,只定义了 excute |
以上伪实现了一个 Commnad 模式
的图片绘制功能,不过 Command 模式
的主要实现就是这样的。通过具象一个操作为一个实例,我们能精准的操控每一个操作,并重复任意的步骤。我们还可以将这些实例进行归档处理,永久保存我们的操作记录。在我们了解的以上所有的设计模式,除了本文的把类作为命令,还有 State 模式
中的把类作为状态。 以后再遇到操作是需要在实例的方法内进行很多的判断和选择,你可以试着将不同的情况拆分为不同的类来实现,或许会豁然开朗。
Interpreter 模式 (翻译模式)
语法规则也是类
又多了一个用类来替换某些东西的类,而这次,我们这模拟的是语法。在某些特殊的情况下,我们可能想要设计一种新的迷你语言
来方便的编写繁琐的操作。例如正则表达式
就可以通过简短的语法来描述复杂的筛选条件。我们也可以设计一款小语言来这样做,再编写一个翻译程序
将它翻译成你所使用的语言。而其中的各种语法可以被翻译为不同的类,比如 Add 作为 +
, CommandList 作为 repeat
等等。但是,这个过程还是很麻烦的,这里的篇幅已经很长了。而叙述一个迷你语言,或许需要更大的篇幅才能讲明白,而这篇文章只是想要使用简单的文字来帮助你了解所谓的23设计模式。
结语
终于看完了所有的23种设计模式,其实很多的设计模式已经不知不觉中被我们使用了无数次了。对于经验丰富的程序员而言,设计模式中的方法在他们看来是理所应当的。毕竟,设计模式本身就是对前辈们经验的总结,本身并没有什么突出的特点。它也不能帮你解决所有的问题,但是通过了解设计模式,我们可以更快的学习到前辈的经验。在实际的使用中,对我们的帮助是显而易见的。
设计模式虽然很重要,但是你却不用想着把它们都记在自己的脑海中。死记硬背从来都不是好方法,你只要有些许的印象,知道遇见这样的问题时该使用什么样的模式,随后再去查询具体的资料就是行了,善用搜索引擎可是程序员最重要的一项技能。
说了那么多,最后再说点我的感悟。
语言技巧很多,黑魔法很多,设计思想也很多,学完所有为大家所称赞的思想和技巧,也并不能让你的项目看起来更完美。遇见问题时,越是简单的实现就越有可能解决问题,也更容易被人看懂。让程序看起来简单,而不是让它看起来 NB。有一句话说的好 “要让程序看起来明显没有问题,而不是没有明显的问题。”