2.4 创建型设计模式
本节开始我们将正式进入具体设计模式的学习,23种常用的设计模式从类型上可以分为创建型模式、结构型模式和行为型模式。
创建型设计模式的核心在于“对象的创建”,其目的是将对象的创建与使用分离,对象的使用者无须关心对象是如何创建出来的。创建型的设计模式包括单例模式、原型模式、工厂方法模式、抽象工厂模式和建造者模式。下面,我们将逐一介绍这些设计模式的思想与应用。
2.4.1 单例设计模式
在iOS开发中,单例模式的应用非常广泛。系统原生提供的很多类的设计都采用了单例模式。在软件系统中,有时为了节省内存资源并保证数据内容的一致性,需要要求某些类只能创建一个实例。类只能有一个实例,这就是单例设计模式的核心定义。
单例设计模式有如下特点:
- 单例类只有一个实例对象。
- 单例类的实例对象由自己创建。
- 单例类需要对外提供一个访问其实例对象的接口。
在软件设计中,有关全局共享的资源数据、大型通用的管理类等都可以使用单例模式,例如登录用户的用户信息类、全局的计时器或计数器类、程序的日志管理类等。
下面我们以一个公共钱包的场景作为示例讲解单例模式,首先新建一个Swift语言的playground文件,在其中编写如下代码:
如上代码所示,假设我们生活在一个集体中,我们每个人的消费都由这个集体的公费进行支付。上面定义了3个类:Goods类用来描述商品;PublicWallet类为公用的钱包类,其中封装了余额属性,并且提供了充值与提现的方法;Customer类用来模拟购买商品的顾客。从最终代码的运行结果可以看出,首先顾客Jaki和Lucy都得到了一个公费为100元的钱包,Jaki购买玩具花费了30元,此时公费钱包余额为70元,但是Lucy并不知道Jaki已经进行了消费,她想要购买价值90元的图书,由于我们设计的PublicWallet类并非单例,因此在Lucy的公共钱包中依然有100元的余额,Lucy也可以消费成功,这违背了我们程序的最初设计。因此,我们需要找到一种方式使得PublicWallet成为真正的公共钱包,所有顾客共享。改写代码如下:
运行代码,控制台将打印如下信息:
从钱包取出现金:30.0,余额:70.0
余额不足,余额:70.0
存入成功,余额:270.0
从钱包取出现金:90.0,余额:180.0
从钱包取出现金:100.0,余额:80.0
从打印信息可以看出,此时顾客Jaki和Lucy实际上是共享同一个公共钱包。在Swift语言中,使用静态场景是构建单例类最常用的一种方法,但是需要注意,如果一个类要成为单例类,则需要将其对外的构造方法都进行隐藏,即将构造方法声明成private类型的,这样可以防止开发者的误操作使得单例类实例化出多个对象。
在实际的项目开发中,类的结构可能非常复杂,组织出来的文件数量也会很庞大,单例设计模式可以从设计上保证类实例的唯一和共享,因此在软件设计时,对于全局共享的数据,要尽量使用单例设计模式来构建。
其实,与单例设计模式的思想类似,有时在软件设计时还会采用多例模式,即一个类只会构造出有限个数的实例,之后对这些实例进行复用来节省空间资源。在iOS原生开发框架中,UITableView列表在构建其Cell数据时就采用了这种设计模式,极大地提高了列表的渲染性能,节省了设备的内存空间。
2.4.2 原型设计模式
原型设计模式也是创建型设计模式之一,提供了一种大量创建复杂对象的方法。原型设计模式的定义为:以一个已经创建的实例作为原型,通过复制该原型对象来创建出新的对象,在使用对象时,使用者无须关心对象创建的细节。在iOS开发中,很多原生类提供了clone方法来快速地创建对象,这就是对原型设计模式的一种实现。
以计算机类的设计为例来演示原型设计模式的应用,代码如下:
如上代码所示,简单设计的计算机类由屏幕与主机组成,屏幕包含分辨率属性,主机包含内存、硬盘和CPU相关属性。在上面的代码中,创建计算机对象做了很多工作,一个计算机对象一旦被创建就会拥有一个唯一的编号。按照上面代码的逻辑,如果需要再创建一个新的配置一样的计算机,则依然需要做大量的配置工作,可以使用原型设计模式来对上面的代码进行改造,修改Computer类如下:
通过在Computer类中增加一个copy方法来快速地生成相同配置但编号不同的计算机,使用如下:
let resolution = Resolution(x: 1200, y: 840) let screen = Screen(resolution: resolution) let cpu = CPU(frequency: 2400) let host = Host(ram: 1024, disk: 1024 * 100, cpu: cpu) let computer = Computer(screen: screen, host: host) computer.printInfo() let computer2 = computer.copy() computer2.printInfo()
通过使用原型模式,一旦第一个对象被创建出来,后面的对象创建都将变得非常容易。实际上,在应用中,我们还可以通过工厂模式将第一个对象的创建也封装起来,将对象的创建与使用完全隔离。
在原型设计模式中,作为模板的对象被称为原型,创建出来的对象拥有和模板对象一致的属性和方法,因此在一些编程语言中会通过原型模式来实现类的结构和继承关系,目前非常流行的JavaScript语言就是其中的一种。
2.4.3 工厂方法设计模式
在原型设计模式一节中,使用原型对象可以快速复制出相同的对象。优化了重复创建大量对象的过程。工厂方法设计模式注重于将对象的创建过程封闭起来,通过定义抽象的工厂接口和商品接口来隐藏负责对象创建的具体类。
在使用工厂方法模式设计的软件系统中,对象的使用者不需要知道具体的类就可以获取到需要使用的对象,系统中增加新的工厂实现类时对之前的代码也不会产生任何影响。图2-8所示为工厂方法设计模式中的UML类图结构关系。
图2-8 工厂方法设计模式
以上一节编写过的计算机对象设计代码为例,计算机可能会因为配置不同而分为低档、中档和高档3类。使用工厂方法模式对计算机对象的创建进行改写,具体如下:
使用工厂方法模式改写后的代码,计算机对象的生成将变得非常容易,使用者只需要明确想要哪一个等级的计算机,工厂类就会自动创建出对应的计算机对象。实际上,如果新增加了一种创建方式完全不同的计算机,我们只需要新建一个遵守ComputerProtocol的计算机类,之后在ComputerFactory类中统一处理这类计算机对象的创建即可,对计算机对象的使用者来说过程完全是隐藏的。
2.4.4 抽象工厂设计模式
抽象工厂设计模式是工厂方法设计模式的一种升级。工厂方法适用于同一个工厂生产一系列相关产品的场景下,其可以隐藏产品生产的具体细节。很多时候,我们需要的产品需要由不同的工厂生产,就如生活中一个大的电器工厂下面可能有多个分厂,这些分厂分别负责不同电器的生产一样。在面向对象的软件设计中,我们也会遇到这样的场景,抽象工厂模式就非常适用于这样的场景。
抽象工厂设计模式的核心是为各种类型的对象提供一组统一的创建接口,使用者无须关心这些对象具体是哪个工厂类创建出来的。我们以生产计算机和电视对象的设计为例,为了简化代码,这里将计算机类与电视类进行了精简:
如上代码所示,goods1对象实际上是由ComputerFactory工厂类构建出来的,goods2对象实际上是由TVFactory工厂类构建出来的,对于对象的使用者来说,无须关心这些细节,抽象的工厂将对象的创建与使用进行了完全的分离。
2.4.5 建造者设计模式
建造者设计模式是5种创建型设计模式中的最后一种。对于复杂对象的创建,使用建造者模式会使代码聚合性更强,逻辑更加清晰。建造者模式通常与工厂方法模式配合进行使用,工厂方法模式着重于对象的创建,建造者模式着重于创建复杂对象过程中组成对象的每一部分的创建和最终组装。
建造者模式的核心在于将复杂对象拆解成多个简单对象,通过一步步构建简单对象最终组合成复杂对象。建造者模式也是合成复用原则的一种应用,在工厂方法设计模式一节中,计算机对象的构建体现出部分建造者模式的思想,生活中这样的例子还有很多,例如许多饭店都有推出套餐服务。套餐可能包括饮料、主食、甜点等,这每一部分对象都可以独立地进行创建,之后组合成完整的套餐。示例代码如下:
如上代码所示,一个完整的套餐对象由饮料对象、主食对象和甜点对象组成,FoodPackageFactory是一个简化版的工厂方法,其中根据用户选择的套餐类型来创建不同的套餐对象,具体套餐对象的组成则是由BuilderA与BuilderB类来完成的。BuilderA与BuilderB类是建造者模式中的核心类,充当了建造者的角色。上面的代码依然有很多可以优化的地方,例如我们可以根据依赖倒置原则将建造者类的行为抽象为一个接口,这样当我们新增或修改一种套餐时,对已有代码的结构不会造成影响。