iOS面试一战到底
上QQ阅读APP看书,第一时间看更新

2.5 结构型设计模式

结构型设计模式主要用来指导类或对象的组织结构,可分为类结构型和对象结构型。类结构型主要通过继承和接口的方式对类的关系进行布局,对象结构型主要通过组合和聚合来对对象的结构进行布局。

在23种经典的设计模式中,结构型设计模式包含代理设计模式、适配器设计模式、桥接设计模式、装饰设计模式、外观设计模式、享元设计模式和组合设计模式。

2.5.1 代理设计模式

在iOS开发中,代理设计模式非常常用。在UIKit框架中,UITableView、UITextView等组件的渲染和交互都采用了代理设计模式。代理设计模式结构比较简单,也非常容易理解,其核心为在具体的功能类与使用者之间建立一个中介类作为代理,使用者通过代理对象来对真实的功能类进行访问。

以病人预约看病的软件系统设计为例。首先整个软件系统中的核心功能类只有两个,即医生类与病人类,病人看病前首先需要预约医生,预约完成后才可进行问诊,之后病人向医生陈述病情,最后医生看病开药。在整个软件系统中,实际上有些行为并非属于医生类也并非属于病人类,如医生的预约、问诊过程的控制等,这时就需要一个代理类,代理医生类处理这些行为,示例代码如下:

如上代码所示,病人类并没有直接和医生类进行交互,而是通过中间的代理类。在实际开发中,使用代理设计模式可以使具体的功能类的聚合性更强,并且可以在某些功能的执行前后进行额外的准备工作和善后工作。

2.5.2 适配器设计模式

适配器设计模式并不是软件设计中的最佳实践。其主要是为了解决软件开发过程中新旧模块不兼容的问题。适配器设计模式的定义是:将一个类的接口转换成使用者期望的另外的接口,使得原本接口不兼容的类可以一起工作。

一般情况下,好的系统设计不需要使用到适配器模式。在实际应用中,一个项目的开发和迭代过程可能会非常久。时间越长,项目越大,旧的逻辑模块重构的成本就越高。适配器模式提供了一种思路,可以低成本地使新旧模块配合工作。

当数据模型版本升级时,可使用适配器模式来兼容旧的数据模型,代码如下:

在实际开发中,由于数据模型升级造成的代码不兼容问题会经常遇到,当项目过于庞大时,如果贸然修改以往的旧代码,会有很大的工作量,同时也会伴随着很大的风险,使用适配器模式就是一种比较适合的折中选择。

2.5.3 桥接设计模式

桥接设计模式是合成复用原则的一种应用,其核心是将抽象与实现分离,用组合关系来代替继承关系,从而给类更多的扩展性,降低类之间的耦合度。

在实际应用中,当某个类具有多种维度的属性时,在组织类的结构时,使用桥接模式十分适合。例如,汽车从功能上可以分为小轿车和公交车等,从颜色上又可以分为黑色汽车和白色汽车等。示例代码如下:

上面的代码首先定义了颜色和汽车类型两个枚举,通过组合的方式来构建汽车对象,避免了因继承带来的耦合问题。之后通过定义TransportProtocol协议来抽象地描述汽车对象。还有一点需要注意,Swift语言支持对协议进行扩展,即可以对协议中的方法提供默认的实现,通过Swift语言的这一特性,具体的Transport类的实现就变得非常简单。

2.5.4 装饰设计模式

从字面意思上理解,对某个事物的“装饰”是指在不改变事物本身性质的基础上添加修饰。在软件设计中也是如此,装饰设计模式的定义为在不改变对象结构的情况下,为该对象增加一些功能。在现实生活中,“装饰”随处可见,例如汽车中的挂饰、手机的屏保和外壳、墙上的壁画等。

以为墙壁添加装饰贴纸的逻辑设计为例,代码如下:

如上代码所示,WallProtocol协议定义了抽象的功能接口,Wall类是实现WallProtocol协议的具体功能类,StickerDecorator类是具体的装饰器类,需要注意,装饰器类也需要完整的实现功能类所实现的接口,这样才不会改变被装饰对象的原始行为。使用装饰模式也可以理解成为对象的行为进行扩展,只是相比较于继承,装饰模式更加灵活、类之间的耦合度也更低。同时,装饰模式可能由于过度设计而增加过多装饰器类,使系统的复杂性变高。

2.5.5 外观设计模式

外观设计模式是迪米特原则的一种实践。在现实生活中,开一家餐馆可能需要与多个社会部门进行交互,例如房屋管理部门、食品安全部门、卫生许可部门、营业许可部门、税务部门等。商户同时参与处理多个流程会非常复杂,这时通常可以求助统一的中介帮商户处理,这就是外观模式的一种现实应用。

在软件设计中,当一个系统的功能越来越强时,子模块会越来越多,应用端对系统的访问也会越来越复杂。这时可以通过提供一个外观类来统一处理这些交互,降低应用端使用的复杂性。

我们以客户购买商品流程的设计来演示外观设计模式的应用,代码如下:

如上代码所示,User类描述顾客,Goods类描述商品,Cashier类描述收银台,Package类描述商品包装机器,顾客完成一个购物流程需要同时与商品类、收银台类和包装机器类进行交互。当每一个模块都变得越来越复杂时,代码的扩展和维护将变得十分困难。对于这样的场景,可以定义一个外观类来统一处理用户的购物逻辑。对于本示例,商店类可以起到外观的作用,顾客只需要与商店一个类进行交互即可。重构代码如下:

2.5.6 享元设计模式

享元也是结构型设计模型的一种。在软件运行过程中,有时会需要创建大量的重复对象,大多时候这些对象内部都有很大一部分数据是重复的,会极大地消耗系统的资源。享元设计模式就是解决这种问题的。享元模式的定义为:通过运用共享技术实现大量细粒度对象的复用,避免大量重复对象造成系统的资源开销。

享元模式并不是任何场景都适用的。为了实现数据的共享,在享元模式中,需要根据共享性将对象中的数据拆分成内部状态与外部状态,之后将内部状态封装成享元对象用于共享。享元模式会增加系统的复杂度,对于不会产生大量重复对象的系统并不适用。

下面以黑白棋棋子的设计为例演示享元设计模式的应用:

如上代码所示,一颗黑白棋的棋子包含位置、颜色、半径数据信息。其中,除了位置每颗棋子都不同外,颜色和半径对大部分棋子来说都是相同的。这种场景小,place属性就是ChessPiece的外部状态,color与radius属性为内部状态,可使用享元模式重构上面的代码:

上面的代码使用到了Unmanaged.passRetained(chessPiece).toOpaque()这样的代码,在Swift语言中,这行代码是用来打印对象内存地址的。上面的代码创建了10个棋子对象,由于内部使用了享元对象,虽然10个棋子对象的地址各不相同,但是其内部的widget对象是共享的,因此10个棋子一共只创建了黑、白两个ChessPieceFlyweight享元对象。随着棋盘上棋子的增多,对系统资源的节省将越来越多。

2.5.7 组合设计模式

组合设计模式是我们将要介绍的最后一种结构型的设计模式。其采用树状层级的结构来表示部分与整体的关系,使得无论是整体对象还是单个对象,对其访问都具有一致性。在某些系统中数据是采用树状结构组织的,这时使用组合模式非常适用。

在面向对象的设计思想中,完整的文件系统至少需要两个类来描述:一个类为文件夹类;一个类为文件类。文件系统实际上就是树结构,文件夹内又可以嵌套文件夹。使用组合设计模式来设计这个系统,我们只需要定义一个类,示例如下:

运行上面的代码,控制台将打印如下信息:

文件1

文件2

文件3

子文件夹1

子文件1-1

子文件1-2

子文件夹2

子文件2-1

子文件2-2

通过定义统一的FileNode接口,使得使用方无论关心当前操作的节点是文件夹还是文件,都有统一的访问方式,并且屏蔽了树结构的数据中层级的概念,这是组合设计模式的最大优势。当然,这样也造成了对象职责的不明确。例如,对于文件类型的节点,其中的添加文件、删除文件、获取所有内部文件等方法都是无意义的,在实际应用中需要针对文件类型的节点让这些方法抛出异常。