欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

掌握23种设计模式,让你的面向对象编程更加得心应手

最编程 2024-08-06 12:46:59
...

前言

设计模式 Design Pattern 是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。。

在《设计模式:可复用面向对象软件的基础》一书中所介绍的 23 种经典设计模式,不过设计模式并不仅仅只有这 23 种,随着软件开发行业的发展,越来越多的新模式不断诞生并得以应用。有经验的开发者在学习设计模式可以和过往的经验互相印证,更容易理解这些设计模式。

设计模式一般包含模式名称、问题、目的、解决方案、效果等组成要素。问题描述了应该在何时使用模式,它包含了设计中存在的问题以及问题存在的原因。解决方案描述了一个设计模式的组成成分,以及这些组成成分之间的相互关系,各自的职责和协作方式,通常解决方案通过 UML 类图和核心代码来进行描述。效果描述了模式的优缺点以及在使用模式时应权衡的问题。

为什么要学习设计模式:

  • 设计模式来源众多专家的经验和智慧,它们是从许多优秀的软件系统中总结出的成功的、能够实现可维护性复用的设计方案,使用这些方案将可以让我们避免做一些重复性的工作
  • 设计模式提供了一套通用的设计词汇和一种通用的形式来方便开发人员之间沟通和交流,使得设计方案更加通俗易懂
  • 大部分设计模式都兼顾了系统的可重用性和可扩展性,这使得我们可以更好地重用一些已有的设计方案、功能模块甚至一个完整的软件系统,避免我们经常做一些重复的设计、编写一些重复的代码
  • 合理使用设计模式并对设计模式的使用情况进行文档化,将有助于别人更快地理解系统
  • 学习设计模式将有助于初学者更加深入地理解面向对象思想

储备知识:

  • 抽象类:一般抽象类都是作为基类,比如说「电脑」就可以作为一个抽象类,根据抽象类派生出「台式电脑」和「笔记本电脑」2种具体类。一般不对抽象类进行实例化。
  • 组合优于继承:不能滥用继承来拓展功能,配合组合会更灵活。同样拿「电脑」抽象类来举例,如果使用继承,区分不同类型的「电脑」我们可以派生出「台式电脑」和「笔记本电脑」,如果再增加一个维度,根据品牌又能继续细分出「联想台式电脑」、「联想笔记本电脑」、「苹果台式电脑」和「苹果笔记本电脑」等等,如果再增加一个维度继续细分下去,显然继承是无法胜任的。这个时候可以使用继承加组合方式,组合的对象也可以进行抽象化设计:
 // 品牌
 @interface Brand : NSObject
 @interface Lenovo : Brand
 @interface Apple : Brand
 // CPU
 @interface CPU : NSObject
 @interface Inter : CPU
 @interface AMD : CPU

 @interface Computer : NSObject
 // 品牌
 @property (nonatomic, strong) Brand *brand;
 // CPU
 @property (nonatomic, strong) CPU *cpu;
 @end
 
 @interface Computer : NSObject
 @interface DesktopComputer : Computer
 @interface NotebookComputer : Computer

一、UML 类图

每个模式都有相应的对象结构图,同时为了展示对象间的交互细节, 有些时候会用到 UML 图来介绍其如何运行。这里不会将 UML 的各种元素都提到,只想讲讲类图中各个类之间的关系, 能看懂类图中各个类之间的线条、箭头代表什么意思后,也就足够应对日常的工作和交流。同时,我们应该能将类图所表达的含义和最终的代码对应起来。有了这些知识,看后面章节的设计模式结构图就没有什么问题了。

本文中大部分是 UML 类图,也有个别简易流程图。由于文中部分模式并未配图,你可以在GitHub里查看我在网络上收集的完整 23 种设计模式 UML 类图。

1.1继承

继承用一条带空心箭头的直接表示。

1.2 实现

实现关系用一条带空心箭头的虚线表示。

1.3 组合

与聚合关系一样,组合关系同样表示整体由部分构成的语义。比如公司由多个部门组成,但组合关系是一种强依赖的特殊聚合关系,如果整体不存在了,则部分也不存在了。例如,公司不存在了,部门也将不存在了。

1.4 聚合

聚合关系用于表示实体对象之间的关系,表示整体由部分构成的语义,例如一个部门由多个员工组成。与组合关系不同的是,整体和部分不是强依赖的,即使整体不存在了,部分仍然存在。例如,部门撤销了,人员不会消失,他们依然存在。

1.5 关联

关联关系是用一条直线表示的,它描述不同类的对象之间的结构关系,它是一种静态关系, 通常与运行状态无关,一般由常识等因素决定的。它一般用来定义对象之间静态的、天然的结构, 所以,关联关系是一种“强关联”的关系。

比如,乘车人和车票之间就是一种关联关系,学生和学校就是一种关联关系,关联关系默认不强调方向,表示对象间相互知道。如果特别强调方向,如下图,表示 A 知道 B ,但 B 不知道 A 。

1.6 依赖

依赖关系是用一套带箭头的虚线表示的,如A依赖于B,他描述一个对象在运行期间会用到另一个对象的关系。

与关联关系不同的是,它是一种临时性的关系,通常在运行期间产生,并且随着运行时的变化,依赖关系也可能发生变化。显然,依赖也有方向,双向依赖是一种非常糟糕的结构,我们总是应该保持单向依赖,杜绝双向依赖的产生。

二、六大原则

2.1 开闭原则

一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。

任何软件都需要面临一个很重要的问题,即它们的需求会随时间的推移而发生变化。当软件系统需要面对新的需求时,我们应该尽量保证系统的设计框架是稳定的。如果一个软件设计符合开闭原则,那么可以非常方便地对系统进行扩展,而且在扩展时无须修改现有代码,使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。随着软件规模越来越大,软件寿命越来越长,软件维护成本越来越高,设计满足开闭原则的软件系统也变得越来越重要。

为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。在Java、C#等编程语言中,可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在很多面向对象编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。

优点:实践开闭原则的优点在于可以在不改动原有代码的前提下给程序扩展功能。增加了程序的可扩展性,同时也降低了程序的维护成本。

2.2 里氏替换原则

所有引用基类对象的地方能够透明地使用其子类的对象

里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类。但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。

例如有两个类,一个类为BaseClass,另一个是SubClass类,并且SubClass类是BaseClass类的子类,那么一个方法如果可以接受一个BaseClass类型的基类对象base的话,如:method1(base),那么它必然可以接受一个BaseClass类型的子类对象sub,method1(sub)能够正常运行。反过来的代换不成立,如一个方法method2接受BaseClass类型的子类对象sub为参数:method2(sub),那么一般而言不可以有method2(base),除非是重载方法。

里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

优点:可以检验继承使用的正确性,约束继承在使用上的泛滥。

2.3 依赖倒置原则

抽象不应该依赖于具体类,具体类应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。

依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。

在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。

优点:通过抽象来搭建框架,建立类和类的关联,以减少类间的耦合性。而且以抽象搭建的系统要比以具体实现搭建的系统更加稳定,扩展性更高,同时也便于维护。

2.4 单一职责原则

一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。

单一职责原则告诉我们:一个类不能太“累”!在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。

单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。

优点:如果类与方法的职责划分得很清晰,不但可以提高代码的可读性,更实际性地更降低了程序出错的风险,因为清晰的代码会让 bug 无处藏身,也有利于 bug 的追踪,也就是降低了程序的维护成本。

2.5 迪米特法则(最少知道原则)

一个软件实体应当尽可能少地与其他实体发生相互作用

如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。

迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。

在将迪米特法则运用到系统设计中时,要注意下面的几点:在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及。在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限。在类的设计上,只要有可能,一个类型应当设计成不变类。在对其他类的引用上,一个对象对其他对象的引用应当降到最低。

优点:实践迪米特法则可以良好地降低类与类之间的耦合,减少类与类之间的关联程度,让类与类之间的协作更加直接。

2.6 接口分离原则

使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。

根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。

在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护。接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。

优点:避免同一个接口里面包含不同类职责的方法,接口责任划分更加明确,符合高内聚低耦合的思想。

2.7 合成复用原则(六大之外的)

尽量使用对象组合,而不是继承来达到复用的目的

合成复用原则就是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分,新对象通过委派调用已有对象的方法达到复用功能的目的。简而言之,复用时要尽量使用组合/聚合关系(关联关系),少用继承。

在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度。一个类的变化对其他类造成的影响相对较少,其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。

优点:避免复用时滥用继承,合理使用组合关系,增加灵活性。

2.8 六大原则 - 学习心得

六大原则中,开闭原则、里氏替换原则、依赖倒置原则 联系比较紧密,后两者是实现开闭原则重要前提,使用中通过抽象化设计具有很好的可拓展性和可维护性。

知道最少原则 可以降低耦合,减少不必要的交互,主张设计接口和类要简单易使用,将复杂的逻辑封装并提供简单易用的接口。

单一职责原则 使项目中的类和方法根据职责细分,避免单个类负担过重。职责越多,被复用的可能性就越小或使用起来越麻烦。

接口分离原则 将功能复杂的接口细分成多个特定功能的接口,只做该做的事情,降低耦合,但是细化粒度不能太细,容易导致接口过多。单一职责原则强调单个类内部根据职责细分的设计,接口分离原则强调类之间的耦合,尽量建立最小的依赖关系。

三、模式分类

《设计模式:可复用面向对象软件的基础》一书中设计模式有23个,它们各具特色,每个模式都为某一个可重复的设计问题提供了一套解决方案。根据它们的用途,设计模式可分为创建型(Creational),结构型(Structural)和行为型(Behavioral)三种,其中创建型模式主要用于描述如何创建对象,结构型模式主要用于描述如何实现类或对象的组合,行为型模式主要用于描述类或对象怎样交互以及怎样分配职责。

此外,根据某个模式主要是用于处理类之间的关系还是对象之间的关系,设计模式还可以分为类模式和对象模式。我们经常将两种分类方式结合使用,如单例模式是对象创建型模式,模板方法模式是类行为型模式。

3.1 创建型

创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将模块中对象的创建和对象的使用分离。为了使结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。

  1. 简单工厂模式(Simple Factory Pattern)
  2. 工厂方法模式(Factory Method Pattern)
  3. 抽象工厂模式(Abstract Factory Pattern)
  4. 单例模式(Singleton Pattern)
  5. 生成器模式(Builder Pattern)
  6. 原型模式(Prototype Pattern)

3.2 结构型

结构型模式(Structural Pattern)描述如何将类或者对 象结合在一起形成更大的结构,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。结构型模式可以分为类结构型模式对象结构型模式

  • 类结构型模式关心类的组合,由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系。
  • 对象结构型模式关心类与对象的组合,通过关联关系使得在一 个类中定义另一个类的实例对象,然后通过该对象调用其方法。 根据“合成复用原则”,在系统中尽量使用关联关系来替代继 承关系,因此大部分结构型模式都是对象结构型模式。
  1. 外观模式
  2. 适配器模式
  3. 桥接模式
  4. 代理模式
  5. 装饰者模式
  6. 享元模式

3.3 行为型

行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化。行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象之间的交互。

  1. 职责链模式
  2. 命令模式
  3. 解释器模式
  4. 迭代器模式
  5. 中介者模式
  6. 备忘录模式
  7. 观察者模式
  8. 状态模式
  9. 策略模式
  10. 模板方法模式
  11. 访问者模式

四、创建型 - 设计模式

4.1 简单工厂模式

简单工厂模式(Simple Factory Pattern):专门定义一个类(工厂类)来负责创建其他类的实例。可以根据创建方法的参数来返回不同类的实例,被创建的实例通常都具有共同的父类。

举例:

简单工厂模式像一个代工厂,一个工厂可以生产多种产品。举个例子,一个饮料加工厂同时帮百事可乐和可口可乐生产,加工厂根据输入参数Type来生产不同的产品。

// 可乐抽象类
@interface Cola : NSObject
@end

// 可口可乐产品类
@interface CocaCola : Cola
@end

// 百事可乐产品类
@interface PesiCola : Cola
@end
// 简单工厂实现
@implementation SimpleFactory

+ (Cola *)createColaWithType:(NSInteger)type {
    switch (type) {
        case 0:
            return [CocaCola new];
        case 1:
            return [PesiCola new];
        default:
            return nil;
            break;
    }
}
@end

// 0 生产可口可乐
Cola *cocaCola = [SimpleFactory createColaWithType:0];

// 1 生产百事可乐
Cola *pesiCola = [SimpleFactory createColaWithType:1];

优点:

  • 使用者只需要给工厂类传入一个正确的约定好的参数,就可以获取你所需要的对象,而不需要知道其创建细节,一定程度上减少系统的耦合。
  • 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,减少开发者的记忆成本。

缺点:

  • 如果业务上添加新产品的话,就需要修改工厂类原有的判断逻辑,这其实是违背了开闭原则的。
  • 在产品类型较多时,有可能造成工厂逻辑过于复杂。所以简单工厂模式比较适合产品种类比较少而且增多的概率很低的情况。

4.2 工厂方法模式

工厂方法模式(Factory Method Pattern)又称为工厂模式,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,即通过不同的工厂子类来创建不同的产品对象。

举例:

工厂方法和简单工厂有一些区别,简单工厂是由一个代工厂生产不同的产品,而工厂方法是对工厂进行抽象化,不同产品都由专门的具体工厂来生产。可口可乐工厂专门生产可口可乐,百事可乐工厂专门生产百事可乐。

// 工厂抽象类
@implementation Factory
+ (Cola *)createCola {
    return [Cola new];
}
@end

// 可口可乐工厂
@implementation CocaColaFactory

+ (Cola *)createCola {
    return [CocaCola new];
}
@end

// 百事可乐工厂
@implementation PesiColaFactory
+ (Cola *)createCola {
    return [PesiCola new];
}
@end
// 根据不同的工厂类生产不同的产品
Cola *pesiCola = [PesiColaFactory createCola];
Cola *cocaCola = [CocaColaFactory createCola];

优点:

  • 用户只需要关心其所需产品对应的具体工厂是哪一个即可,不需要关心产品的创建细节,也不需要知道具体产品类的类名。
  • 当系统中加入新产品时,不需要修改抽象工厂和抽象产品提供的接口,也无须修改客户端和其他的具体工厂和具体产品,而只要添加一个具体工厂和与其对应的具体产品就可以了,符合了开闭原则。

缺点:

  • 当系统中加入新产品时,除了需要提供新的产品类之外,还要提供与其对应的具体工厂类。因此系统中类的个数将成对增加,增加了系统的复杂度。

4.3 抽象工厂模式

抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

举例:

抽象工厂和工厂方法不同的地方在于,生产产品的工厂是抽象的。举例,可口可乐公司生产可乐的同时,也需要生产装可乐的瓶子和箱子,瓶子和箱子也是可口可乐专属定制的,同样百事可乐公司也会有这个需求。这个时候我们的工厂不仅仅是生产可乐饮料的工厂,还必须同时生产同一主题的瓶子和箱子,所以它是一个抽象的主题工厂,专门生产同一主题的不同商品。

// 可乐抽象类和派生类
@interface Cola : NSObject
@end
@interface CocaCola : Cola
@end
@interface PesiCola : Cola
@end

// 瓶子抽象类和派生类
@interface Bottle : NSObject
@end
@interface CocaColaBottle : Bottle
@end
@interface PesiColaBottle : Bottle
@end

// 箱子抽象类和派生类
@interface Box : NSObject
@end
@interface CocaColaBox : Box
@end
@interface PesiColaBox : Box
@end
// 工厂抽象类
@implementation Factory

+ (Cola *)createCola {
    return [Cola new];
}
+ (Bottle *)createBottle {
    return [Bottle new];
}
+ (Box *)createBox {
    return [Box new];
}
@end

// 可口可乐主题工厂
@implementation CocaColaFactory

+ (CocaCola *)createCola {
    return [CocaCola new];
}
+ (CocaColaBottle *)createBottle {
    return [CocaColaBottle new];
}
+ (CocaColaBox *)createBox {
    return [CocaColaBox new];
}
@end

// 百事可乐主题工厂
@implementation PesiColaFactory
+ (PesiCola *)createCola {
    return [PesiCola new];
}
+ (PesiColaBottle *)createBottle {
    return [PesiColaBottle new];
}
+ (PesiColaBox *)createBox {
    return [PesiColaBox new];
}
@end
// 可口可乐主题
Cola *cocaCola = [CocaColaFactory createCola];
Bottle *cocaColaBottle = [CocaColaFactory createBottle];
Box *cocaColaBox = [CocaColaFactory createBox];

// 百事可乐主题
Cola *pesiCola = [PesiColaFactory createCola];
Bottle *pesiColaBottle = [PesiColaFactory createBottle];
Box *pesiColaBox = [PesiColaFactory createBox];

优点:

  • 具体产品在应用层代码隔离,不需要关心产品细节。只需要知道自己需要的产品是属于哪个工厂的即可 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。

缺点:

  • 规定了所有可能被创建的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂的接口。

4.4 单例模式

单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,并提供一个访问它的全剧访问点。

举例:

单例模式下,对应类只能生成一个实例。就像一个王国只能有一个国王,一旦王国里的事务多起来,这唯一的国王也容易职责过重。

@implementation Singleton

+ (instancetype)shareInstance {
    static Singleton *shareInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shareInstance = [[Singleton alloc] init];
    });
    return shareInstance;
}

@end

优点:

  • 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
  • 因为该类在系统内存中只存在一个对象,所以可以节约系统资源。

缺点:

  • 由于单例模式中没有抽象层,因此单例类很难进行扩展。
  • 对于有垃圾回收系统的语言 Java,C# 来说,如果对象长时间不被利用,则可能会被回收。那么如果这个单例持有一些数据的话,在回收后重新实例化时就不复存在了。

4.5 生成器模式

生成器模式(Builder Pattern):也叫创建者模式,它将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

举例:

生成器模式将复杂的创建逻辑进行分割,例如生产汽车,分步骤创建安装不同的零件。如果创建逻辑简单则没有拆分的必要。

// 汽车生产器
@interface Builder : NSObject

+ (void)buildEngine;
+ (void)buildWheel;
+ (void)buildBody;

@end
// 创建过程进行拆分
Builder *builder = [Builder new];
[builder buildBody];
[builder buildWheel];
[builder buildEngine];

优点:

  • 客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
  • 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者, 用户使用不同的具体建造者即可得到不同的产品对象 。
  • 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”。
  • 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。

4.6 原型模式

原型模式(Prototype Pattern): 使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。

举例:

原型模式就像复印技术,根据原对象复印出一个新对象,并根据需求对新对象进行微调。

@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *age;
@property (nonatomic, copy) NSString *class;
@property (nonatomic, copy) NSString *school;

@end
// 原对象
Student *lily = [Student alloc] init];
lily.name = @"lily";
lily.age = @"13";
lily.class = @"五年一班";
lily.school = @"实现学校";

// 复制原对象
Student *tom = [lily copy];

// 在原对象基础上微调
tom.name = @"tom";

优点:

  • 可以利用原型模式简化对象的创建过程,尤其是对一些创建过程繁琐,包含对象层级比较多的对象来说,使用原型模式可以节约系统资源,提高对象生成的效率。
  • 可以很方便得通过改变值来生成新的对象:有些对象之间的差别可能只在于某些值的不同;用原型模式可以快速复制出新的对象并手动修改值即可。

缺点:

  • 对象包含的所有对象都需要配备一个克隆的方法,这就使得在对象层级比较多的情况下,代码量会很大,也更加复杂。

五、结构型 - 设计模式

5.1 装饰模式

装饰模式(Decorator Pattern) :不改变原有对象的前提下,动态地给一个对象增加一些额外的功能。

举例:

装饰模式贴合开闭原则,在不改变原有类的情况下,对父类进行改造或新增功能。举例,定一个抽象类Tea,只能提供白开水,但是通过装饰类BlackTea装饰之后拓展了新功能,通过BlackTea类可以用白开水泡红茶,还可以选择加柠檬。

@interface Tea : NSObject

+ (instancetype)createTea;

@end

@interface BlackTea : Tea

@property (nonatomic, strong) Tea *tea;

// 加红茶
- (void)addBlackTea;
// 红茶可以加柠檬
- (void)addLemon;

@end
@implementation Tea

+ (instancetype)createTea {
    NSLog(@"add water");
    return [self new];
}

@end

@implementation BlackTea
// 先加红茶,再加水
+ (instancetype)createTea {
    return [self new];
}

- (void)addBlackTea {
    NSLog(@"add black tea");
}

- (void)addLemon {
    NSLog(@"add lemon");
}

@end
// 茶
Tea *tea = [Tea createTea]; 
// output: add water

// 红茶
BlackTea *blackTea = [BlackTea createTea];
blackTea.tea = tea
[blackTea addBlackTea];
[blackTea addLemon];
// output: 
// add black tea 
// add lemon

优点:

  • 比继承更加灵活:不同于在编译期起作用的继承;装饰者模式可以在运行时扩展一个对象的功能。另外也可以通过配置文件在运行时选择不同的装饰器,从而实现不同的行为。也可以通过不同的组合,可以实现不同效果。
  • 符合“开闭原则”:装饰者和被装饰者可以独立变化。用户可以根据需要增加新的装饰类,在使用时再对其进行组合,原有代码无须改变。

缺点:

  • 装饰者模式需要创建一些具体装饰类,会增加系统的复杂度。

5.2 外观模式

外观模式(Facade Pattern):外观模式定义了一个高层接口,为子系统中的一组接口提供一个统一的接口。外观模式又称为门面模式,它是一种结构型设计模式模式。

举例:

外观模式提供了简单明确的接口,但是在内部众多子系统功能进行整合。就像图片缓存,内部包含了涉及到其他子系统的如缓存、下载等处理,外观模式将这些复杂的逻辑都隐藏了。在UIImageView和UIButton调用的时候,你只需要调一个setImageWithUrl:(NSString *)url接口就可以了,达到解耦合的目的。

@implementation WebImage

+ (UIImage *)getImageWithUrl:(NSString *)url {
    // 查看图片是否有缓存
    id cacheImage = [ImageCaches getImageFromCacheWithUrl:url];
    if (cacheImage) {
        return cacheImage;
    }
    
    // 下载图片
    id downloadImage = [ImageDownloader downloadImageWithUrl:url];
    if (downloadImage) {
      // 缓存图片
      [ImageCaches cacheImage:downloadImage];
      return downloadImage;
    }else{
      return nil;
    }
}
@end

@implementation UIImageView + WebImage / UIButton + WebImage

- (void)setImageWithUrl:(NSString *)url {
    UIImage webImage = [WebImage getImageWithUrl:url];
    if (webImage) {
      [self setImage:webImage];
    }
}
@end
// 使用的时候不需要关系内部缓存逻辑
UIImageView *webImage = [UIImageView new];
[webImage setImageWithUrl:@"https://imageUrl"];

UIButton *webButton = [UIButton new];
[webButton setImageWithUrl:@"https://imageUrl"];

优点:

  • 实现了客户端与子系统间的解耦:客户端无需知道子系统的接口,简化了客户端调用子系统的调用过程,使得子系统使用起来更加容易。同时便于子系统的扩展和维护。
  • 符合迪米特法则(最少知道原则):子系统只需要将需要外部调用的接口暴露给外观类即可,而且他的接口则可以隐藏起来。

缺点:

  • 违背了开闭原则:在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的代码。

5.3 代理模式

代理模式(Proxy Pattern) :为某个对象提供一个代理,并由这个代理对象控制对原对象的访问。

举例:

代理模式像一个房屋中介,买家只能通过中介来买房,代理具备被代理类的所有功能,就像房东有卖房功能,中介也具有卖房功能。此外代理实例还可以帮助被代理实例进行一些额外处理,比如中介可以帮助房东筛选优质买家的功能,帮助房东pass掉一些不符合条件的买家。还有消息队列也是该模式。

// 顾客
@interface Customer ()

@property (nonatomic, strong) Waiter *waiter;

@end

@implementation Customer

// 叫服务生
- (Waiter *)callWaiter {
    _waiter = [Waiter new];
    return _waiter;
}

// 顾客点菜
- (void)orderingFood:(NSString *)food  {
    [_waiter orderingFood:food];
}

// 顾客取消某个菜
- (void)removeFood:(NSString *)food  {
    [_waiter removeFood:food];
}

@end

// 服务生
@interface Waiter ()

@property (nonatomic, strong) NSMutableArray *cacheList;

@end

@implementation Waiter

// 记下顾客点的菜
- (void)orderingFood:(NSString *)food  {
    [self.cacheList addObject:food];
}

// 帮助顾客取消某个菜
- (void)removeFood:(NSString *)food  {
    [self.cacheList removeObject:food];
}

// 将最早点的菜推给厨师
- (void)pushToChef:(Chef *)chef {
    [chef cookFood:self.cacheList.firstObject];
    [self.cacheList removeObject:self.cacheList.firstObject];
}

@end

// 厨师
@implementation Chef

- (void)cookFood:(NSString *)food {
    NSLog(@"cook %@",food);
}

@end
// 餐厅厨师
Chef *chef = [Chef new];

// 顾客
Customer *lily = [Customer new];
// 顾客叫服务生
[lily callWaiter];
// 顾客点餐
[lily orderingFood:@"小鸡炖蘑菇"];
[lily orderingFood:@"东坡肉"];
[lily orderingFood:@"虾饺皇"];
[lily orderingFood:@"红烧大虾"];
// 顾客取消某个菜
[lily removeFood:@"东坡肉"];

// 将最早点的菜菜推给厨师去烹饪
[waiter pushToChef:chef];

优点:

  • 降低系统的耦合度:代理模式能够协调调用者和被调用者,在一定程度上降低了系 统的耦合度。
  • 不同类型的代理可以对客户端对目标对象的访问进行不同的控制:
  • 远程代理,使得客户端可以访问在远程机器上的对象,远程机器 可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
  • 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。
  • 保护代理可以控制客户端对真实对象的使用权限。

缺点:

  • 由于在客户端和被代理对象之间增加了代理对象,因此可能会让客户端请求的速度变慢。

5.4 享元模式

享元模式(Flyweight Pattern):运用共享技术复用大量细粒度的对象,降低程序内存的占用,提高程序的性能。

举例:

例如 UITableViewCell 的缓存机制,达到降低内存消耗的目的。举例,音乐服务根据收费划分出免费用户和会员用户,免费用户只能听部分免费音乐,会员用户可以听全部的音乐,并且可以下载。虽然权限上二者间有一些区别,但是他们所享受的音乐来是自于同一个音乐库,这样所有的音乐都只需要保存一份就可以了。另外如果出现音乐库里没有的音乐时,则需要新增该音乐,然后其他服务也可以享受新增的音乐,相当于享元池或缓存池的功能。

享元模式区保证共享内部状态如音乐库,而外部状态根据不同需求定制如各种访问权限,使用中不能去改变内部状态,以达到共享的目的。

// 音乐服务
@interface MusicService ()

// 共享的音乐库
@property (nonatomic, strong) NSArray *musicLibrary;

@end

@implementation MusicService
// 听音乐
- (void)listenToMusct:(NSString *)music {
    ...
}
// 下载音乐
- (void)downloadMusic:(NSString *)music {
    ...
}

@end

// 免费音乐服务
@interface FreeMusicService : NSObject

@property (nonatomic, strong) MusicService *musicSever;

- (void)listenFreeMusic:(NSString *)music;

@end

@implementation FreeMusicService

// 只能听免费音乐
- (void)listenToFreeMusic:(NSString *)music {
    if ([music isEqualToString:@"free"]) {
      // 如果是免费则播放
        [self.musicSever listenMusct:music];
    }else{
      // 如果是收费音乐,则提示用户升级 Vip
        NSLog(@"please upgrade to Vip");
    }
}

@end


// Vip 音乐服务
@interface VipMusicService : NSObject

@property (nonatomic, strong) MusicService *musicSever;

- (void)listenMusic:(NSString *)music;

- (void)downloadMusic:(NSString *)music;

@end

@implementation VipMusicService

// 可以听全部的音乐
- (void)listenToMusic:(NSString *)music {
    [self.musicSever listenMusct:music];
}

// 可以下载音乐
- (void)downloadMusic:(NSString *)music {
    [self.musicSever downloadMusic:music];
}

@end
// 新建一个基础音乐库
MusicService *musicService = [MusicService new];

// 免费服务
FreeMusicService *freeService = [FreeMusicService new];
// 收费服务
VipMusicService *vipService = [VipMusicService new];

// 共享一个音乐库
freeService.musicSever = musicService;
vipService.musicSever = musicService;

[freeService listenFreeMusic:@"免费音乐"];
[vipService listenMusic:@"全部音乐"];

优点:

  • 使用享元模可以减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份,降低系统的使用内存,也可以提性能。
  • 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。

缺点:

  • 使用享元模式需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
  • 对象在缓冲池中的复用需要考虑线程问题。

5.5 桥接模式

桥接模式(Simple Factory Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。

举例:

尽管手机都有各自的不同之处,但是他们都有一个手机卡卡槽,卡槽里可以插不同运营商的卡。不管手机和卡内部如何改变,只要卡槽的行业标准没有变,就都可以正常使用。桥接模式在于将复杂的类进行分割,优先对象组合的方式,就像将手机里的手机卡抽离出去新建一个类,实现手机实例持有一个手机卡实例的组合方式。而不是通过继承来新建多个不同手机卡的手机子类。

// 创建手机 SIM 卡协议
@protocol SIMCardProtocol <NSObject>
// 读取 SIM 卡信息接口
- (void)getSIMInfo;

@end

// SIM 卡抽象类
@interface SIMCard : NSObject
- (void)getSIMInfo;
@end

@implementation SIMCard
- (void)getSIMInfo {}
@end

// 联通 SIM 卡
@interface UnicomSIMCard : SIMCard<SIMCardProtocol>
@end

@implementation UnicomSIMCard
- (void)getSIMInfo {
    NSLog(@"Welcome Unicom User");
}
@end

// 移动 SIM 卡
@interface MobileSIMCard : SIMCard<SIMCardProtocol>
@end

@implementation MobileSIMCard
- (void)getSIMInfo {
    NSLog(@"Welcome Mobile User");
}
@end

// 手机抽象类
@interface Phone : NSObject
// 持有 SIM 卡
@property (nonatomic, strong) SIMCard *simCard;
// 启动手机方法
- (void)launchPhone;
@end

@implementation Phone
- (void)launchPhone {
    if (self.simCard) {
        [self.simCard getSIMInfo];
    }
}
@end

// iPhone
@interface iPhone : Phone
@end

// 小米手机
@interface miPhone : Phone
@end
// 联通卡
UnicomSIMCard *unicomSim = [UnicomSIMCard new];
// 移动卡
MobileSIMCard *mobileSim = [MobileSIMCard new];

// 小米手机安装上联调卡
miPhone *mi9 = [miPhone new];
[mi9 setSimCard:unicomSim];

// iPhone 安装上移动卡
iPhone *iPhoneX = [iPhone new];
[iPhoneX setSimCard:mobileSim];

// 开机
[mi9 launchPhone];
[iPhoneX launchPhone];

SIMCardProtocol 协议相当于行业标准,所以手机卡都要遵循该协议。而各个手机生产商知道该协议,就可以直接利用该协议获得 SIM 卡内部信息。

优点:

  • 扩展性好,符合开闭原则:将抽象与实现分离,让二者可以独立变化

缺点:

  • 在设计之前,需要识别出两个独立变化的维度。

5.6 适配器模式

适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。适配器模式的别名是包装器模式(Wrapper),是一种结构型设计模式。

举例:

适配器模式顾名思义,比如内地用像港版插头需要一个转接头。再比如iPhone的手机卡是特别小的 Nano 卡,把 Nano 卡拿到其他手机上不能贴合卡槽尺寸,所以我们需要加一个符合卡槽尺寸的卡套。

// 标准卡 尺寸协议
@protocol StandardSIMSizeProtocol <NSObject>
- (void)normalSize;
@end

// nano 卡尺寸协议
@protocol NanoSIMSizeProtocol <NSObject>
- (void)nanoSize;
@end

// 标准卡 遵循标准协议
@interface StandardSIMCard : SIMCard<StandardSIMSizeProtocol>
@end

@implementation StandardSIMCard
- (void)normalSize {}
@end

// nano 卡遵循 nano 协议
@interface NanoSIMCard : SIMCard<NanoSIMSizeProtocol>
@end

@implementation NanoSIMCard
- (void)nanoSize {}
@end

// Nano 卡套
@interface NanoAdapter : SIMCard<StandardSIMSizeProtocol>
@property (nonatomic, strong) NanoSIMCard *nanoSIMCard;
@end

@implementation NanoAdapter
- (void)normalSize {}
@end

// 
@interface OnePhone : Phone

- (void)setSimCard:(SIMCard *)simCard;

@end

@implementation OnePhone
- (void)setSimCard:(SIMCard *)simCard {
    [simCard normalSize];
}
@end
// 标准卡
StandardSIMCard *standardCard = [StandardSIMCard new];
// Nano 卡
NanoSIMCard *nanoCard = [NanoSIMCard new];

// 创建大卡槽手机 插入卡后会调用 normalSize() 方法
OnePhone *onePhone = [OnePhone new];

// 标准卡 遵循 StandardSIMSizeProtocol,协议 实现了 normalSize() 方法
[onePhone setSimCard:standardCard];

// Nano 遵循 NanoSIMSizeProtocol 协议,并没有实现了 normalSize() 方法,所以会报错
[onePhone setSimCard:nanoCard];

// 加一个 Nano 卡套
NanoAdapter *nanoAdapter = [NanoAdapter new];
// 卡套持有 Nano 卡实例,方便获取 Nano 卡信息
nanoAdapter.nanoSIMCard = nanoCard;
// 将卡套放入手机, 卡套遵循 StandardSIMSizeProtocol,实现了 normalSize() 方法
[onePhone setSimCard:nanoAdapter];

优点:

  • 符合开闭原则:使用适配器而不需要改变现有类,提高类的复用性。
  • 目标类和适配器类解耦,提高程序扩展性。

缺点:

  • 增加了系统的复杂性

六、行为型 - 设计模式

6.1 职责链模式

职责链模式(Chain of Responsibility Pattern):避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。

举例:

职责链模式在 iOS 中有大量的应用,比如事件响应链,事件传递下来会先判断该事件是不是应该由自己处理,如果不是由自己处理则传给下一位响应者去处理,如此循环下去。需要注意的是要避免响应链循环调用造成死循环,还有当所有的响应者都无法处理时的情况。

// 响应者
@interface Responder : NSObject

@property (nonatomic, strong) Responder *nextResponder;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSUInteger UpperLimit;
@property (nonatomic, assign) NSUInteger LowerLimit;

- (void)respondWithCode:(NSUInteger)code;

@end

@implementation Responder

- (void)respondWithCode:(NSUInteger)code {
    if (_LowerLimit <= code && code <= _UpperLimit) {
      // 在处理范围内 
        NSLog(@"code: %ld is %@", code, _name);
    }else if (_nextResponder){
      // 不在处理范围则 传递给下一位响应者
        [_nextResponder respondWithCode:code];
    }else{
      // 响应链结束
        NSLog(@"code: %ld no match responder", code);
    }
}

@end
// 建立响应链
Responder *successResponder = [Responder new];
Responder *warmingResponder = [Responder new];
Responder *httpResponder = [Responder new];
Responder *serviceResponder = [Responder new];

successResponder.nextResponder = warmingResponder;
warmingResponder.nextResponder = httpResponder;
httpResponder.nextResponder = serviceResponder;

// 设置处理范围
successResponder.LowerLimit = 200;
successResponder.UpperLimit = 299;
successResponder.name = @"success";

warmingResponder.LowerLimit = 300;
warmingResponder.UpperLimit = 399;
warmingResponder.name = @"warming";

httpResponder.LowerLimit = 400;
httpResponder.UpperLimit = 499;
httpResponder.name = @"http fail";

serviceResponder.LowerLimit = 500;
serviceResponder.UpperLimit = 599;
serviceResponder.name = @"service fail";

// 使用响应者链
[successResponder respondWithCode:200]; // code: 200 is success
[successResponder respondWithCode:310]; // code: 310 is warming
[successResponder respondWithCode:401]; // code: 401 is http fail
[successResponder respondWithCode:555]; // code: 555 is service fail
[successResponder respondWithCode:666]; // code: 666 no match responder

优点:

  • 职责链模式使得一个对象无须知道是其他哪一个对象处理其请求,对象仅需知道该请求会被处理即可,接收者和发送者都没有对方的明确信息,且链中的对象不需要知道链的结构,由客户端负责链的创建,降低了系统的耦合度。
  • 请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接。
  • 在给对象分派职责时,职责链可以给我们更多的灵活性,可以通过在运行时对该链进行动态的增加或修改来增加或改变处理一个请求的职责。
  • 在系统中增加一个新的具体请求处理者时无须修改原有系统

推荐阅读