设计模式Books Reading
文章目录
-
- 设计模式
- 创建型设计模式
-
- 工厂方法
-
- 示例说明
- 工厂方法模式结构
- 案例伪代码
- 工厂方法模式适合应用
- 实现方式
- 工厂方法模式优缺点
- 与其他模式的关系
- 概念示例
- 抽象工厂
-
- 抽象工厂模式结构
- 抽象工厂模式适合应用场景
- 实现方式
- 抽象工厂模式优缺点
- 与其他模式的关系
- 代码示例
- 生成器模式
-
- 生成器模式结构
- 生成器模式适合应用场景
- 实现方法
- 生成器模式优缺点
- 与其他模式的关系
- 概念示例
- 原型模式
-
- 原型模式结构
- 原型模式适合应用场景
- 实现方式
- 原型模式优缺点
- 与其他模式的关系
- 概念示例
- 单例模式
-
- 单例模式结构
- 单例模式适合应用场景
- 实现方式
- 单例模式优缺点
- 与其他模式的关系
- 伪代码
- 概念示例
- 结构性模式
-
- 适配器模式
-
- 适配器模式结构
- 适配器模式适合应用场景
- 实现方式
- 适配器模式优缺点
- 与其他模式的关系
- 概念示例
- 桥接模式
-
- 桥接模式结构
- 桥接模式适合应用场景
- 实现方法
- 桥接模式优缺点
- 与其他模式的关系
- 概念示例
- 组合模式
-
- 组合模式结构
- 组合模式适合应用场景
- 实现方式
- 组合模式优缺点
- 与其他模式的关系
- 概念示例
- 装饰模式
-
- 装饰模式结构
- 装饰模式适合应用场景
- 实现方法
- 装饰模式优缺点
- 与其他模式的关系
- 概念示例
- 外观模式
-
- 外观模式结构
- 外观模式适合应用场景
- 实现方式
- 外观模式优缺点
- 与其他模式的关系
- 概念示例
- 享元模式
-
- 享元模式结构
- 享元模式适合应用场景
- 实现方式
- 享元模式优缺点
- 与其他模式的关系
- 概念示例
- 代理模式
-
- 代理模式结构
- 代理模式适合应用场景
- 实现方式
- 代理模式优缺点
- 与其他模式的关系
- 概念示例
- 行为模式
-
- 责任链模式
-
- 责任链模式结构
- 责任链模式适合应用场景
- 实现方式
- 与其他模式的关系
- 代码示例
- 命令模式
-
- 命令模式结构
- 命令模式适合应用场景
- 实现方式
- 命令模式优缺点
- 与其他模式的关系
- 概念示例
- 迭代器模式
-
- 迭代器模式结构
- 迭代器模式适合应用场景
- 实现方式
- 迭代器模式优缺点
- 与其他模式的关系
- 概念示例
- 中介者模式
-
- 中介者模式结构
- 中介者模式适合应用场景
- 实现方式
- 中介者模式优缺点
- 与其他模式的关系
- 概念示例
- 备忘录模式
-
- 备忘录模式结构
- 备忘录模式适合应用场景
- 实现方式
- 备忘录模式优缺点
- 与其他模式的关系
- 概念示例
- 观察者模式
-
- 观察者模式结构
- 观察者模式适合应用场景
- 实现方式
- 观察者模式优缺点
- 与其他模式的关系
- 概念示例
- 状态模式
-
- 状态模式结构
- 状态模式适合应用场景
- 实现方式
- 状态模式优缺点
- 与其他模式的关系
- 概念示例
- 策略模式
-
- 策略模式结构
- 策略模式适合应用场景
- 实现方式
- 策略模式优缺点
- 与其他模式的关系
- 概念示例
- 模板方法模式
-
- 模板方法模式结构
- 模板方法模式适合应用场景
- 实现方式
- 模板方法模式优缺点
- 与其他模式的关系
- 概念示例
- 访问者模式
-
- 访问者模式结构
- 访问者模式适合应用场景
- 实现方式
- 访问者模式优缺点
- 与其他模式的关系
- 概念示例
- End
设计模式
为什么我们需要了解设计模式?
- **轻松应对面试和审核。**关于模式的问题总是一再出现。
- 获得你应得的加薪和升职!
- 扩展你的工具箱,在其中添加经过验证且没有问题的代码示例。
- 定制现成的解决方案。不要重新开发所有内容。
- 更好地与同事沟通,无需冗长的解释。
- 只需指出设计模式的名称,你就能轻松解决问题!
什么是设计模式?
- 设计模式是软件设计中常见问题的典型解决方案。 每个模式就像一张蓝图, 你可以通过对其进行定制来解决代码中的特定设计问题。
- 设计模式与方法或库的使用方式不同, 你很难直接在自己的程序中套用某个设计模式。 模式并不是一段特定的代码, 而是解决特定问题的一般性概念。 你可以根据模式来实现符合自己程序实际所需的解决方案。
- 人们常常会混淆模式和算法, 因为两者在概念上都是已知特定问题的典型解决方案。 但算法总是明确定义达成特定目标所需的一系列步骤, 而模式则是对解决方案的更高层次描述。 同一模式在两个不同程序中的实现代码可能会不一样。
- 算法更像是菜谱: 提供达成目标的明确步骤。 而模式更像是蓝图: 你可以看到最终的结果和模式的功能, 但需要自己确定实现步骤。
模式包含哪些内容?
大部分模式都有正规的描述方式, 以便在不同情况下使用。 模式的描述通常会包括以下部分:
- 意图部分简单描述问题和解决方案。
- 动机部分将进一步解释问题并说明模式会如何提供解决方案。
- 结构部分展示模式的每个部分和它们之间的关系。
- 在不同语言中的实现提供流行编程语言的代码, 让读者更好地理解模式背后的思想。
部分模式介绍中还列出其他的一些实用细节, 例如模式的适用性、 实现步骤以及与其他模式的关系。
为什么以及如何学习设计模式?
- 或许你已从事程序开发工作多年, 却完全不知道单例模式是什么。 很多人都是这样。 即便如此, 你可能也在不自知的情况下已经使用过一些设计模式了。 所以为什么不花些时间来更进一步学习它们呢?
- 设计模式是针对软件设计中常见问题的工具箱, 其中的工具就是各种经过实践验证的解决方案。 即使你从未遇到过这些问题, 了解模式仍然非常有用, 因为它能指导你如何使用面向对象的设计原则来解决各种问题。
- 设计模式定义了一种让你和团队成员能够更高效沟通的通用语言。 你只需说 “哦, 这里用单例就可以了”, 所有人都会理解这条建议背后的想法。 只要知晓模式及其名称, 你就无需解释什么是单例。
设计模式分类
不同设计模式的复杂程度、 细节层次以及在整个系统中的应用范围等方面各不相同。 我喜欢将其类比于道路的建造: 如果你希望让十字路口更加安全, 那么可以安装一些交通信号灯, 或者修建包含行人地下通道在内的多层互通式立交桥。
最基础的、 底层的模式通常被称为惯用技巧。 这类模式一般只能在一种编程语言中使用。
最通用的、 高层的模式是构架模式。 开发者可以在任何编程语言中使用这类模式。 与其他模式不同, 它们可用于整个应用程序的架构设计。
此外, 所有模式可以根据其意图或目的来分类。 本书覆盖了三种主要的模式类别:
- 创建型模式提供创建对象的机制, 增加已有代码的灵活性和可复用性。
- 结构型模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。
- 行为模式负责对象间的高效沟通和职责委派。
设计模式的优势
- 模式是针对软件设计中常见问题的解决方案工具箱, 它们定义了一种让你的团队能更高效沟通的通用语言。
创建型设计模式
这类模式提供创建对象的机制, 能够提升已有代码的灵活性和可复用性。
1、工厂方法模式
- 在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。
2、抽象工厂模式
- 让你能创建一系列相关的对象,而无需指定其具体类。
3、生成器模式
- 使你能够分步骤创建复杂对象。该模式允许你使用相同的创建代码生成不同类型和形式的对象。
4、原型模式
- 让你能够复制已有的对象,而又无需使代码依赖它们所属的类。
5、单例模式
- 让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。
工厂方法
Factory Method
亦称:虚拟构造函数、Virtual Constructor、Factory Method
示例说明
意图:
-
工厂方法模式是一种创建型设计模式, 其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。
-
物流 Logistics
路上物流 RoadLogistics
海上物流 SEALogistics
问题:
-
假设你正在开发一款物流管理应用。 最初版本只能处理卡车运输, 因此大部分代码都在位于名为
卡车
的类中。 -
一段时间后, 这款应用变得极受欢迎。 你每天都能收到十几次来自海运公司的请求, 希望应用能够支持海上物流功能。
-
如果代码其余部分与现有类已经存在耦合关系, 那么向程序中添加新类其实并没有那么容易。如:
- 卡车、轮船 一些功能具有耦合关系
-
这可是个好消息。 但是代码问题该如何处理呢? 目前, 大部分代码都与
卡车
类相关。 在程序中添加轮船
类需要修改全部代码。 更糟糕的是, 如果你以后需要在程序中支持另外一种运输方式, 很可能需要再次对这些代码进行大幅修改。 -
最后, 你将不得不编写繁复的代码, 根据不同的运输对象类, 在应用中进行不同的处理。
解决方案:
-
工厂方法模式建议使用特殊的工厂方法代替对于对象构造函数的直接调用 (即使用
new
运算符)。 不用担心, 对象仍将通过new
运算符创建, 只是该运算符改在工厂方法中调用罢了。 工厂方法返回的对象通常被称作 “产品”。-
子类可以修改工厂方法返回的对象类型。如:计划交付+创建运输
-
// 基类 Logistics: planDeliver()createTransport()// 继承 Logistics RoadLogistics: createTransport() return new Truck()// 继承 Logistics SEA Logistics: createTransport() return new Ship()
-
-
-
乍看之下,这种更改可能毫无意义:我们只是改变了程序中调用构造函数的位置而已。但是,仔细想一下,现在你可以在子类中重写工厂方法,从而改变其创建产品的类型。
-
但有一点需要注意:仅当这些产品具有共同的基类或者接口时,子类才能返回不同类型的产品,同时基类中的工厂方法还应将其返回类型声明为这一共有接口。
-
所有产品都必须使用同一接口。
-
// 基类 <<interface>> Transportdeliver()// 继承 Transport Truck...deliver()// 继承 Transport Ship...deliver()
-
-
-
举例来说,
卡车Truck
和轮船Ship
类都必须实现运输Transport
接口, 该接口声明了一个名为deliver
交付的方法。 每个类都将以不同的方式实现该方法:卡车走陆路交付货物, 轮船走海路交付货物。陆路运输RoadLogistics
类中的工厂方法返回卡车对象, 而海路运输SeaLogistics
类则返回轮船对象。-
只要产品类实现一个共同的接口, 你就可以将其对象传递给客户代码, 而无需提供额外数据。
-
-
调用工厂方法的代码(通常被称为客户端代码)无需了解不同子类返回实际对象之间的差别。 客户端将所有产品视为抽象的
运输
。客户端知道所有运输对象都提供交付
方法, 但是并不关心其具体实现方式。
工厂方法模式结构
-
1、产品 (Product) 将会对接口进行声明。 对于所有由创建者及其子类构建的对象, 这些接口都是通用的。
-
<<interface>> ProductdoStuff()
-
-
2、具体产品 (Concrete Products) 是产品接口的不同实现。
-
ConcreateProductAConcreateProductB
-
-
3、创建者 (Creator) 类声明返回产品对象的工厂方法。 该方法的返回对象类型必须与产品接口相匹配。
-
你可以将工厂方法声明为抽象方法, 强制要求每个子类以不同方式实现该方法。 或者, 你也可以在基础工厂方法中返回默认产品类型。
-
注意, 尽管它的名字是创建者, 但它最主要的职责并不是创建产品。 一般来说, 创建者类包含一些与产品相关的核心业务逻辑。 工厂方法将这些逻辑处理从具体产品类中分离出来。 打个比方, 大型软件开发公司拥有程序员培训部门。 但是, 这些公司的主要工作还是编写代码, 而非生产程序员。
-
Product p = createProduct() p.doStuff()creator...someOperation()createProduction():Product
-
-
4、具体创建者 (Concrete Creators) 将会重写基础工厂方法, 使其返回不同类型的产品。
-
注意, 并不一定每次调用工厂方法都会创建新的实例。 工厂方法也可以返回缓存、 对象池或其他来源的已有对象。
-
ConcreateCreatorA...createProduct(): Productreturn new ConcreateCreatorA()ConcreateCreatorA...createProduct(): Product
-
案例伪代码
以下示例演示了如何使用工厂方法开发跨平台 UI (用户界面) 组件, 并同时避免客户代码与具体 UI 类之间的耦合。
-
### 执行 Button okButton = createButton() okButton.onClick(closeDialog) okButton.render()<<interface>> Buttonreader()onClick()## WindowsButton ## HTMLButtonDialog...render()createButton(): ButtonWindowsDialog...createButton(): Buttonreturn new WindowsButton()WebDialog...createButton(): Button
工厂方法模式适合应用
当你在编写代码的过程中, 如果无法预知对象确切类别及其依赖关系时, 可使用工厂方法。
- 工厂方法将创建产品的代码与实际使用产品的代码分离, 从而能在不影响其他代码的情况下扩展产品创建部分代码。
- 例如, 如果需要向应用中添加一种新产品, 你只需要开发新的创建者子类, 然后重写其工厂方法即可。
如果你希望用户能扩展你软件库或框架的内部组件, 可使用工厂方法。
- 继承可能是扩展软件库或框架默认行为的最简单方法。 但是当你使用子类替代标准组件时, 框架如何辨识出该子类?
- 解决方案是将各框架中构造组件的代码集中到单个工厂方法中, 并在继承该组件之外允许任何人对该方法进行重写。
- 让我们看看具体是如何实现的。 假设你使用开源 UI 框架编写自己的应用。 你希望在应用中使用圆形按钮, 但是原框架仅支持矩形按钮。 你可以使用
圆形按钮
RoundButton子类来继承标准的按钮
Button类。 但是, 你需要告诉UI框架
UIFramework类使用新的子类按钮代替默认按钮。 - 为了实现这个功能, 你可以根据基础框架类开发子类
圆形按钮 UI
UIWithRoundButtons , 并且重写其createButton
创建按钮方法。 基类中的该方法返回按钮
对象, 而你开发的子类返回圆形按钮
对象。 现在, 你就可以使用圆形按钮 UI
类代替UI框架
类。 就是这么简单!
如果你希望复用现有对象来节省系统资源, 而不是每次都重新创建对象, 可使用工厂方法。
- 在处理大型资源密集型对象 (比如数据库连接、 文件系统和网络资源) 时, 你会经常碰到这种资源需求。
- 让我们思考复用现有对象的方法:
- 首先, 你需要创建存储空间来存放所有已经创建的对象。
- 当他人请求一个对象时, 程序将在对象池中搜索可用对象。
- … 然后将其返回给客户端代码。
- 如果没有可用对象, 程序则创建一个新对象 (并将其添加到对象池中)。
- 这些代码可不少! 而且它们必须位于同一处, 这样才能确保重复代码不会污染程序。
- 可能最显而易见, 也是最方便的方式, 就是将这些代码放置在我们试图重用的对象类的构造函数中。 但是从定义上来讲, 构造函数始终返回的是新对象, 其无法返回现有实例。
- 因此, 你需要有一个既能够创建新对象, 又可以重用现有对象的普通方法。 这听上去和工厂方法非常相像。
- 让我们思考复用现有对象的方法:
实现方式
-
让所有产品都遵循同一接口。 该接口必须声明对所有产品都有意义的方法。
-
在创建类中添加一个空的工厂方法。 该方法的返回类型必须遵循通用的产品接口。
-
在创建者代码中找到对于产品构造函数的所有引用。 将它们依次替换为对于工厂方法的调用, 同时将创建产品的代码移入工厂方法。
你可能需要在工厂方法中添加临时参数来控制返回的产品类型。
工厂方法的代码看上去可能非常糟糕。 其中可能会有复杂的
switch
分支运算符, 用于选择各种需要实例化的产品类。 但是不要担心, 我们很快就会修复这个问题。 -
现在, 为工厂方法中的每种产品编写一个创建者子类, 然后在子类中重写工厂方法, 并将基本方法中的相关创建代码移动到工厂方法中。
-
如果应用中的产品类型太多, 那么为每个产品创建子类并无太大必要, 这时你也可以在子类中复用基类中的控制参数。
例如, 设想你有以下一些层次结构的类。 基类
邮件
及其子类航空邮件
和陆路邮件
;运输
及其子类飞机
,卡车
和火车
。航空邮件
仅使用飞机
对象, 而陆路邮件
则会同时使用卡车
和火车
对象。 你可以编写一个新的子类 (例如火车邮件
) 来处理这两种情况, 但是还有其他可选的方案。 客户端代码可以给陆路邮件
类传递一个参数, 用于控制其希望获得的产品。 -
如果代码经过上述移动后, 基础工厂方法中已经没有任何代码, 你可以将其转变为抽象类。 如果基础工厂方法中还有其他语句, 你可以将其设置为该方法的默认行为。
工厂方法模式优缺点
优点:
- 你可以避免创建者和具体产品之间的紧密耦合。
- 单一职责原则。你可以将产品创建代码放在程序的单一位置, 从而使得代码更容易维护。
- 开闭原则。无需更改现有客户端代码, 你就可以在程序中引入新的产品类型。
缺点:
- 应用工厂方法模式需要引入许多新的子类,代码可能会因此变得更复杂。 最好的情况是将该模式引入创建者类的现有层次结构中。
与其他模式的关系
- 在许多设计工作的初期都会使用工厂方法模式 (较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式、 原型模式或生成器模式 (更灵活但更加复杂)。
- 抽象工厂模式通常基于一组工厂方法, 但你也可以使用原型模式来生成这些类的方法。
- 你可以同时使用工厂方法和迭代器模式来让子类集合返回不同类型的迭代器, 并使得迭代器与集合相匹配。
- 原型并不基于继承, 因此没有继承的缺点。 另一方面, 原型需要对被复制对象进行复杂的初始化。 工厂方法基于继承, 但是它不需要初始化步骤。
- 工厂方法是模板方法模式的一种特殊形式。 同时, 工厂方法可以作为一个大型模板方法中的一个步骤。
概念示例
使用示例: 工厂方法模式在 C++ 代码中得到了广泛使用。 当你需要在代码中提供高层次的灵活性时, 该模式会非常实用。
识别方法: 工厂方法可通过构建方法来识别, 它会创建具体类的对象, 但以抽象类型或接口的形式返回这些对象。
本例说明了工厂方法设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
/*** Product 接口声明了所有具体产品必须实现的操作。*/class Product {public:virtual ~Product() {}virtual std::string Operation() const = 0;
};/*** 具体产品提供了产品接口的各种实现。*/
class ConcreteProduct1 : public Product {public:std::string Operation() const override {return "{Result of the ConcreteProduct1}";}
};
class ConcreteProduct2 : public Product {public:std::string Operation() const override {return "{Result of the ConcreteProduct2}";}
};/*** Creator 类声明了一个工厂方法,该方法应该返回一个 Product 类的对象。* Creator 的子类通常提供该方法的实现。*/class Creator {/*** Note that the Creator may also provide some default implementation of the* factory method.*/public:virtual ~Creator(){};virtual Product* FactoryMethod() const = 0;/*** 另请注意,尽管名称如此,Creator 的主要职责并非创建产品。*通常,它包含一些依赖于工厂方法返回的 Product 对象的核心业务逻辑。*子类可以通过重写工厂方法并返回不同类型的产品来间接更改该业务逻辑。*/std::string SomeOperation() const {// Call the factory method to create a Product object.Product* product = this->FactoryMethod();// Now, use the product.std::string result = "Creator: The same creator's code has just worked with " + product->Operation();delete product;return result;}
};/**
*具体创建者会重写工厂方法来更改最终产品的类型。
*/
class ConcreteCreator1 : public Creator {/*** 请注意,即使该方法实际返回的是具体产品,该方法的签名仍然使用抽象产品类型。* 这样,Creator 就可以独立于具体产品类。*/public:Product* FactoryMethod() const override {return new ConcreteProduct1();}
};class ConcreteCreator2 : public Creator {public:Product* FactoryMethod() const override {return new ConcreteProduct2();}
};/**
*客户端代码可以通过其基接口与具体创建者的实例交互。
*只要客户端继续通过基接口与创建者交互,就可以将任何创建者的子类传递给它。
*/
void ClientCode(const Creator& creator) {// ...std::cout << "Client: I'm not aware of the creator's class, but it still works.\n"<< creator.SomeOperation() << std::endl;// ...
}/*** 应用程序根据配置或环境选择创建者的类型。*/int main() {std::cout << "App: Launched with the ConcreteCreator1.\n";Creator* creator = new ConcreteCreator1();ClientCode(*creator);std::cout << std::endl;std::cout << "App: Launched with the ConcreteCreator2.\n";Creator* creator2 = new ConcreteCreator2();ClientCode(*creator2);delete creator;delete creator2;return 0;
}
抽象工厂
亦称:Abstract Factory
意图:
-
抽象工厂模式是一种创建型设计模式, 它能创建一系列相关的对象, 而无需指定其具体类。
-
抽象工厂
球形工厂
金字塔工厂
问题:
-
假设你正在开发一款家具商店模拟器。 你的代码中包括一些类, 用于表示:
- 一系列相关产品, 例如
椅子Chair
、沙发ofa
和咖啡桌CoffeeTable
。 - 系列产品的不同变体。 例如, 你可以使用
现代Modern
、维多利亚Victorian
、装饰风艺术ArtDeco
等风格生成椅子
、沙发
和咖啡桌
。
- 一系列相关产品, 例如
-
你需要设法单独生成每件家具对象, 这样才能确保其风格一致。 如果顾客收到的家具风格不一样, 他们可不会开心。
-
如:
- 你好,我上周订购了一些椅子,现在我还需要一款沙发
- 嗯… 看上去有些不对劲
- 现代风格的沙发和维多利亚风格的椅子不搭。
-
-
此外, 你也不希望在添加新产品或新风格时修改已有代码。 家具供应商对于产品目录的更新非常频繁, 你不会想在每次更新时都去修改核心代码的。
解决方案
-
首先, 抽象工厂模式建议为系列中的每件产品明确声明接口 (例如椅子、 沙发或咖啡桌)。 然后, 确保所有产品变体都继承这些接口。 例如, 所有风格的椅子都实现
椅子
接口; 所有风格的咖啡桌都实现咖啡桌
接口, 以此类推。-
每个产品申明接口
同一对象的所有变体都必须放置在同一个类层次结构之中。如:
-
<<interface>> ChairhasLegs()sitOn()VictorianChairhasLegs()sitOn()ModernChairhasLegs()sitOn()
-
-
-
接下来, 我们需要声明抽象工厂——包含系列中所有产品构造方法的接口。 例如
createChair创建椅子
、createSofa创建沙发
和createCoffeeTable 创建咖啡桌
。这些方法必须返回抽象产品类型, 即我们之前抽取的那些接口:椅子
,沙发
和咖啡桌
等等。-
每个具体工厂类都对应一个特定的产品变体。如:
-
<<interface>> furnitureFactorycreateChair(): ChaircreateCoffeeTable(): CoffeeTablecreateSofa(): SofaVictorianFurnitureFactorycreateChair(): ChaircreateCoffeeTable(): CoffeeTablecreateSofa(): SofaModernFurnitureFactorycreateChair(): ChaircreateCoffeeTable(): CoffeeTablecreateSofa(): Sofa
-
-
-
那么该如何处理产品变体呢? 对于系列产品的每个变体, 我们都将基于
抽象工厂
接口创建不同的工厂类。- 每个工厂类都只能返回特定类别的产品, 例如,现代家具工厂
ModernFurnitureFactory
只能创建 现代椅子ModernChair
、现代沙发ModernSofa和
现代咖啡桌ModernCoffeeTable
对象。 - 客户端无需了解其所调用工厂的具体类信息。
- 每个工厂类都只能返回特定类别的产品, 例如,现代家具工厂
-
假设客户端想要工厂创建一把椅子。 客户端无需了解工厂类, 也不用管工厂类创建出的椅子类型。 无论是现代风格, 还是维多利亚风格的椅子, 对于客户端来说没有分别, 它只需调用抽象
椅子
接口就可以了。 这样一来, 客户端只需知道椅子以某种方式实现了sitOn
坐下方法就足够了。 此外, 无论工厂返回的是何种椅子变体, 它都会和由同一工厂对象创建的沙发或咖啡桌风格一致。 -
最后一点说明: 如果客户端仅接触抽象接口, 那么谁来创建实际的工厂对象呢? 一般情况下, 应用程序会在初始化阶段创建具体工厂对象。 而在此之前, 应用程序必须根据配置文件或环境设定选择工厂类别。
抽象工厂模式结构
-
1、抽象产品(Abstract Product) 为构成系列产品的一组不同但相关的产品声明接口。
-
AbstractProductAAbstractProductB
-
-
2、具体产品(Concrete Product) 是抽象产品的多种不同类型实现。 所有变体(维多利亚/现代) 都必须实现相应的抽象产品(椅子/沙发)。
-
ConcreateProductA1 ConcreateProductA2ConcreateProductB1 ConcreateProductB2
-
-
3、抽象工厂 (Abstract Factory) 接口声明了一组创建各种抽象产品的方法。
-
<<interface>> AbstractFactory...createProductA(): ProductAcreateProductB(): ProductB
-
-
4、具体工厂 (Concrete Factory) 实现抽象工厂的构建方法。 每个具体工厂都对应特定产品变体, 且仅创建此种产品变体。
-
ConcreateFactory1...createProductA(): ProductAcreateProductB(): ProductBConcreateFactory2...createProductA(): ProductAcreateProductB(): ProductB
-
-
5、尽管具体工厂会对具体产品进行初始化, 其构建方法签名必须返回相应的抽象产品。 这样, 使用工厂类的客户端代码就不会与工厂创建的特定产品变体耦合。客户端**(Client) 只需通过抽象接口调用工厂和产品对象, 就能与任何具体工厂/产品变体交互。
-
Clientfactory: AbstractFactory //具体产品的抽象+Client(f:AbstractFactory)+someOperation()
-
抽象工厂模式适合应用场景
- 如果代码需要与多个不同系列的相关产品交互, 但是由于无法提前获取相关信息, 或者出于对未来扩展性的考虑, 你不希望代码基于产品的具体类进行构建, 在这种情况下, 你可以使用抽象工厂。
- 抽象工厂为你提供了一个接口, 可用于创建每个系列产品的对象。 只要代码通过该接口创建对象, 那么你就不会生成与应用程序已生成的产品类型不一致的产品。
- 如果你有一个基于一组抽象方法的类, 且其主要功能因此变得不明确, 那么在这种情况下可以考虑使用抽象工厂模式。
- 在设计良好的程序中, 每个类仅负责一件事。 如果一个类与多种类型产品交互, 就可以考虑将工厂方法抽取到独立的工厂类或具备完整功能的抽象工厂类中。
实现方式
- 以不同的产品类型与产品变体为维度绘制矩阵。
- 为所有产品声明抽象产品接口。 然后让所有具体产品类实现这些接口。
- 声明抽象工厂接口, 并且在接口中为所有抽象产品提供一组构建方法。
- 为每种产品变体实现一个具体工厂类。
- 在应用程序中开发初始化代码。 该代码根据应用程序配置或当前环境, 对特定具体工厂类进行初始化。 然后将该工厂对象传递给所有需要创建产品的类。
- 找出代码中所有对产品构造函数的直接调用, 将其替换为对工厂对象中相应构建方法的调用。
抽象工厂模式优缺点
优点:
- 你可以确保同一工厂生成的产品相互匹配。
- 你可以避免客户端和具体产品代码的耦合。
- 单一职责原则。 你可以将产品生成代码抽取到同一位置, 使得代码易于维护。
- 开闭原则。 向应用程序中引入新产品变体时, 你无需修改客户端代码。
缺点:
- 由于采用该模式需要向应用中引入众多接口和类, 代码可能会比之前更加复杂。
与其他模式的关系
- 在许多设计工作的初期都会使用工厂方法模式 (较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式、 原型模式或生成器模式 (更灵活但更加复杂)。
- 生成器重点关注如何分步生成复杂对象。 抽象工厂专门用于生产一系列相关对象。 抽象工厂会马上返回产品, 生成器则允许你在获取产品前执行一些额外构造步骤。
- 抽象工厂模式通常基于一组工厂方法, 但你也可以使用原型模式来生成这些类的方法。
- 当只需对客户端代码隐藏子系统创建对象的方式时, 你可以使用抽象工厂来代替外观模式。
- 你可以将抽象工厂和桥接模式搭配使用。 如果由桥接定义的抽象只能与特定实现合作, 这一模式搭配就非常有用。 在这种情况下, 抽象工厂可以对这些关系进行封装, 并且对客户端代码隐藏其复杂性。
- 抽象工厂、 生成器和原型都可以用单例模式来实现。
代码示例
使用示例: 抽象工厂模式在 C++ 代码中很常见。 许多框架和程序库会将它作为扩展和自定义其标准组件的一种方式。
识别方法: 我们可以通过方法来识别该模式——其会返回一个工厂对象。 接下来, 工厂将被用于创建特定的子组件。
本例说明了抽象工厂设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
/*** 产品系列中的每个不同产品都应具有一个基类接口。该产品的所有变体都必须实现此接口。*/
class AbstractProductA {public:virtual ~AbstractProductA(){};virtual std::string UsefulFunctionA() const = 0;
};/*** 具体产品是由相应的具体工厂创建的。*/
class ConcreteProductA1 : public AbstractProductA {public:std::string UsefulFunctionA() const override {return "The result of the product A1.";}
};class ConcreteProductA2 : public AbstractProductA {std::string UsefulFunctionA() const override {return "The result of the product A2.";}
};/*** 这是另一个产品的基础接口。所有产品都可以相互交互,但只有同一种具体变体的产品之间才能进行正常的交互。*/
class AbstractProductB {/*** 产品 B 能够做自己的事情......*/public:virtual ~AbstractProductB(){};virtual std::string UsefulFunctionB() const = 0;/*** ...但它也可以与 ProductA 协作。 * * 抽象工厂确保其创建的所有产品都属于同一变体,因此兼容。*/virtual std::string AnotherUsefulFunctionB(const AbstractProductA &collaborator) const = 0;
};/*** 具体产品是由相应的具体工厂创建的。*/
class ConcreteProductB1 : public AbstractProductB {public:std::string UsefulFunctionB() const override {return "The result of the product B1.";}/*** 变体 Product B1 只能与变体 Product A1 正确配合使用。尽管如此,它仍然接受 AbstractProductA 的任何实例作为参数。*/std::string AnotherUsefulFunctionB(const AbstractProductA &collaborator) const override {const std::string result = collaborator.UsefulFunctionA();return "The result of the B1 collaborating with ( " + result + " )";}
};class ConcreteProductB2 : public AbstractProductB {public:std::string UsefulFunctionB() const override {return "The result of the product B2.";}/*** 变体 Product B2 只能与变体 Product A2 正确配合使用。尽管如此,它仍然接受 AbstractProductA 的任何实例作为参数。*/std::string AnotherUsefulFunctionB(const AbstractProductA &collaborator) const override {const std::string result = collaborator.UsefulFunctionA();return "The result of the B2 collaborating with ( " + result + " )";}
};/*** 抽象工厂接口声明了一组返回不同抽象产品的方法。这些产品被称为一个系列,它们通过一个高级主题或概念相互关联。同一系列的产品通常能够相互协作。一个产品系列可能有多个变体,但其中一个变体的产品与另一个变体的产品不兼容。*/
class AbstractFactory {public:virtual AbstractProductA *CreateProductA() const = 0;virtual AbstractProductB *CreateProductB() const = 0;
};/*** 具体工厂会生产属于同一变体的一系列产品。工厂保证生成的产品兼容。注意:具体工厂方法的签名返回的是抽象产品,而方法内部则会实例化一个具体产品。*/
class ConcreteFactory1 : public AbstractFactory {public:AbstractProductA *CreateProductA() const override {return new ConcreteProductA1();}AbstractProductB *CreateProductB() const override {return new ConcreteProductB1();}
};/*** 每个具体工厂都有一个对应的产品变体。*/
class ConcreteFactory2 : public AbstractFactory {public:AbstractProductA *CreateProductA() const override {return new ConcreteProductA2();}AbstractProductB *CreateProductB() const override {return new ConcreteProductB2();}
};/*** 客户端代码仅通过抽象类型:AbstractFactory和AbstractProduct与工厂和产品交互。这使得您可以将任何工厂或产品子类传递给客户端代码而不会破坏它。*/void ClientCode(const AbstractFactory &factory) {const AbstractProductA *product_a = factory.CreateProductA();const AbstractProductB *product_b = factory.CreateProductB();std::cout << product_b->UsefulFunctionB() << "\n";std::cout << product_b->AnotherUsefulFunctionB(*product_a) << "\n";delete product_a;delete product_b;
}int main() {std::cout << "Client: Testing client code with the first factory type:\n";ConcreteFactory1 *f1 = new ConcreteFactory1();ClientCode(*f1);delete f1;std::cout << std::endl;std::cout << "Client: Testing the same client code with the second factory type:\n";ConcreteFactory2 *f2 = new ConcreteFactory2();ClientCode(*f2);delete f2;return 0;
}
生成器模式
**亦称:**建造者模式、Builder
意图:
- 生成器模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。
问题:
-
假设有这样一个复杂对象, 在对其进行构造时需要对诸多成员变量和嵌套对象进行繁复的初始化工作。 这些初始化代码通常深藏于一个包含众多参数且让人基本看不懂的构造函数中; 甚至还有更糟糕的情况, 那就是这些代码散落在客户端代码的多个位置。
-
如果为每种可能的对象都创建一个子类, 这可能会导致程序变得过于复杂。
-
HouseHouseWithGarageHouseWithSwimmingPoolHouseWithFancyStatues...
-
-
-
例如, 我们来思考如何创建一个
房屋
House对象。 建造一栋简单的房屋, 首先你需要建造四面墙和地板, 安装房门和一套窗户, 然后再建造一个屋顶。 但是如果你想要一栋更宽敞更明亮的房屋, 还要有院子和其他设施 (例如暖气、 排水和供电设备), 那又该怎么办呢? -
最简单的方法是扩展
房屋
基类, 然后创建一系列涵盖所有参数组合的子类。 但最终你将面对相当数量的子类。 任何新增的参数 (例如门廊类型) 都会让这个层次结构更加复杂。 -
另一种方法则无需生成子类。 你可以在
房屋
基类中创建一个包括所有可能参数的超级构造函数, 并用它来控制房屋对象。 这种方法确实可以避免生成子类, 但它却会造成另外一个问题。-
拥有大量输入参数的构造函数也有缺陷: 这些参数也不是每次都要全部用上的。
-
House...House(windows,doors,rooms,hasGarage,hasSwimPool,hasStatues,hasGarden,...)// 使用时 new House(4,2,4,true,null,null,null,...)new House(4,2,4,true,true,true,true,...)
-
-
-
通常情况下, 绝大部分的参数都没有使用, 这使得对于构造函数的调用十分不简洁。 例如, 只有很少的房子有游泳池, 因此与游泳池相关的参数十之八九是毫无用处的。
解决方案
-
生成器模式建议将对象构造代码从产品类中抽取出来, 并将其放在一个名为生成器的独立对象中。
-
生成器模式建议将对象构造代码从产品类中抽取出来, 并将其放在一个名为生成器的独立对象中。
HouseBuilderbuildWarlls()buildDoors()buildWindows()buildRoof()buildGarage()getResult(): House
-
-
该模式会将对象构造过程划分为一组步骤, 比如
buildWalls
创建墙壁和buildDoor
创建房门创建房门等。 每次创建对象时, 你都需要通过生成器对象执行一系列步骤。 重点在于你无需调用所有步骤, 而只需调用创建特定对象配置所需的那些步骤即可。 -
当你需要创建不同形式的产品时, 其中的一些构造步骤可能需要不同的实现。 例如, 木屋的房门可能需要使用木头制造, 而城堡的房门则必须使用石头制造。
-
在这种情况下, 你可以创建多个不同的生成器, 用不同方式实现一组相同的创建步骤。 然后你就可以在创建过程中使用这些生成器 (例如按顺序调用多个构造步骤) 来生成不同类型的对象。
-
不同生成器以不同方式执行相同的任务。
- 根据 实际需求 进行生成。
-
-
例如, 假设第一个建造者使用木头和玻璃制造房屋, 第二个建造者使用石头和钢铁, 而第三个建造者使用黄金和钻石。 在调用同一组步骤后, 第一个建造者会给你一栋普通房屋, 第二个会给你一座小城堡, 而第三个则会给你一座宫殿。 但是, 只有在调用构造步骤的客户端代码可以通过通用接口与建造者进行交互时, 这样的调用才能返回需要的房屋。
主管
-
你可以进一步将用于创建产品的一系列生成器步骤调用抽取成为单独的主管类。 主管类可定义创建步骤的执行顺序, 而生成器则提供这些步骤的实现。
-
主管知道需要哪些创建步骤才能获得可正常使用的产品。
-
-
严格来说, 你的程序中并不一定需要主管类。 客户端代码可直接以特定顺序调用创建步骤。 不过, 主管类中非常适合放入各种例行构造流程, 以便在程序中反复使用。
-
此外, 对于客户端代码来说, 主管类完全隐藏了产品构造细节。 客户端只需要将一个生成器与主管类关联, 然后使用主管类来构造产品, 就能从生成器处获得构造结果了。
生成器模式结构
-
1、生成器(Builder) 接口声明在所有类型生成器中通用的产品构造步骤。
-
<<interface>> Builderreset()buildStepA()buildStepB()buildStepC()
-
-
2、具体生成器(Concrete Builders) 提供构造过程的不同实现。 具体生成器也可以构造不遵循通用接口的产品。
-
ConcreteBuilder1result: Product1buildStepA()buildStepB()buildStepC()getResult(): Product1ConcreteBuilder2result: Product2buildStepA()buildStepB()buildStepC()getResult(): Product2
-
-
3、产品(Products) 是最终生成的对象。 由不同生成器构造的产品无需属于同一类层次结构或接口。
-
Product1Product2
-
-
4、主管(Director) 类定义调用构造步骤的顺序, 这样你就可以创建和复用特定的产品配置。
-
Director-builder: Builder+Director(builder)+changeBuilder(builder)+make(type)builder.reset()if(type == "simple"){builder.buildStepA()} else {builder.buildStepB()builder.buildStepC()}
-
-
5、客户端(Client) 必须将某个生成器对象与主管类关联。 一般情况下, 你只需通过主管类构造函数的参数进行一次性关联即可。 此后主管类就能使用生成器对象完成后续所有的构造任务。 但在客户端将生成器对象传递给主管类制造方法时还有另一种方式。 在这种情况下, 你在使用主管类生产产品时每次都可以使用不同的生成器。
生成器模式适合应用场景
-
使用生成器模式可避免 “重叠构造函数 (telescoping constructor)” 的出现。
-
假设你的构造函数中有十个可选参数, 那么调用该函数会非常不方便; 因此, 你需要重载这个构造函数, 新建几个只有较少参数的简化版。 但这些构造函数仍需调用主构造函数, 传递一些默认数值来替代省略掉的参数。
-
class Pizza {Pizza(int size) { …… }Pizza(int size, boolean cheese) { …… }Pizza(int size, boolean cheese, boolean pepperoni) { …… }// ……
-
只有在 C# 或 Java 等支持方法重载的编程语言中才能写出如此复杂的构造函数。
-
生成器模式让你可以分步骤生成对象, 而且允许你仅使用必须的步骤。 应用该模式后, 你再也不需要将几十个参数塞进构造函数里了。
-
-
当你希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时, 可使用生成器模式。
- 如果你需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异, 此时可使用生成器模式。
- 基本生成器接口中定义了所有可能的制造步骤, 具体生成器将实现这些步骤来制造特定形式的产品。 同时, 主管类将负责管理制造步骤的顺序。
-
使用生成器构造组合树或其他复杂对象。
-
生成器模式让你能分步骤构造产品。 你可以延迟执行某些步骤而不会影响最终产品。 你甚至可以递归调用这些步骤, 这在创建对象树时非常方便。
-
生成器在执行制造步骤时, 不能对外发布未完成的产品。 这可以避免客户端代码获取到不完整结果对象的情况。
实现方法
-
清晰地定义通用步骤, 确保它们可以制造所有形式的产品。 否则你将无法进一步实施该模式。
-
在基本生成器接口中声明这些步骤。
-
为每个形式的产品创建具体生成器类, 并实现其构造步骤。
不要忘记实现获取构造结果对象的方法。 你不能在生成器接口中声明该方法, 因为不同生成器构造的产品可能没有公共接口, 因此你就不知道该方法返回的对象类型。 但是, 如果所有产品都位于单一类层次中, 你就可以安全地在基本接口中添加获取生成对象的方法。
-
考虑创建主管类。 它可以使用同一生成器对象来封装多种构造产品的方式。
-
客户端代码会同时创建生成器和主管对象。 构造开始前, 客户端必须将生成器对象传递给主管对象。 通常情况下, 客户端只需调用主管类构造函数一次即可。 主管类使用生成器对象完成后续所有制造任务。 还有另一种方式, 那就是客户端可以将生成器对象直接传递给主管类的制造方法。
-
只有在所有产品都遵循相同接口的情况下, 构造结果可以直接通过主管类获取。 否则, 客户端应当通过生成器获取构造结果。
生成器模式优缺点
优点:
- 你可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。
- 生成不同形式的产品时, 你可以复用相同的制造代码。
- 单一职责原则。 你可以将复杂构造代码从产品的业务逻辑中分离出来。
缺点:
- 由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加。
与其他模式的关系
- 在许多设计工作的初期都会使用工厂方法模式 (较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式、 原型模式或生成器模式 (更灵活但更加复杂)。
- 生成器重点关注如何分步生成复杂对象。 抽象工厂专门用于生产一系列相关对象。 抽象工厂会马上返回产品, 生成器则允许你在获取产品前执行一些额外构造步骤。
- 你可以在创建复杂组合模式树时使用生成器, 因为这可使其构造步骤以递归的方式运行。
- 你可以结合使用生成器和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。
- 抽象工厂、 生成器和原型都可以用单例模式来实现。
概念示例
使用示例: 生成器模式是 C++ 世界中的一个著名模式。 当你需要创建一个可能有许多配置选项的对象时, 该模式会特别有用。
识别方法: 生成器模式可以通过类来识别, 它拥有一个构建方法和多个配置结果对象的方法。 生成器方法通常支持方法链 (例如
someBuilder->setValueA(1)->setValueB(2)->create()
)。
本例说明了生成器设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
/*** 只有当你的产品非常复杂且需要大量配置时,使用建造者模式才有意义。
*
* 与其他创建型模式不同,不同的具体建造者可以生成不相关的产品。
换句话说,不同建造者的结果可能并不总是遵循相同的接口。*/class Product1{public:std::vector<std::string> parts_;void ListParts()const{std::cout << "Product parts: ";for (size_t i=0;i<parts_.size();i++){if(parts_[i]== parts_.back()){std::cout << parts_[i];}else{std::cout << parts_[i] << ", ";}}std::cout << "\n\n"; }
};/*** Builder 接口指定了创建 Product 对象不同部分的方法。*/
class Builder{public:virtual ~Builder(){}virtual void ProducePartA() const =0;virtual void ProducePartB() const =0;virtual void ProducePartC() const =0;
};
/*** 具体建造者类遵循建造者接口,并提供建造步骤的具体实现。您的程序可能包含多个不同的建造者变体,且实现方式也不同。*/
class ConcreteBuilder1 : public Builder{private:Product1* product;/*** 新的构建器实例应包含一个空白产品对象,用于进一步组装。*/public:ConcreteBuilder1(){this->Reset();}~ConcreteBuilder1(){delete product;}void Reset(){this->product= new Product1();}/*** 所有生产步骤均使用相同的产品实例。*/void ProducePartA()const override{this->product->parts_.push_back("PartA1");}void ProducePartB()const override{this->product->parts_.push_back("PartB1");}void ProducePartC()const override{this->product->parts_.push_back("PartC1");}/*** 具体构建器应该提供自己的方法来检索结果。这是因为不同类型的构建器可能会创建完全不同的、不遵循相同接口的产品。因此,此类方法不能在基础构建器接口中声明(至少在静态类型编程语言中是这样)。请注意,PHP 是动态类型语言,此方法可以存在于基础接口中。但为了清晰起见,我们不会在那里声明它。* 通常,在将最终结果返回给客户端后,构建器实例应该准备好开始生成另一个产品。因此,通常的做法是在 `getProduct` 方法主体的末尾调用 reset 方法。但是,此行为并非强制性的,您可以让构建器等待来自客户端代码的显式 reset 调用,然后再处理先前的结果。*//*** 请注意内存所有权。一旦调用 GetProduct,此函数的用户就有责任释放这块内存。使用智能指针来避免内存泄漏可能是一个更好的选择。*/Product1* GetProduct() {Product1* result= this->product;this->Reset();return result;}
};/*** Director只负责按照特定的顺序执行构建步骤。当按照特定的顺序或配置生产产品时,这很有帮助。严格来说,Director类是可选的,因为客户端可以直接控制构建器。*/
class Director{/*** @var Builder*/private:Builder* builder;/*** Director 可以与客户端代码传递给它的任何构建器实例协同工作。这样,客户端代码可能会改变新组装产品的最终类型。 **/public:void set_builder(Builder* builder){this->builder=builder;}/*** 总监可以使用相同的构建步骤构建多个产品变体。*/void BuildMinimalViableProduct(){this->builder->ProducePartA();}void BuildFullFeaturedProduct(){this->builder->ProducePartA();this->builder->ProducePartB();this->builder->ProducePartC();}
};
/*** 客户端代码创建一个构建器对象,将其传递给主管,然后 启动构造过程。最终结果将从构建器对象中检索。*/
/*** 为了简单起见,我使用了原始指针,但您可能更喜欢在这里使用智能指针。*/
void ClientCode(Director& director)
{ConcreteBuilder1* builder = new ConcreteBuilder1();director.set_builder(builder);std::cout << "Standard basic product:\n"; director.BuildMinimalViableProduct();Product1* p= builder->GetProduct();p->ListParts();delete p;std::cout << "Standard full featured product:\n"; director.BuildFullFeaturedProduct();p= builder->GetProduct();p->ListParts();delete p;// Remember, the Builder pattern can be used without a Director class.std::cout << "Custom product:\n";builder->ProducePartA();builder->ProducePartC();p=builder->GetProduct();p->ListParts();delete p;delete builder;
}int main(){Director* director= new Director();ClientCode(*director);delete director;return 0;
}
原型模式
亦称:克隆、Clone、Prototype
意图:
- 原型模式是一种创建型设计模式, 使你能够复制已有对象, 而又无需使代码依赖它们所属的类。
问题:
-
如果你有一个对象, 并希望生成与其完全相同的一个复制品, 你该如何实现呢? 首先, 你必须新建一个属于相同类的对象。 然后, 你必须遍历原始对象的所有成员变量, 并将成员变量值复制到新对象中。
-
不错! 但有个小问题。 并非所有对象都能通过这种方式进行复制, 因为有些对象可能拥有私有成员变量, 它们在对象本身以外是不可见的。
-
“从外部” 复制对象并非总是可行。
-
-
直接复制还有另外一个问题。 因为你必须知道对象所属的类才能创建复制品, 所以代码必须依赖该类。 即使你可以接受额外的依赖性, 那还有另外一个问题: 有时你只知道对象所实现的接口, 而不知道其所属的具体类, 比如可向方法的某个参数传入实现了某个接口的任何对象。
解决方案
-
原型模式将克隆过程委派给被克隆的实际对象。 模式为所有支持克隆的对象声明了一个通用接口, 该接口让你能够克隆对象, 同时又无需将代码和对象所属类耦合。 通常情况下, 这样的接口中仅包含一个
克隆
方法。 -
所有的类对
克隆
方法的实现都非常相似。 该方法会创建一个当前类的对象, 然后将原始对象所有的成员变量值复制到新建的类中。 你甚至可以复制私有成员变量, 因为绝大部分编程语言都允许对象访问其同类对象的私有成员变量。 -
支持克隆的对象即为原型。 当你的对象有几十个成员变量和几百种类型时, 对其进行克隆甚至可以代替子类的构造。
-
预生成原型可以代替子类的构造。
-
-
其运作方式如下: 创建一系列不同类型的对象并不同的方式对其进行配置。 如果所需对象与预先配置的对象相同, 那么你只需克隆原型即可, 无需新建一个对象。
真实世界类比
- 现实生活中, 产品在得到大规模生产前会使用原型进行各种测试。 但在这种情况下, 原型只是一种被动的工具, 不参与任何真正的生产活动。
- 由于工业原型并不是真正意义上的自我复制, 因此细胞有丝分裂 (还记得生物学知识吗?) 或许是更恰当的类比。 有丝分裂会产生一对完全相同的细胞。 原始细胞就是一个原型, 它在复制体的生成过程中起到了推动作用。
原型模式结构
-
1、原型 (Prototype) 接口将对克隆方法进行声明。 在绝大多数情况下, 其中只会有一个名为
clone
克隆的方法。-
<<interface>> Prototype+clone():Prototype
-
-
2、具体原型 (Concrete Prototype) 类将实现克隆方法。 除了将原始对象的数据复制到克隆体中之外, 该方法有时还需处理克隆过程中的极端情况, 例如克隆关联对象和梳理递归依赖等等。
-
ConcretePrototype-field1+ConcretePrototype(Prototype)+clone():PrototypeSubClassPrototype-field1+SubClassPrototype(Prototype)+clone():Prototype
-
-
3、客户端 (Client) 可以复制实现了原型接口的任何对象。
-
Clientcopy = exist.clone()
-
原型模式适合应用场景
- 如果你需要复制一些对象, 同时又希望代码独立于这些对象所属的具体类, 可以使用原型模式。
- 这一点考量通常出现在代码需要处理第三方代码通过接口传递过来的对象时。 即使不考虑代码耦合的情况, 你的代码也不能依赖这些对象所属的具体类, 因为你不知道它们的具体信息。
- 原型模式为客户端代码提供一个通用接口, 客户端代码可通过这一接口与所有实现了克隆的对象进行交互, 它也使得客户端代码与其所克隆的对象具体类独立开来。
- 如果子类的区别仅在于其对象的初始化方式, 那么你可以使用该模式来减少子类的数量。 别人创建这些子类的目的可能是为了创建特定类型的对象。
- 在原型模式中, 你可以使用一系列预生成的、 各种类型的对象作为原型。
- 客户端不必根据需求对子类进行实例化, 只需找到合适的原型并对其进行克隆即可。
实现方式
-
创建原型接口, 并在其中声明
克隆
方法。 如果你已有类层次结构, 则只需在其所有类中添加该方法即可。 -
原型类必须另行定义一个以该类对象为参数的构造函数。 构造函数必须复制参数对象中的所有成员变量值到新建实体中。 如果你需要修改子类, 则必须调用父类构造函数, 让父类复制其私有成员变量值。
如果编程语言不支持方法重载, 那么你可能需要定义一个特殊方法来复制对象数据。 在构造函数中进行此类处理比较方便, 因为它在调用
new
运算符后会马上返回结果对象。 -
克隆方法通常只有一行代码: 使用
new
运算符调用原型版本的构造函数。 注意, 每个类都必须显式重写克隆方法并使用自身类名调用new
运算符。 否则, 克隆方法可能会生成父类的对象。 -
你还可以创建一个中心化原型注册表, 用于存储常用原型。
你可以新建一个工厂类来实现注册表, 或者在原型基类中添加一个获取原型的静态方法。 该方法必须能够根据客户端代码设定的条件进行搜索。 搜索条件可以是简单的字符串, 或者是一组复杂的搜索参数。 找到合适的原型后, 注册表应对原型进行克隆, 并将复制生成的对象返回给客户端。
最后还要将对子类构造函数的直接调用替换为对原型注册表工厂方法的调用。
原型模式优缺点
优点:
- 你可以克隆对象, 而无需与它们所属的具体类相耦合。
- 你可以克隆预生成原型, 避免反复运行初始化代码。
- 你可以更方便地生成复杂对象。
- 你可以用继承以外的方式来处理复杂对象的不同配置。
缺点:
- 克隆包含循环引用的复杂对象可能会非常麻烦。
与其他模式的关系
- 在许多设计工作的初期都会使用工厂方法模式 (较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式、 原型模式或生成器模式 (更灵活但更加复杂)。
- 抽象工厂模式通常基于一组工厂方法, 但你也可以使用原型模式来生成这些类的方法。
- 原型可用于保存命令模式的历史记录。
- 大量使用组合模式和装饰模式的设计通常可从对于原型的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。
- 原型并不基于继承, 因此没有继承的缺点。 另一方面, 原型需要对被复制对象进行复杂的初始化。 工厂方法基于继承, 但是它不需要初始化步骤。
- 有时候原型可以作为备忘录模式的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。
- 抽象工厂、 生成器和原型都可以用单例模式来实现。
概念示例
识别方法: 原型可以简单地通过
clone
或copy
等方法来识别。
本例说明了原型设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
using std::string;// 原型设计模式
// 目的:允许你复制现有对象,而无需使代码依赖于它们的类。enum Type {PROTOTYPE_1 = 0,PROTOTYPE_2
};/*** 具有克隆功能的示例类。我们将看到不同类型的字段值如何被克隆。*/class Prototype {protected:string prototype_name_;float prototype_field_;public:Prototype() {}Prototype(string prototype_name): prototype_name_(prototype_name) {}virtual ~Prototype() {}virtual Prototype *Clone() const = 0;virtual void Method(float prototype_field) {this->prototype_field_ = prototype_field;std::cout << "Call Method from " << prototype_name_ << " with field : " << prototype_field << std::endl;}
};/*** ConcretePrototype1 是 Prototype 的子类,并实现了 Clone 方法。在本例中,Prototype 类的所有数据成员都在 Stack 中。如果你的属性中包含指针,例如 String* name_,则需要实现复制构造函数,以确保从 clone 方法获得深层复制。*/class ConcretePrototype1 : public Prototype {private:float concrete_prototype_field1_;public:ConcretePrototype1(string prototype_name, float concrete_prototype_field): Prototype(prototype_name), concrete_prototype_field1_(concrete_prototype_field) {}/*** 注意,Clone 方法返回一个指向新的 ConcretePrototype1 副本的指针。因此,客户端(调用 clone 方法的客户端)有责任释放该内存。如果您了解智能指针,您可能更喜欢在这里使用 unique_pointer。*/Prototype *Clone() const override {return new ConcretePrototype1(*this);}
};class ConcretePrototype2 : public Prototype {private:float concrete_prototype_field2_;public:ConcretePrototype2(string prototype_name, float concrete_prototype_field): Prototype(prototype_name), concrete_prototype_field2_(concrete_prototype_field) {}Prototype *Clone() const override {return new ConcretePrototype2(*this);}
};/*** 在原型工厂中,您有两个具体的原型,每个具体的原型类一个,因此每次您想要创建一个项目符号时,您都可以使用现有的原型并克隆它们。*/class PrototypeFactory {private:std::unordered_map<Type, Prototype *, std::hash<int>> prototypes_;public:PrototypeFactory() {prototypes_[Type::PROTOTYPE_1] = new ConcretePrototype1("PROTOTYPE_1 ", 50.f);prototypes_[Type::PROTOTYPE_2] = new ConcretePrototype2("PROTOTYPE_2 ", 60.f);}/*** 小心释放所有分配的内存。再次强调,如果你有智能指针,最好在这里使用它。*/~PrototypeFactory() {delete prototypes_[Type::PROTOTYPE_1];delete prototypes_[Type::PROTOTYPE_2];}/*** 请注意,您只需指定所需的原型类型即可。 该方法将根据此类型的对象创建。*/Prototype *CreatePrototype(Type type) {return prototypes_[type]->Clone();}
};void Client(PrototypeFactory &prototype_factory) {std::cout << "Let's create a Prototype 1\n";Prototype *prototype = prototype_factory.CreatePrototype(Type::PROTOTYPE_1);prototype->Method(90);delete prototype;std::cout << "\n";std::cout << "Let's create a Prototype 2 \n";prototype = prototype_factory.CreatePrototype(Type::PROTOTYPE_2);prototype->Method(10);delete prototype;
}int main() {PrototypeFactory *prototype_factory = new PrototypeFactory();Client(*prototype_factory);delete prototype_factory;return 0;
}
单例模式
亦称: 单件模式、Singleton
意图:
- 单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
问题:
-
单例模式同时解决了两个问题, 所以违反了单一职责原则:
-
1、保证一个类只有一个实例。 为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。
-
它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。
-
注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它必须总是返回一个新对象。
-
单例模式:
- 客户端甚至可能没有意识到它们一直都在使用同一个对象。
-
-
2、为该实例提供一个全局访问节点。 还记得你 (好吧, 其实是我自己) 用过的那些存储重要对象的全局变量吗? 它们在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。
- 和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。
- 还有一点: 你不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此。
-
如今, 单例模式已经变得非常流行, 以至于人们会将只解决上文描述中任意一个问题的东西称为单例。
解决方案
-
将默认构造函数设为私有, 防止其他对象使用单例类的
new
运算符。 -
新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。
-
如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。
真实世界类比
- 政府是单例模式的一个很好的示例。 一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么, “某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。
单例模式结构
-
1、单例 (Singleton) 类声明了一个名为
getInstance
获取实例的静态方法来返回其所属类的一个相同实例。-
单例的构造函数必须对客户端 (Client) 代码隐藏。 调用
获取实例
方法必须是获取单例对象的唯一方式。 -
SingLeton-instance: Singleton // 静态实例-SingLeton()+getInstance():SingLetonif(instance==nullptr) {instance = new Singleton()}return instance
-
单例模式适合应用场景
- 如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。
- 单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。
- 如果你需要更加严格地控制全局变量,可以使用单例模式。
- 单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。
- 请注意, 你可以随时调整限制并设定生成单例实例的数量, 只需修改
获取实例
方法, 即 getInstance 中的代码即可实现。
实现方式
- 在类中添加一个私有静态成员变量用于保存单例实例。
- 声明一个公有静态构建方法用于获取单例实例。
- 在静态方法中实现"延迟初始化"。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。
- 将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。
- 检查客户端代码, 将对单例的构造函数的调用替换为对其静态构建方法的调用。
单例模式优缺点
优点
- 你可以保证一个类只有一个实例。
- 你获得了一个指向该实例的全局访问节点。
- 仅在首次请求单例对象时对其进行初始化。
缺点
- 违反了单一职责原则。 该模式同时解决了两个问题。
- 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。
- 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
- 单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。
与其他模式的关系
- 外观模式类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
- 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元模式就和单例类似了。 但这两个模式有两个根本性的不同。
- 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
- 单例对象可以是可变的。 享元对象是不可变的。
- 抽象工厂模式、 生成器模式和原型模式都可以用单例来实现。
伪代码
// 数据库类会对`getInstance(获取实例)`方法进行定义以让客户端在程序各处
// 都能访问相同的数据库连接实例。
class Database is// 保存单例实例的成员变量必须被声明为静态类型。private static field instance: Database// 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构// 造方法。private constructor Database() is// 部分初始化代码(例如到数据库服务器的实际连接)。// ……// 用于控制对单例实例的访问权限的静态方法。public static method getInstance() isif (Database.instance