Iterator模式 (迭代器)
一个一个遍历
一个集合类可以遵守 Iterator 协议,并实现一个 Iterator,一般包含 next()方法来获取下一个值,借此来实现数据的遍历。我们来实现一个简单的数组的迭代器例子:
protocol IteratorProtocol {
associatedtype Element // 关联类型
func next() -> Self.Element? // 返回下一个数值
}
class Iterator: IteratorProtocol {
var currentIndex: Int = 0
var datas: [Element]
init(datas: [Element]) {
self.datas = datas
}
func next() -> Self.Element? {
if self.datas.count > currentIndex {
defer {
self.currentIndex += 1
}
return self.datas[currentIndex]
} else {
return nil
}
}
}
可以将遍历和实现分离开来,如果以后集合类型发生改变,只要同样为其实现了 Iteraltor协议的方法,就无需大批量的修改代码。
Adapter模式 (适配器)
加个适配器以便复用
就像要将一个220v的电源,转换为10v的输出一样,我们需要一个适配器来进行输入和输出的转换。如果一个类Number有createNumber()的方法,而有一个协议Print需要实现一个printNumber() 的方法,我们可以通过一个中间类NumberPrint继承至Number并实现Print 协议,这样的话我们就可以使用 createNumber()的方法,来实现printNumber()方法。这无疑是提高了代码的复用,并且降低了代码的耦合度,我们不需要修改原来的类型就能够进行新的扩展。这种方法叫做类适配器模式。
class Number {
func createNumber() -> Int { // 生成一个随机数
return arc4random() % 999
}
}
protocol Print {
// 需要打印出一个数字,这里举例只是打印数字。实际项目中可能需要的是一个复杂的操作
func printNumber()
}
// 一个继承于`Number`并遵守`�Print`的适配器类,它可以借助父类的方法来实现协议的方法
class NumberPrint: Number, Print {
func printNumber() {
print(super.createNumber()) // 借助父类的方法实现,来实现协议的功能
}
}
还有一种模式是对象适配器模式,如果需要转换的不是协议Print,而是类Print。对于无法进行多继承的语言来说,无法创建一个中间类NumberPrint 同时继承两个类。
这是我们需要进行一些转变,我们可以创建一个继承于Print 的类 NumberPrint,而NumberPrint 中有一个属性是 Number 的实例变量,我们可以通过调用实例变量的对象方法来实现Print 中的 printNumber()方法。
// 此时的 Print 是一个类
class NumberPrint: Print {
var number: Number = Number()
// 重写父类的方法,利用属性`number` 来实现父类的方法
func printNumber() {
let n = self.number.createNumber()
print(n)
}
}
当需要扩展类的功能时,无需对现有的类进行修改。有时一个类已经被多地方复用,并确认可靠,如果冒然进行修改可能会出现意想不到的错误,而且又要进行一轮新的测试,所以可以使用适配器进行处理。
还有当进行版本更新的时候,可能会有版本适配的要求,这个时候如何对旧的代码进行适配就很重要了,我们可以通过适配器模式,对新的功能进行转换,而保留旧的接口。
交给子类
Template Method模式(模板模式)
将具体的实现交给子类
如果一个类的逻辑代码在父类中,而其具体的方法需要子类来实现,我们就可以称之为模板模式。其实父类就是一个抽象类,比如我们定义一个类Person,它实现了eat()、run()和sleep()方法。我们可以在父类中定义它的执行顺序。但是具体的方法是如何吃、如何跑、如何睡觉的我们要交给子类来实现,毕竟Child(小孩)和Adult(成人)的习惯是不同的,不是吗?
使用模板模式,当我们遇到了执行逻辑的改变时,我们不需要去修改各个子类,我们只需要修改抽象类就行了。并且无论是任何子类都可以被父类执行。
class Person {
// 控制人的行为,先吃放后跑步,最后睡觉
func action() { eat()
run()
sleep()
}
// 要交给子类实现的方法
func run(){}
func eat(){}
func sleep()
}
class Child: Person {
func eat() {
print("drink milk")
}
func run() {
print("run 10m")
}
func sleep() {
print("sleep for 10 hours")
}
}
class Adult: Person {
func eate() {
print("eat food")
}
func run() {
print("run 10km")
}
func sleep() {
print("sleep for 6 hours")
}
}
上面的例子中我们使用一个抽象类来描述Person。你也可以使用协议Protocol来达到同样的目的,不同的操作实现,相同的操作步骤。
我们这里将逻辑代码放到了父类中,把具体实现放到了子类中,但是实际使用时,如何分配父类和子类之间的代码的处理级别就需要大家们自己斟酌了,如果父类中实现的太多,就失去了模板的意义,降低了父类的灵活性。但是父类实现的太少也会导致子类中的代码重复,所以一切看大家的感觉了。
Factory Method模式(工厂模式)
将实例的生成交给子类
什么是工厂模式呢?通过一个Factory 生成一个 Product, 而 Factory和 Product 的实现是由子类来实现的,使用了模板模式。所以你可以定制自己的工厂生产出你想要的实例。 举个例子来说,我们把一种面点机器作为Factory,而生成的面点是类Product。面点机器生成面点用方法create(),而面点可以被吃掉有方法eat()。
protocol Factory { // 定义了一个工厂
func create() -> Product
}
protocol Product { // 定义了一个产品
func eat()
}
但是我们并没有定义机器如何生产面点以及面点该如何被吃掉,这个时候我们创建类Dumpling Machine(饺子机器)继承于Factory 并实现方法create().
接下来我们实现类Dumpling 继承至Product 并实现了方法eat()。然后我们就可以通过DumplingMachine的create() 来生成Dumpling的实例了。
class DumplingMachine: Factory {
func create() -> Dumpling {
return Dumpling()
}
}
class Dumpling: Product {
func eat() {
... // 实现怎么吃饺子
}
}
这就是工厂模式的使用,那我们什么时候使用工厂模式呢。它又有什么好处呢?
工厂模式将框架和具体实现分离开来了,当我们实现自己的框架(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 只是提供了复制的方法,你不能获取和修改母体。
protocol Product {
func copySelf() -> Product
func use()
}
class Manager { // 用来管理可以自我复制的实例
private var datas: [String: Product] = []
public func register(name: String, object: Product)
{
datas[string] = object
}
public func createClone(name: String) -> Product {
return datas[name].copySelf
}
}
class State: Product {
var height: Double = 0
var weight: Double = 0
var age: Int = 0
func run() {
print("run")
}
func copySelf() -> State {
return self.copy()
}
func use() {
run()
}
}
Builder 模式 (构建模式)
组装复杂的实例
有时候,当我们在构建一个复杂的模块的时候,我们需要将其拆分出来。形成一个个小的组件,然后通过一个管理类,进行重新组合。这样的话我们可以最大限度的提供代码的复用性及可替代性。
Builder 的思路十分的简洁,主要分为Director(管理者), Builder(构建器)和 Buinder 的子类。Director 通过一个 Builder 的实例来生成所需要的数据,而数据的具体实现方式,则是通过子类来实现的。Builder 中应当涵盖构建数据所需要的所有必要方法,但是不应当含有特别的方法。这样 Director 可以通过一个 Builder 来实现功能。
而具体的实现方式,它就不知道了,它只是知道其调用了一个 Builder,但是 Builder 有很多,它并不知道调用的是哪一个 Builder。这种不知道则提高了模式的灵活,只有不知道,才能够被替换。
protocol Builder {
func playVideo()
}
class Director {
var builder: Builder? // 一个可以播放视频的构建器
func playVideo() { // 管理者想要实现播放视频的功能
builder?.playVideo()
}
}
class Mp4Player: Builder { //实现一个 mp4播放器
func playVideo() {
self.playWithMp4()
}
func playWithMp4() {
... // 特有方法,以 MP4格式播放
}
}
class AviPlayer: Builder { // 实现一个 Avi 播放器
func playVideo() {
self.playWithAvi()
}
func playWithAvi() {
... // 特有方法,以 Avi格式播放
}
}
// 实际的使用中,你可以为 Director 提供 mp4或是 avi 播放器
// 只要符合`Builder`标准, 你可以随意替换 builder 及其内部的实现
// 而不影响其他的代码
main {
let player = Director()
player.builder = Mp4Player()
// palyer.builder = AviPlayer()
}
到现在为止,大家们对抽象这个概念应该都很了解了,抽象可以是
抽象类也可以是接口,抽象的目的就是隐藏实现而突出逻辑。将逻辑和实现分开是实现代码复用和提高维护性减少耦合常用的方法。以后如果提到抽象,希望大家都能理解它的含义。
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,
class NumberGenerator {
var value: Int = 0 // 在这里简单的表示为自己的值
public var observers: [Observer] = [] // 储存所有的观察者
func addObserver(ob: Observer) {...} // 添加观察者
func deleteObserver(ob: Observer) {...} // 删除观察者
func notifyObserver() {...} // 通过所有的观察者,数据发生改变
func excute() {...} // 执行数据跟新
func getNumber() { FatalError() }// 交给子类实现,实现数值如何生成
}
class RandomNumberGenerator: NumberGenerator {
func getNumber() {...} // 返回一个随机数
}
接下来就是创建一个接口Observer,只有实现了此接口的类才能成为 NumberGenerator 的观察者。它只有一个 update 方法 [在 swfit 中接口相当于协议]
protocol Observer {
func update(obj: NumberGenerator)
}
class Display: Observer {
func update(obj: NumberGenerator) {
print(obj.value)
}
}
我们通过Dispaly的实现,将每次订阅到的值显示出来。下面是一个简单的使用
let generator = RandomNumberGenerator()
let observer = Display()
generator.addObserver(observer)
generator.excute()
// print: 2 打印出一个随机数
以上就是一个简单化的Observer 模式的使用,如果细心的话你会看到,我们直接将被观察者本身返回给了观察者。一个对象可以同时被很多观察者观察,但是观察者想要获取的信息可能各有不同,所以直接将自身传递,让观察者自己去查找。
当然了,这是由于我们的设计过于简陋。在 Objective-C 中,我们可以直接监听各个对象的属性。
其实,观察者模式,我们也可以称为订阅模式。观察者并不是去主动观察,而是被观察者通知观察者的。如果理解为发布和订阅就更加契合了,你可以订阅一个对象,如果他发布了新的内容,你就会得到通知。
到这里,如果你上面的各种模式都了解了一遍的话,你就会发现,在很多模式中已经出现了很多的这种可替换性设计了。通常进行替换性设计,可以提高系统的灵活性和降低耦合性。一般我们通过以下两者方式进行替换性设计。
- 利用抽象类和接口从具体类中提取出抽象方法
- 在将实例作为参数传递至类中,或是在类的字段中保存实例时,不使用具体的类型,而是使用抽象类和接口
使用这种设计我们可以轻松的替换项目中的具体类。
Momento 模式
保存对象状态
我们平时使用的文本编辑器、PS 等等,都有一系列十分重要的功能,就是撤销(undo)、 重做(redo) 和历史快照(history)。像是撤销这样的操作我每天要使用几百次,那如何记录每个操作节点的状态就十分重要了。
而 Momento 模式就十分善于处理这种情况,Momento 有纪念品的意思,我们也可以想象着把一个对象每个时间点的状态拍上一张照片作为纪念品。
当我们需要的时候,我们可以通过每个时间点的快照来恢复对象的状态。比如我们要记录一个棋局,类ChessGame表示一局正在进行的棋盘。里面有方法createMomento()通过当前棋子的数据存储快照。我们是创建一个类Momento 来存储棋局数据的。生成的快照被存入棋局的数组history 中,当调用undo()方法时,我们就取出最后一个棋局状态进行棋局的复原,这就是Momento 模式。
class ChessGame {
private var chessmanLocations: [Any]! // 这里面是此次双方旗子的位置信息
private var history: [Momento]? // 所以得快照数组
func undo() {
let state = self.history.pop()
... 根据信息恢复所有的棋子数据
} // 撤销
func createMomento() {
let mom = Momento(self.chessmanLocations)
self.history.append(mom)
} // 生成一个快照,并存入数组中
}
class Momento {
var chessmanLocations: [Any]!
}
这里的 Momento 模式和以前的 Prototype 模式在存储状态上也一点点相似,但是这里的Momento 只是存储恢复状态所需要的必要数据,而Prototype 模式中,实例复制成的则是完完全全相同的另一个实例,所以它们的区别还是很明显的。
State 模式 (状态模式)
用类表示状态
有些时候我们在项目当中会遇到各种各样的状态,比如应用的夜间模式和白天模式,再或者是一个警报系统的各个预警状态。使用夜间、白天模式是一些阅读软件常备的功能,切换不同的模式,整个应用的界面会发生色调的转变。而警报系统在不同的预警状态下,对同一事件的处理方式也是不同的。
针对这种需要根据状态判断的例子,我们通常使用的方法,就是通过 if或是switch 来判断不同的状态,而执行不同的实现方法。比如应用的夜间和白天模式:
class Manager {
public var isNight: Bool
func navBarColor() -> UIColor {
if self.isNight {
return UIColor.black
} else {
return UIColor.white
}
}
func bgColor() -> UIColor {
if self.isNight {
return ...
} else {
return ...
}
}
...
}
这个就是我们一般的实现方式,这样的实现方式在简单的状态切换时到没有什么。但是像是以上这样的白天和黑夜模式的界面颜色获取,可能有几十个方法,一个类中满满的都是if 看起来就眼花。如果这个时候你需要添加另一个模式,你就需要在每个方法下面添加一个 else if,重要的是,编译器并不会因为你忘记写一个,而通知你, 所以,在添加新的模式时,我们很容易出错,接下来就是用到 State 模式的时候了�。
通过一个类来表示一个状态,就是状态模式。 在State 模式中我们通过创建一个类来表示一个新状态。像以前一样,我们需要创建一个抽象类State 来定义状态中需要实现的方法。接下来我们分别定义NightState 和DayState来表示白天和黑夜的状态,通过以下的代码我们来看看有什么区别。
public class State {
public func navBarColor() -> UIColor {
FatalError("no implementation")
}
public func bgColor() -> UIColor {
FatalError("no implementation")
}
}
class DayState: State {
static let instance = DayState() // 状态不需要重复创建,使用单例模式
class func shared() -> DayState {
return self;
}
override public func navBarColor() -> UIColor {
return UIColor.white
}
override public func bgColor() -> UIColor {
return UIColor.white
}
}
class DayState: State {
static let instance = DayState() // 状态不需要重复创建,使用单例模式
override public func navBarColor() -> UIColor {
return UIColor.black
}
override public func bgColor() -> UIColor {
return UIColor.black
}
}
class UIManager {
var currentState: State;
func resetState() {
let date = NSDate()
if (date => 9am && date <= 7pm) {
self.currentState = DayState.instance
} else {
self.currentState = NightState.instance
}
}
func showNavBarColor() {
setupNavBarColor(self.currentState.navBarColor)
}
func showBgColor() {
setupBgColor(self.currentState.bgColor)
}
}
通过上面的例子,我想你一定明白了它们的区别。在这样的 State 模式下,UIManager 是用来控制界面的颜色显示的。它负责切换和控制状态,所以它需要知道所有状态的条件。
除了让UIManager 控制状态的切换外,我们还可以让每个状态本身去控制现在的状态,这里就像是 Chain of Responsibility 模式(责任模式)。我们扩展一下这个协议:
extension State {
func setTime(manager: UIManager, time: Date) {
}
}
// DaySate 和 NightState 需要将上对应的方法
class DayState: State {
.....
func setTime(manager: UIManager, time: Date) {
if (date < 9am && date > 7pm) {
manager.currentState = NightState.instance
}
}
}
class NightState: State {
..... func setTime(manager: UIManager, time: Date) {
if (date >= 9am && date <= 7pm) {
manager.currentState = DayState.instance
}
}
}
可以看出来,UIManager 只需要默认一个状态,然后再调用方法前,告知当前模式时间,它就可以通过自己的判断来寻找正确的状态。这里的状态只有两种,如果有很多种的话,自己不是此状态,就传递给下一个状态,直到找到一个正确的状态。
使用第一种方法,manager 就需要知道所有的状态关系。但是耦合度很低,各个状态不需要知道其他的状态。
而第二种方法,每个状态或多或少的需要知道其他的状态,这样增加了耦合度。不过 Manager 不用再管理所有的状态了,它只需要处理方法就行了。
-
我们可以方便的添加各种各样的状态我们只需要实现
State的方法就行了,可能还需要处理一下切换到其他状态的情况,不过这是你使用第二种Manager 管理的时候。 -
添加依赖于状态的处理十分的麻烦当我们对状态添加一个新的处理方法的时候,我们需要修改每一个状态,这十分的麻烦。所幸的是,我们不会忘了给其中的一个状态添加新的处理方法,因为编译器会提示我们,如果我们忘记了给任意一个状态添加方法。如果不使用
State 模式就不会得到编译器的帮助,可想而知,一旦大意,就会引发不可知的 bug。
避免浪费
Flyweight 模式 (轻量级模式)
共享对象,避免浪费
我们都知道在应用当中使用的对象都占用了一定的系统内存,当我们的对象占用内存过大时,就会降低系统的运行速度和稳定性,甚至引发崩溃。 如果有些对象可以被共同使用,就可以减少创建新对象的开销,也可以降低内存的占用。所以 Flyweight 模式就是 通过尽量共享实例来避免 new 出新的实例来大大降低系统的内存消耗。
这里我们举一个例子,比如我们要打印一张图片,而这张图片是又几种不同的素材图片拼出来的。当我们在在打印图片的时候,我们需要先将对应的素材按照顺序排列好,才能进行打印。
class Image {
let id: Int
let data: Data?
init(id: Int) {
self.id = id
self.data = createData(id)
}
func createData(id: Int) -> Data {
... // 根据id 生成图片的数据
return data
}
}
class ImageManager {
var imageIds: [Int] = [] // 需要排列的图片 id 数组
var imageCache: [Int : Image] = [:] // 每个 id 对应一个它的图片缓存
// 通过 id 获取图片,如果缓存中有的话就直接使用,如果没有的话,就创建一个放入到缓存中
func getImage(id: Int) -> Image {
if let image = imageCache[id] {
return image
} else {
let image = Image(id)
imageCache[id]= image
return image
}
}
// 打印图片,根据 id 数组的顺序进行排序
func printImage(imageIds: [Int]) {
self.imageIds = imageIds
var images = imageIds.map { return getImage($0) }
printWithImageData(images)
}
func printWithImageData(imageDatas: [Image]) {
... //根据图片的数据进行打印
}
}
我们创建Image 当做是素材,ImageManager是用来排版素材的类,它通过传入一个包含素材 id 的数组来打印出对应的图片。 在排列过程中,我们每种素材的信息其实是不变的,所以它是可以共享的,我们使用一个字典把 id 当做 key来 实现缓存素材数据。当通过 id 排列素材时,我们直接获取缓存中的素材数据,如果重复使用了一个素材,也不会再次创建,而是共享一个对象。通过这样的方式,我们就能够减少一大部分的内存消耗。
不过共享同一个对象也有问题,就是改变了这个对象,那么所有共享它的也会发生改变。这有时候是好事,有时候是坏事,具体要看应用的场景。
但是大概可以这样判断是否该共享该对象。
-
代表本质的,不依赖于状态和位置的对象可以共享它是一个
intrinsic 信息。 -
外在的,依赖于状态和位置的对象不能共享 它是一个
extrinsic 信息。
一般的对象都适用于这两个规则。根据项目的实现目的,灵活的运用Flyweight 模式可以优化你的应用内存占用。
Proxy 模式 (代理人模式)
只在必要时生成实例
当读到代理人模式的时候,希望你不会把它和OC 中的delegate 弄混淆了。OC中的 delegate 其实是接口interface或者说是protocol 的使用,而我们今天要了解的Proxy 模式中的代理人指的是替原本的对象来执行操作。
在哪些情况下,我们需要使用Proxy 模式呢? 通常是当一个对象的创建需要消耗大量的性能,而它的重要操作又可以延后的时候。在这种情况下,如果需要使用此对象,就立刻创建,可能会占用过高的性能,而后又没有使用到这个对象的重要功能,那岂不是白白浪费了大量的系统算力。
所以我们需要使用一个代理人来替代这个本人。它实现这个本人的基本属性和方法,而将耗时的工作交给真正的本人去做,那样只有在真正需要本人去做得事情才会去创建本人,而其他的不耗时操作将交给代理人去做。
这里我们举一个例子,比如有一个打印图片的类ImagePrint,它通过一个url 来初始化实例,调用Print 方法就可以打印出这张图片,这就是本人。又有一个类ImagePrintProxy 表示它的代理人。接口ImagePrintable 规定了本人和代理人都应该具备的方法和属性。下面我们通过伪代码来具体了解一下整个过程:
// 首先是接口 ImagePrintable,它定义了一个能打印图片的类,都需要实现什么方法
protocol ImagePrintable { func setUrl(urlStr: String) // 设置图片地址
func getUrl() -> String // 获取图片地址
func print() // 根据地址,打印图片
}
// ImagePrint 本人,它是打印图片的实际操作者,打印图片是一个耗时的操作,
class ImagePrint: ImagePrintable {
var url: String
var printer: PhotoPrinter? // 这是一个图片打印机,初始化它需要耗费大量的时间
init(url: String) { self.url = url
self.printer = PhotoPrinter() // 这是一个耗时操作
}
func getUrl() -> String {
return self.url
}
func setUrl(urlStr: String) { self.url = urlStr
}
func print() {
self.printer.printImage()
... // 根据 url 下载图片然后使用 printer 再打印出来
}
}
// 最后就是 ImagePrintProxy 代理人,通过代理人我们可以在不打印图片时
// 设置和获取图片的地址,而不用初始化 ImagePrint。因为初始化`ImagePrint` 时
// 会创建`printer`,这会耗费大量的时间。而是在调用 print 的时候,在初始化它。
class ImagePrintProxy: ImagePrintable {
var url: String
var real: ImagePrint? // 这是真正执行打印操作的对象
init(url: String) {
self.url = url
}
func getUrl() -> String {
return self.url
}
func setUrl(urlStr: String) {
if self.real != nil { //当存在本人时,就设置本人的值
self.real.url = urlStr
}
self.url = urlStr
}
func print() {
self.release()
self.real.print() // 调用本人来实现打印图片的方法
}
func release() { // 生成原始对象的方法
if self.real == nil {
self.real = ImagePrint(self.url)
}
}
}
看过这个例子以后,就很容易理解什么是Proxy 模式了,使用 Proxy 模式的时候,调用者并不关心是谁实现了里面的方法,它只是调用了符合ImagePrintable 的类。而实际的执行者ImagePrint 也不关心自己是被直接调用还是间接调用。对问题的处理就交给了ImagePrintProxy 这个代理人身上。这样的话,代理人就可以根据实际的情况来替�本人完成一些简单的工作,而尽量将本人的创建延后,只在真正需要使用的时候,才会创建本人。
这样的设计,对外显示出了一致性,在不影响调用关系的情况下。节省了系统的性能消耗,能提高应用的流畅性。
用类来表示
Command 模式 (命令模式)
命令也是类
通常我们所说的命令都是实例的方法,虽然调用的结果会在实例的状态中得到反馈,但是却无法留下调用的历史记录。当我们想要把每一次调用都记录下来时,我们可以把类当作命令来看待,使用类来表示要做的操作。这样我们管理一系列操作时就是直接管理这些命令类的实例,而不是通过方法进行动态操作了。
那我们该如何进行设计以实现Command 模式呢,一样,我们举一个例子。比如我们实现一个和Flyweight 模式(上上个模式)一样的功能,通过素材打印图片,这里我们再为它添加一些新的功能,并进行优化。
- 如果素材进行排列的时候,不是按照顺序,而是有各自的坐标
- 并且每添加一个素材我们就立即打印出来。
现在我们把每次添加一个素材的操作不在看做是一个方法里面的循环执行,而是一个个命令。我们需要一个接口Command (interface)表示什么是命令,命令很简单,只需要能执行就 OK 了。 每次绘制素材的操作用DrawCommand来表示,它继承于Command。
有时我们可能需要执行一系列的操作,所以我们需要一个表示操作集合的类MacroCommand,它同样也继承于Command,在MacroCommand 中有添加和移除Command 的命令,同样有保存所有操作的属性commands。
有了命令,但是命令本身不执行具体的绘制操作,它仅仅是提供操作的具体数据。我们还需要一个绘制类,这个绘制类我们不具体创建,而是通过一个接口Drawable 来表示, Drawable 需要实现绘制方法draw()。为什么这样设计,如果你看了以上的设计模式,我想你应该已经很清楚了。使用接口,能方便的替换绘制实现,也为你要绘制不同的东西提供了扩展的可能性,并且不影响其他代码的结构,这就是代码的可替换性。
这里我们用ImageDrawCanvas 来表示一个简单的绘制图片的图层。下面是伪代码的实现
// 命令接口,只定义了 excute
protocol Command {
open func excute()
}
// DrawCommand 表示绘制命令的类
class DrawCommand: Command {
var url: String // 图片地址
var position: Point // 图片位置
var drawable: Drawable // 执行绘制操作的图层,并未指定具体的类型,而是接口 Drawable
// 初始化一个命令
init(url: String, position: Point, drawable: Drawable) {
self.url = url
self.position = position
self.drawable = drawable
}
func excute() { // 执行绘制命令
self.drawable.draw(url: self.url, position: self.position)
}
}
// MacroCommand 一个命令集合
class MacroCommand: Command {
var commands: [Command] // 所有的命令,只要是`Command` 就可以,这意味着不但可以添加`DrawCommand`还可以添加`MacroCommand`,命令集合在本质上还是命令。
func addCommand(command: Command) { // 添加一个命令
if command != self { // 不能添加添加自己,防止死循环
self.commands.append(command)
}
}
func undo() { // 移除最后一个命令
self.commands.removeLast();
}
func clear() { // 移除所有的命令
self.commands.removeAll()
}
func excute() { // 执行命令
for command in self.commands { // 遍历执行所有的命令
command.excute()
}
}
}
// 绘制接口
protocol Drawable {
func draw(url:String, position: Point)
}
// 图片绘制类
class ImageDrawCanvas: Drawable {
var history: MacroCommand // 绘制的命令历史,当你需要重新绘制的时候,可以直接调用
var size: Size // 画布大小
init(size: Size, history: MacroCommand) {
self.size = size
self.history = history
}
func draw(url: String, position: Point) {
... // 根据图片的地址和坐标,进行图片的绘制
}
func redo() { //重新绘制
self.history.excute()
}
}
// 所有的类都准备好了,我们来看一下如何操作
func main { var history: MacroCommand = MacroCommand()
lazy var imageCanvas: ImageDrawCanvas {
return ImageDrawCanvas(Size(width: 1000, height: 1000), self.history)c
}
func viewDidload() {
super.viewDidLoad()
for i in 0...100 { // 循环添加100个素材
let command = DrawCommand(url: "http://www.ssbun.com/12.png", position: Point(x: i, y: i), self.imageCanvas)
command.excute() // 执行绘制
self.history.addCommand(command) // 加入到历史记录中
}
}
}
以上伪实现了一个Commnad 模式的图片绘制功能,不过Command 模式的主要实现就是这样的。通过具象一个操作为一个实例,我们能精准的操控每一个操作,并重复任意的步骤。我们还可以将这些实例进行归档处理,永久保存我们的操作记录。在我们了解的以上所有的设计模式,除了本文的把类作为命令,还有State 模式中的把类作为状态。 以后再遇到操作是需要在实例的方法内进行很多的判断和选择,你可以试着将不同的情况拆分为不同的类来实现,或许会豁然开朗。
Interpreter 模式 (翻译模式)
语法规则也是类
又多了一个用类来替换某些东西的类,而这次,我们这模拟的是语法。在某些特殊的情况下,我们可能想要设计一种新的迷你语言来方便的编写繁琐的操作。例如正则表达式就可以通过简短的语法来描述复杂的筛选条件。我们也可以设计一款小语言来这样做,再编写一个翻译程序将它翻译成你所使用的语言。而其中的各种语法可以被翻译为不同的类,比如Add 作为 +, CommandList 作为 repeat 等等。但是,这个过程还是很麻烦的,这里的篇幅已经很长了。而叙述一个迷你语言,或许需要更大的篇幅才能讲明白,而这篇文章只是想要使用简单的文字来帮助你了解所谓的23设计模式。
结语
终于看完了所有的23种设计模式,其实很多的设计模式已经不知不觉中被我们使用了无数次了。对于经验丰富的程序员而言,设计模式中的方法在他们看来是理所应当的。毕竟,设计模式本身就是对前辈们经验的总结,本身并没有什么突出的特点。它也不能帮你解决所有的问题,但是通过了解设计模式,我们可以更快的学习到前辈的经验。在实际的使用中,对我们的帮助是显而易见的。
设计模式虽然很重要,但是你却不用想着把它们都记在自己的脑海中。死记硬背从来都不是好方法,你只要有些许的印象,知道遇见这样的问题时该使用什么样的模式,随后再去查询具体的资料就是行了,善用搜索引擎可是程序员最重要的一项技能。
说了那么多,最后再说点我的感悟。
语言技巧很多,黑魔法很多,设计思想也很多,学完所有为大家所称赞的思想和技巧,也并不能让你的项目看起来更完美。遇见问题时,越是简单的实现就越有可能解决问题,也更容易被人看懂。让程序看起来简单,而不是让它看起来 NB。有一句话说的好 “要让程序看起来明显没有问题,而不是没有明显的问题。”









网友评论