前置知识 来复习

根据:

深入设计模式
亚历山大 · 什韦茨

进行的个人总结,感谢支持

设计模式分类

  • Creational Pattern
    • 创建型模式提供了创建对象的机制, 能够提升已有代码的灵活性和可复用性
      • Factory Method - 工厂方法
        • 父类中提供一个创建对象的接口以允许子类决定实例化对象的类型
      • Abstract Factory - 抽象工厂
        • 允许创建一系列相关的对象而 「不用」指定其具体类
      • Builder - 生成器
        • 分步骤创建复杂对象,允许使用相同的创建代码生成不同类型和形式的对象
      • Prototype - 原型
        • 让人能复制已有对象,而又无须使代码依赖他们所属的类
      • Singleton - 单例
        • 让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点
  • Structural Pattern
    • 结构型模式介绍如何将对象和类组装成较大的结构,并保持结构的灵活和高效
      • Adapter - 适配器
        • 让接口不兼容的对象能够相互工作
      • Bridge - 桥接
        • 可将一个大类或一系列紧密相关的类拆分为「抽象」和「实现」两个独立的层次结构,从而能在开发时分别使用
      • Composite - 组合
        • 使用它将对象组合成树状结构,并且像独立对象一样使用它们
      • Decorator - 装饰
        • 通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为
      • Facade - 外观
        • 为程序库,框架或其他复杂类提供一个简单的接口
      • Flyweight - 享元
        • 摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,让你能在有限的内存容量中载入更多对象
      • Proxy - 代理
        • 让你能够提供对象的替代品或其占位符。代理控制着对于原对象的访问,并允许在将请求提交给对象前后进行一些处理
  • Behavioral Pattern
    • 行为模式负责对象间的高效沟通和职责委派

Creational Pattern

工厂方法 - Factory Method

Factory_Method_1

工厂方法是一种创建型设计模式,其在父类中提供一个创建对象的方法,允许子类决定实例化对象的类型。

现实问题

一开始写的代码只支持卡车运输,大部分代码都在卡车类下。一段时间以后,需要支持其他的运输方式,比如海运,那么由于现在的代码几乎都在卡车下,想要实现新的方式就会比较困难。因此不得不大幅度,频繁修改代码,并根据不同的运输对象类在应用中进行不同的处理

解决方案

工厂方法建议使用特殊的工厂方法代替对于对象构造函数的直接调用

Factory_Method_2

在 子类的createTransport中调用 new 从而返回产品

这样的好处是我们现在可以在子类中重写工厂方法,从而改变其创建产品的类型。

但是,仅当这些产品具有共同的基类或者接口时,子类才可以返回不同类型的产品。同时,基类中的工厂方法还应将返回类型声明为这一公共接口:

Factory_Method_3

总结:卡车Truck轮船Ship 类都必须实现 运输Trans­port 接口, 该接口声明了一个名为 deliver交付 的方法。 每个类都将以不同的方式实现该方法: 卡车走陆路交付货物, 轮船走海路交付货物。 陆路运输Road­Logis­tics类中的工厂方法返回卡车对象, 而 海路运输Sea­Logis­tics类则返回轮船对象。

这样就可以使得调用工厂部分的代码,即客户端代码 不需要知道不同子类和返回实际对象之间的区别。客户端将所有产品都认为是 抽象的 运输 客户端知道所有运输对象都提供 交付 的方法,但是不需要知道具体是怎么实现的。

结构

Factory_Method_4

产品 (Prod­uct) 将会对接口进行声明。 对于所有由创建者及其子类构建的对象, 这些接口都是通用的。

具体产品 (Con­crete Prod­ucts) 是产品接口的不同实现。

创建者 (Cre­ator) 类声明返回产品对象的工厂方法。 该方法的返回对象类型必须与产品接口相匹配。
你可以将工厂方法声明为抽象方法, 强制要求每个子类以不同方式实现该方法。 或者, 你也可以在基础工厂方法中返回默认产品类型。
注意, 尽管它的名字是创建者, 但它最主要的职责并不是创建产品。 一般来说, 创建者类包含一些与产品相关的核心业务逻辑。 工厂方法将这些逻辑处理从具体产品类中分离出来。 打个比方, 大型软件开发公司拥有程序员培训部门。 但是, 这些公司的主要工作还是编写代码, 而非生产程序员。

具体创建者 (Con­crete Cre­ators) 将会重写基础工厂方法, 使其返回不同类型的产品。
注意, 并不一定每次调用工厂方法都会创建新的实例。 工厂方法也可以返回缓存、 对象池或其他来源的已有对象。

样例

Factory_Method_5

如果使用工厂方法, 就不需要为每种操作系统重写对话框逻辑。 如果我们声明了一个在基本对话框类中生成按钮的工厂方法, 那么我们就可以创建一个对话框子类, 并使其通过工厂方法返回 Win­dows 样式按钮。 子类将继承对话框基础类的大部分代码, 同时在屏幕上根据 Win­dows 样式渲染按钮。

如需该模式正常工作, 基础对话框类必须使用抽象按钮 (例如基类或接口), 以便将其扩展为具体按钮。 这样一来, 无论对话框中使用何种类型的按钮, 其代码都可以正常工作。

每向对话框中添加一个新的工厂方法, 你就离抽象工厂模式更近一步。

总结

产品接口 + 产品类:

需要 一个 interface / 基类 product 抽象化从而可以扩展为具体的product

需要 concrete products 来实现具体的功能

工厂接口 + 工厂类:

需要一个Creator 从而让客户端能够调用

需要 concrete creator来返回具体的实例类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 创建者类声明的工厂方法必须返回一个产品类的对象。创建者的子类通常会提供
// 该方法的实现。
class Dialog is
  // 创建者还可提供一些工厂方法的默认实现。
  abstract method createButton():Button

  // 请注意,创建者的主要职责并非是创建产品。其中通常会包含一些核心业务
  // 逻辑,这些逻辑依赖于由工厂方法返回的产品对象。子类可通过重写工厂方
  // 法并使其返回不同类型的产品来间接修改业务逻辑。
  method render() is
    // 调用工厂方法创建一个产品对象。
    Button okButton = createButton()
    // 现在使用产品。
    okButton.onClick(closeDialog)
    okButton.render()


// 具体创建者将重写工厂方法以改变其所返回的产品类型。
class WindowsDialog extends Dialog “is
  method createButton():Button is
    return new WindowsButton()

class WebDialog extends Dialog is
  method createButton():Button is
    return new HTMLButton()


// 产品接口中将声明所有具体产品都必须实现的操作。
interface Button is
  method render()
  method onClick(f)

// 具体产品需提供产品接口的各种实现。
class WindowsButton implements Button is
  method render(a, b) is
    // 根据 Windows 样式渲染按钮。
  method onClick(f) is
    // 绑定本地操作系统点击事件。

class HTMLButton implements Button is
  method render(a, b) is
    // 返回一个按钮的 HTML 表述。
  method onClick(f) is
    // 绑定网络浏览器的点击事件。


class Application is
  field dialog: Dialog

  // 程序根据当前配置或环境设定选择创建者的类型。
  method initialize() is
    config = readApplicationConfigFile()

    if (config.OS == "Windows"then
      dialog = new WindowsDialog()
    else if (config.OS == "Web"then
      dialog = new WebDialog()
else
      throw new Exception("错误!未知的操作系统。")

  // 当前客户端代码会与具体创建者的实例进行交互,但是必须通过其基本接口
  // 进行。只要客户端通过基本接口与创建者进行交互,你就可将任何创建者子
  // 类传递给客户端。
  method main() is
    this.initialize()
    dialog.render()

使用场景

  1. 无法预知对象确切类别以及依赖关系

    1. 工厂方法将创建产品的代码和实际的使用逻辑分离,从而能在不影响其他代码的情况下扩展产品创建部分的代码
    2. 例如,如果需要向应用中添加一种新产品,我们现在只需要开发新的创建者子类,然后重写其工厂方法即可
  2. 需要用户扩展应用库或框架的内部组件

    1. 将各框架中构造组件的代码集中到单个工厂方法中, 并在继承该组件之外允许任何人对该方法进行重写。
    2. 假设你使用开源 UI 框架编写自己的应用。 你希望在应用中使用圆形按钮, 但是原框架仅支持矩形按钮。 你可以使用 圆形按钮Round­But­ton子类来继承标准的 按钮But­ton类。 但是, 你需要告诉 UI框架UIFrame­work类使用新的子类按钮代替默认按钮。
      为了实现这个功能, 你可以根据基础框架类开发子类 圆形按钮UI UIWith­Round­But­tons , 并且重写其 create­Button创建按钮方法。 基类中的该方法返回 按钮对象, 而你开发的子类返回 圆形按钮对象。 现在, 你就可以使用 圆形按钮 UI类代替 UI框架类。
  3. 复用对象,而不是每一次都创建新对象

    1. 比如数据库连接

    2. 复用现有对象的方法:

      首先, 你需要创建存储空间来存放所有已经创建的对象。
      当他人请求一个对象时, 程序将在对象池中搜索可用对象。
      …然后将其返回给客户端代码。
      如果没有可用对象, 程序则创建一个新对象 (并将其添加到对象池中)

    3. 我们需要 一个既能够创建新对象, 又可以重用现有对象的普通方法 – 工厂方法:

      1. 让所有产品都遵循同一接口。 该接口必须声明对所有产品都有意义的方法。
      2. 在创建类中添加一个空的工厂方法。 该方法的返回类型必须遵循通用的产品接口。
      3. 在创建者代码中找到对于产品构造函数的所有引用。 将它们依次替换为对于工厂方法的调用, 同时将创建产品的代码移入工厂方法。 你可能需要在工厂方法中添加临时参数来控制返回的产品类型。
        工厂方法的代码看上去可能非常糟糕。 其中可能会有复杂的 switch分支运算符, 用于选择各种需要实例化的产品类。 但是不要担心, 我们很快就会修复这个问题。
      4. 现在, 为工厂方法中的每种产品编写一个创建者子类, 然后在子类中重写工厂方法, 并将基本方法中的相关创建代码移动到工厂方法中。
      5. 如果应用中的产品类型太多, 那么为每个产品创建子类并无太大必要, 这时你也可以在子类中复用基类中的控制参数。
        例如, 设想你有以下一些层次结构的类。 基类 邮件及其子类 航空邮件和 陆路邮件 ; 运输及其子类 飞机, 卡车和 火车 。 航空邮件仅使用 飞机对象, 而 陆路邮件则会同时使用 卡车和 火车对象。 你可以编写一个新的子类 (例如 火车邮件 ) 来处理这两种情况, 但是还有其他可选的方案。 客户端代码可以给 陆路邮件类传递一个参数, 用于控制其希望获得的产品。
      6. 如果代码经过上述移动后, 基础工厂方法中已经没有任何代码, 你可以将其转变为抽象类。 如果基础工厂方法中还有其他语句, 你可以将其设置为该方法的默认行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 产品接口以及具体的产品类:
// 产品接口
public interface Product {
void useProduct();
}

// 具体产品类:AirMail
public class AirMail implements Product {
@Override
public void useProduct() {
System.out.println("Using AirMail");
}
}

// 具体产品类:LandMail
public class LandMail implements Product {
@Override
public void useProduct() {
System.out.println("Using LandMail");
}
}
// 工厂类
import java.util.HashMap;
import java.util.Map;

public class ProductFactory {
private Map<String, Product> pool = new HashMap<>();

// 工厂方法
public Product getProduct(String type) {
Product product = pool.get(type);

if (product == null) {
// 根据类型创建新产品
switch (type) {
case "AirMail":
product = new AirMail();
break;
case "LandMail":
product = new LandMail();
break;
}
if (product != null) {
pool.put(type, product); // 将新创建的产品添加到池中
}
}
return product;
}

// 释放产品实例
public void releaseProduct(String type, Product product) {
pool.put(type, product);
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
ProductFactory factory = new ProductFactory();

// 获取AirMail产品实例
Product airMail = factory.getProduct("AirMail");
airMail.useProduct();

// 获取LandMail产品实例
Product landMail = factory.getProduct("LandMail");
landMail.useProduct();
}
}

ProductFactory 类管理着一个对象池,当请求一个特定类型的产品时,它首先尝试从池中获取。如果池中没有可用的产品实例,则创建一个新的实例并将其添加到池中。这样,当同一类型的产品再次被请求时,可以重用之前创建的实例,而不是每次都创建新的实例。

额外的:工厂方法 后 演变为 抽象工厂,原型,生成器 方法

抽象工厂方法 - Abstract Factory

其是一种创建型设计模式,它能 创建一系列相关的对象 而无需指定具体类

abstract_factory_1

现实问题

假设正在开发一款家具商店模拟器

代码中有一些类:

  1. 来表示相关产品:椅子,沙发,咖啡桌
  2. 系列产品的不同变体。比如 有 现代风格的椅子,或者XX风格的椅子

abstract_factory_2

也就是说对于每一种商品。我们有多种变体。

因此我们需要对于每一种艺术风格,我们都需要设计一种家具对象。并且我们当然不希望在添加新产品/风格的时候修改已有的代码。

解决方案

  1. 抽象工厂模式建议为 系列中的每一件产品 明确声明接口。然后确保所有变体继承/实现这些接口

e.g: 所有风格的椅子都实现椅子接口。

abstract_factory_3

  1. 然后我们需要一个抽象工厂其包含系列中所有产品构造方法的接口。createChair createSofa createXXX,这些接口都必须返回抽象产品类型也就是 createChair:Chair createSofa:Sofa``createCoffeeTable:CoffeTable

abstract_factory_4

  1. 对于每一个变体,使用「抽象工厂接口」来创建不同的「工厂类」 每一个工厂类都只能返回特定类别的产品,比如VictorianFurnitureFactory 返回 Victorian,Modern 返回 Modern

这样,客户端就可以通过相应的抽象接口调用工厂和产品类而无需修改客户端代码

结构

abstract_factory_5

  1. ProductA, ProductB: (Abstract Product 抽象产品) 构成系列产品的一组不同但相关的产品声明接口
  2. Concrete ProductX: (Concrete Product 具体产品) 抽象产品的多种不同类型实现。所有变体(风格,比如维多利亚/现代)都必须实现相应的抽象产品(椅子/沙发)
  3. Abstract Factory 抽象工厂: 接口声明一组创建 抽象产品的方法
  4. ConcreteFactoryX 具体工厂:实现抽象工厂的构建方法,每一个具体工厂都生产且仅创建对应产品变体
  5. 客户端对于具体产品的初始化,其构建方法签名必须返回相应的 抽象 产品。这样,使用工厂类的客户端代码就不会与工厂创建的特定产品变体耦合。客户端只需要通过抽象接口嗲用工厂和产品对象,就能与任何具体工厂/产品变体交互。

样例

abstract_factory_6

这个例子通过使用 抽象工厂 模式从而使得客户端代码与具体UI类耦合从而创建跨平台的UI元素,同时确保所创建的元素与制定的操作系统匹配。

跨平台应用中的相同 UI 元素功能类似, 但是在不同操作系统下的外观有一定差异。 此外, 你需要确保 UI 元素与当前操作系统风格一致。 你一定不希望在 Win­dows 系统下运行的应用程序中显示 macOS 的控件。

抽象工厂接口声明一系列构建方法, 客户端代码可调用它们生成不同风格的 UI 元素。 每个具体工厂对应特定操作系统, 并负责生成符合该操作系统风格的 UI 元素。

其运作方式如下: 应用程序启动后检测当前操作系统。 根据该信息, 应用程序通过与该操作系统对应的类创建工厂对象。 其余代码使用该工厂对象创建 UI 元素。 这样可以避免生成错误类型的元素。

使用这种方法, 客户端代码只需调用抽象接口, 而无需了解具体工厂类和 UI 元素。 此外, 客户端代码还支持未来添加新的工厂或 UI 元素。

这样一来, 每次在应用程序中添加新的 UI 元素变体时, 你都无需修改客户端代码。 你只需创建一个能够生成这些 UI 元素的工厂类, 然后稍微修改应用程序的初始代码, 使其能够选择合适的工厂类即可。

总结

伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 抽象工厂接口声明了一组能返回不同抽象产品的方法。这些产品属于同一个系列
// 且在高层主题或概念上具有相关性。同系列的产品通常能相互搭配使用。系列产
// 品可有多个变体,但不同变体的产品不能搭配使用。
interface GUIFactory is
  method createButton():Button
  method createCheckbox():Checkbox


// 具体工厂可生成属于同一变体的系列产品。工厂会确保其创建的产品能相互搭配
// 使用。具体工厂方法签名会返回一个抽象产品,但在方法内部则会对具体产品进
// 行实例化。
class WinFactory implements GUIFactory is
  method createButton():Button is
    return new WinButton()
  method createCheckbox():Checkbox is
    return new WinCheckbox()

// 每个具体工厂中都会包含一个相应的产品变体。
class MacFactory implements GUIFactory is
  method createButton():Button is
    return new MacButton()
  method createCheckbox():Checkbox is
    return new MacCheckbox()


// 系列产品中的特定产品必须有一个基础接口。所有产品变体都必须实现这个接口。
interface Button is
  method paint()

// 具体产品由相应的具体工厂创建。
class WinButton implements Button is
  method paint() is
    // 根据 Windows 样式渲染按钮。

class MacButton implements Button is
  method paint() is
    // 根据 macOS 样式渲染按钮

// 这是另一个产品的基础接口。所有产品都可以互动,但是只有相同具体变体的产
// 品之间才能够正确地进行交互。
interface Checkbox is
  method paint()

class WinCheckbox implements Checkbox is
  method paint() is
    // 根据 Windows 样式渲染复选框。
class MacCheckbox implements Checkbox is
  method paint() is
    // 根据 macOS 样式渲染复选框。

// 客户端代码仅通过抽象类型(GUIFactory、Button 和 Checkbox)使用工厂
// 和产品。这让你无需修改任何工厂或产品子类就能将其传递给客户端代码。
class Application is
  private field factory: GUIFactory
  private field button: Button
  constructor Application(factory: GUIFactory) is
    this.factory = factory
  method createUI() is
    this.button = factory.createButton()
  method paint() is
    button.paint()


// 程序会根据当前配置或环境设定选择工厂类型,并在运行时创建工厂(通常在初
// 始化阶段)。
class ApplicationConfigurator is
  method main() is
    config = readApplicationConfigFile()

    if (config.OS == "Windows"then
      factory = new WinFactory()
    else if (config.OS == "Mac"then
      factory = new MacFactory()
    else
      throw new Exception("错误!未知的操作系统。")

    Application app = new Application(factory)

实际的java code例子:

在这个例子中,我们创建一个关于电子产品的简单场景。

假设有两类产品:手机和平板电脑,分别有不同的品牌(例如:苹果和三星)。我们将实现抽象工厂和具体工厂来创建这些产品。

在这个例子中,ElectronicsFactory 是一个抽象工厂接口,定义了创建智能手机和平板电脑的方法。AppleFactorySamsungFactory 是具体的工厂实现,分别创建苹果和三星品牌的产品。客户端代码依赖于抽象工厂和产品接口,而不是具体的实现类,这使得增加新的产品品牌(例如添加一个华为工厂)时,无需修改客户端代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 抽象产品
public interface Smartphone {
void display();
}

public interface Tablet {
void display();
}

// 具体产品
public class AppleSmartphone implements Smartphone {
@Override
public void display() {
System.out.println("Apple Smartphone");
}
}

public class SamsungSmartphone implements Smartphone {
@Override
public void display() {
System.out.println("Samsung Smartphone");
}
}

public class AppleTablet implements Tablet {
@Override
public void display() {
System.out.println("Apple Tablet");
}
}

public class SamsungTablet implements Tablet {
@Override
public void display() {
System.out.println("Samsung Tablet");
}
}


// 抽象工厂
public interface ElectronicsFactory {
Smartphone createSmartphone();
Tablet createTablet();
}

// 具体工厂
public class AppleFactory implements ElectronicsFactory {
@Override
public Smartphone createSmartphone() {
return new AppleSmartphone();
}

@Override
public Tablet createTablet() {
return new AppleTablet();
}
}

public class SamsungFactory implements ElectronicsFactory {
@Override
public Smartphone createSmartphone() {
return new SamsungSmartphone();
}

@Override
public Tablet createTablet() {
return new SamsungTablet();
}
}


// 客户端
public class Client {
public static void main(String[] args) {
ElectronicsFactory appleFactory = new AppleFactory();
Smartphone appleSmartphone = appleFactory.createSmartphone();
Tablet appleTablet = appleFactory.createTablet();

appleSmartphone.display();
appleTablet.display();

ElectronicsFactory samsungFactory = new SamsungFactory();
Smartphone samsungSmartphone = samsungFactory.createSmartphone();
Tablet samsungTablet = samsungFactory.createTablet();

samsungSmartphone.display();
samsungTablet.display();
}
}

使用场景

  1. 代码需要与多个不同系列的相关产品交互,但是我们无法预先获取相关信息,不希望代码对于产品具体类进行构建
    1. 抽象工厂的接口可以用于创建么个系列产品的对象,只要代码通过该接口创建对象。
  2. 有一个基于一组抽象方法的类,且其主要功能因此变得不明确
    1. 好的代码需要:每个类仅负责一件事,如果一个类与多个类型产品交互,那么我们可以考虑将工厂方法抽取到独立的工厂类或具备完整功能的抽象公差那个类

实现方式

  1. 以不同的产品类型与产品变体为维度绘制矩阵
    • 在示例中,我们有两种产品类型:SmartphoneTablet(抽象产品接口),这构成了矩阵的一维。
    • 另一维是产品变体,即不同品牌:AppleSamsung。每个品牌都有其对应的智能手机和平板电脑产品。
  2. 为所有产品声明抽象产品接口,并且让所有具体产品类实现这些接口
    • 抽象产品接口为 SmartphoneTablet。具体产品类如 AppleSmartphone, SamsungSmartphone, AppleTablet, SamsungTablet 实现了这些接口。
  3. 声明抽象工厂接口,并且在接口中为所有抽象产品提供一组构建方法
    • 抽象工厂接口是 ElectronicsFactory,它定义了创建智能手机和平板电脑的方法(createSmartphone()createTablet())。
  4. 为每种产品变体实现一个具体工厂类
    • 具体工厂类为 AppleFactorySamsungFactory,每个工厂类都实现了 ElectronicsFactory 接口,并能创建特定品牌的智能手机和平板电脑。
  5. 在应用程序中开发初始化代码,根据应用程序配置或当前环境对特定具体工厂类进行初始化,并将该工厂对象传递给所有需要创建产品的类
    • 客户端代码 (Client 类) 演示了如何根据需要选择并初始化具体工厂(例如 AppleFactorySamsungFactory),并使用这些工厂来创建产品。
  6. 找出代码中所有对产品构造函数的直接调用,将其替换为对工厂对象中相应构建方法的调用
    • 在客户端代码中,不直接使用产品的构造函数来创建产品实例。相反,它调用工厂对象的 createSmartphone()createTablet() 方法来获取产品实例。

抽象工厂和桥接(bridge) 可以搭配使用,如果由桥接定义的抽象只能与特定实现合作, 这一模式搭配就非常有用。 在这种情况下, 抽象工厂可以对这些关系进行封装, 并且对客户端代码隐藏其复杂性。

生成器 - Builder

生成器模式允许我们能够分步骤创建复杂对象。该模式允许使用相同的创建代码生成不同类型和形式的对象

现实问题

有个对象比较复杂,在构造的时候需要对诸多成员变量和嵌套对象进行复杂的初始化工作。这些用于初始化的代码可能深藏于一个包含众多参数且让人基本看不懂的构造函数中;也可能会散落在客户端代码的多个位置

假如我们需要创建一个 house,house可能有 garage,可能有swimmingpool,garden etc;如果为每种可能的对象都创建一个子类,那么可能会导致程序变得过于复杂。

一种可行的方法为:我们有一个House的基类,然后创建一系列涵盖所有参数组合的子类。但是这会使得我们在新增了任何参数比如门框大小,雕塑等等都会使得这个房屋的层次结构变得非常复杂。

builder_1

另一种方法则是实用一个超级构造函数:

builder_2

但是这样的话就会使得构造函数非常的臃肿,并且我们有很多参数是不会使用的。

解决方案

生成器模式则建议将对象构造代码从产品类中抽出,并且方位一个生成器 的类中:

builder_3

这个模式会让构造对象划分为一组步骤:buildWalls() buildDoors()。在创建对象的时候,都需要通过生成器对象来执行一系列步骤。不同的是,我们只需要选择需要的步骤即可。

当我们需要创建不同形式的产品时,其中的一些构造步骤需要不同的实现,比如木屋需要木制门,铁屋需要铁门等等。

如果是这种情况,那我们可以创建多个不同的Builder,用不同的方式实现一组相同的创建步骤,然后在创建过程中使用这些生成器来实现不同的对象

builder_4

主管

额外的,我们可以进一步将用于创建产品的一系列生成器步骤调用抽取成为单独的主管类。主管类可定义创建步骤的执行顺序,而生成器提供步骤的具体实现

builder_5

主管类不是必须的,他比较适用于在流水线工程中以便能够重复使用。在一般情况下,客户端代码直接以特定顺序调用创建步骤即可。

对于客户端代码来说,主管类隐藏了产品构造细节。

结构

builder_6

  1. 生成器 (Builder) 接口声明在所有类型生成器中通用的产品构造步骤。
  2. 具体生成器 (Con­crete Builders) 提供构造过程的不同实现。 具体生成器也可以构造不遵循通用接口的产品。
  3. 产品 (Prod­ucts) 是最终生成的对象。 由不同生成器构造的产品无需属于同一类层次结构或接口。
  4. 主管 (Direc­tor) 类定义调用构造步骤的顺序, 这样你就可以创建和复用特定的产品配置。
  5. 客户端 (Client) 必须将某个生成器对象与主管类关联。 一般情况下, 你只需通过主管类构造函数的参数进行一次性关联即可。 此后主管类就能使用生成器对象完成后续所有的构造任务。 但在客户端将生成器对象传递给主管类制造方法时还有另一种方式。 在这种情况下, 你在使用主管类生产产品时每次都可以使用不同的生成器。

样例

使用builder pattern来生成Car以及CarManual

builder_7

汽车是一个复杂对象, 有数百种不同的制造方法。 我们没有在 汽车类中塞入一个巨型构造函数, 而是将汽车组装代码抽取到单独的汽车生成器类中。 该类中有一组方法可用来配置汽车的各种部件。

如果客户端代码需要组装一辆与众不同、 精心调教的汽车, 它可以直接调用生成器。 或者, 客户端可以将组装工作委托给主管类, 因为主管类知道如何使用生成器制造最受欢迎的几种型号汽车。

每辆汽车都需要一本使用手册 (说真的, 谁会去读它们呢?)。 使用手册会介绍汽车的每一项功能, 因此不同型号的汽车, 其使用手册内容也不一样。 因此, 你可以复用现有流程来制造实际的汽车及其对应的手册。 当然, 编写手册和制造汽车不是一回事, 所以我们需要另外一个生成器对象来专门编写使用手册。 该类与其制造汽车的兄弟类都实现了相同的制造方法, 但是其功能不是制造汽车部件, 而是描述每个部件。 将这些生成器传递给相同的主管对象, 我们就能够生成一辆汽车或是一本使用手册了。

最后一个部分是获取结果对象。 尽管金属汽车和纸质手册存在关联, 但它们却是完全不同的东西。 我们无法在主管类和具体产品类不发生耦合的情况下, 在主管类中提供获取结果对象的方法。因此, 我们只能通过负责制造过程的生成器来获取结果对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// 只有当产品较为复杂且需要详细配置时,使用生成器模式才有意义。下面的两个
// 产品尽管没有同样的接口,但却相互关联。
class Car is
  // 一辆汽车可能配备有 GPS 设备、行车电脑和几个座位。不同型号的汽车(
  // 运动型轿车、SUV 和敞篷车)可能会安装或启用不同的功能。

class Manual is
  // 用户使用手册应该根据汽车配置进行编制,并介绍汽车的所有功能。


// 生成器接口声明了创建产品对象不同部件的方法。
interface Builder is
  method reset()
  method setSeats(...)
  method setEngine(...)
  method setTripComputer(...)
  method setGPS(...)

// 具体生成器类将遵循生成器接口并提供生成步骤的具体实现。你的程序中可能会
// 有多个以不同方式实现的生成器变体。
class CarBuilder implements Builder is
  private field car:Car

  // 一个新的生成器实例必须包含一个在后续组装过程中使用的空产品对象。
  constructor CarBuilder() is
this.reset()

  // reset(重置)方法可清除正在生成的对象。
  method reset() is
    this.car = new Car()

  // 所有生成步骤都会与同一个产品实例进行交互。
  method setSeats(...) is
    // 设置汽车座位的数量。

  method setEngine(...) is
    // 安装指定的引擎。

  method setTripComputer(...) is
    // 安装行车电脑。

  method setGPS(...) is
    // 安装全球定位系统。

  // 具体生成器需要自行提供获取结果的方法。这是因为不同类型的生成器可能
  // 会创建不遵循相同接口的、完全不同的产品。所以也就无法在生成器接口中
  // 声明这些方法(至少在静态类型的编程语言中是这样的)。
  //
  // 通常在生成器实例将结果返回给客户端后,它们应该做好生成另一个产品的
  // 准备。因此生成器实例通常会在 `getProduct(获取产品)`方法主体末尾
  // 调用重置方法。但是该行为并不是必需的,你也可让生成器等待客户端明确
  // 调用重置方法后再去处理之前的结果。
  method getProduct():Car is
    product = this.car
    this.reset()
    return product

// 生成器与其他创建型“模式的不同之处在于:它让你能创建不遵循相同接口的产品。
class CarManualBuilder implements Builder is
  private field manual:Manual

  constructor CarManualBuilder() is
    this.reset()

  method reset() is
    this.manual = new Manual()

  method setSeats(...) is
    // 添加关于汽车座椅功能的文档。

  method setEngine(...) is
    // 添加关于引擎的介绍。

  method setTripComputer(...) is
    // 添加关于行车电脑的介绍。

  method setGPS(...) is
    // 添加关于 GPS 的介绍。

  method getProduct():Manual is
    // 返回使用手册并重置生成器。
// 主管只负责按照特定顺序执行生成步骤。其在根据特定步骤或配置来生成产品时
// 会很有帮助。由于客户端可以直接控制生成器,所以严格意义上来说,主管类并
// 不是必需的。
class Director is
  private field builder:Builder

  // 主管可同由客户端代码传递给自身的任何生成器实例进行交互。客户端可通
  // 过这种方式改变最新组装完毕的产品的最终类型。
  method setBuilder(builder:Builder)
    this.builder = builder

  // 主管可使用同样的生成步骤创建多个产品变体。
  method constructSportsCar(builder: Builder) is
    builder.reset()
    builder.setSeats(2)
    builder.setEngine(new SportEngine())
    builder.setTripComputer(true)
    builder.setGPS(true)

  method constructSUV(builder: “Builder) is
    // ...


// 客户端代码会创建生成器对象并将其传递给主管,然后执行构造过程。最终结果
// 将需要从生成器对象中获取。
class Application is

  method makeCar() is
    director = new Director()

    CarBuilder builder = new CarBuilder()
    director.constructSportsCar(builder)
    Car car = builder.getProduct()

    CarManualBuilder builder = new CarManualBuilder()
    director.constructSportsCar(builder)

    // 最终产品通常需要从生成器对象中获取,因为主管不知晓具体生成器和
    // 产品的存在,也不会对其产生依赖。
    Manual manual = builder.getProduct()

使用场景

  • 避免重叠构造函数的出现

    • 一个构造函数中有十几个可选参数,或者复写这个函数来包含一些较少参数的简化版方法:
    1
    2
    3
    4
    class Pizza {
      Pizza(int size) { ... }
      Pizza(int size, boolean cheese) { ... }
      Pizza(int size, boolean cheese, boolean pepperoni) { ... }
    • 生成器模式让你可以分步骤生成对象, 而且允许你仅使用必须的步骤。 应用该模式后, 你再也不需要将几十个参数塞进构造函数里了。
  • 使用代码创建不同形式的产品 (例如石头或木头房屋) 时, 可使用生成器模式。

    • 如果你需要创建的各种形式的产品, 它们的制造过程相似且仅有细节上的差异, 此时可使用生成器模式。
    • 基本生成器接口中定义了所有可能的制造步骤, 具体生成器将实现这些步骤来制造特定形式的产品。 同时, 主管类将负责管理制造步骤的顺序
  • 使用生成器构造组合树或其他复杂对象。

    • 生成器模式让你能分步骤构造产品。 你可以延迟执行某些步骤而不会影响最终产品。 你甚至可以递归调用这些步骤, 这在创建对象树时非常方便。 生成器在执行制造步骤时, 不能对外发布未完成的产品。 这可以避免客户端代码获取到不完整结果对象的情况。

实现方法

  1. 定义通用步骤,确保可以制造所有形式的产品
  2. 在基本生成器接口中生命这些步骤
  3. 为每个形式的产品创建具体生成器类并实现具体的步骤,还要实现获取构造结果对象的方法。不能在生成器接口中声明该方法, 因为不同生成器构造的产品可能没有公共接口, 因此你就不知道该方法返回的对象类型。 但是, 如果所有产品都位于单一类层次中, 你就可以安全地在基本接口中添加获取生成对象的方法。
  4. 考虑创建主管类。 它可以使用同一生成器对象来封装多种构造产品的方式.
  5. 客户端代码会同时创建生成器和主管对象。 构造开始前, 客户端必须将生成器对象传递给主管对象。 通常情况下, 客户端只需调用主管类构造函数一次即可。 主管类使用生成器对象完成后续所有制造任务。 还有另一种方式, 那就是客户端可以将生成器对象直接传递给主管类的制造方法。
  6. 只有在所有产品都遵循相同接口的情况下, 构造结果可以直接通过主管类获取。 否则, 客户端应当通过生成器获取构造结果。

原型 - Clone, Prototype

原型模式使得能够复制已有对象而又无需使代码依赖他们所属的类

现实问题

我们有一个对象并且希望生成一个完全相同的复制品。一个可行的办法是创建个属于相同类的对象然后循环复制所有的原始对象的成员变量。

但是这个的问题在于,我们会有一些私有成员变量而这些在对象本身以外是不可见的

还有另外一个问题。 因为你必须知道对象所属的类才能创建复制品, 所以代码必须依赖该类。 即使你可以接受额外的依赖性, 那还有另外一个问题: 有时你只知道对象所实现的接口, 而不知道其所属的具体类, 比如可向方法的某个参数传入实现了某个接口的任何对象。

解决方案

通过原型模式我们可以将克隆过程委派给被克隆的实际对象。模式为所有支持克隆的对象声明了一个通用接口这个接口使得我们能够克隆对象同时又无需将代码和对象所属类耦合。通常情况下,这样的接口中仅包含一个克隆方法,

所有的类对 克隆 这个方法的实现都非常类似。这个方法会:创建一个当前类的对象,然后将原始对象所有的成员变量值复制到新建的类中。这样我们就可以复制私有成员变量。

支持克隆的对象被称为原型

当对象有十几个成员变量和几百种类型时,我们可以创建一系列不同的类型的对象并用不同的方式对其进行配置。如果所需对象与预先配置的对象相同。直接克隆原型即可。

结构

基本实现

prototype_1

  1. 原型 (Pro­to­type) 接口将对克隆方法进行声明。 在绝大多数情况下, 其中只会有一个名为 clone克隆的方法。
  2. 具体原型 (Con­crete Pro­to­type) 类将实现克隆方法。 除了将原始对象的数据复制到克隆体中之外, 该方法有时还需处理克隆过程中的极端情况, 例如克隆关联对象和梳理递归依赖等等。
  3. 客户端 (Client) 可以复制实现了原型接口的任何对象。

原型注册表实现

prototype_2

原型注册表 (Pro­to­type Reg­istry) 提供了一种访问常用原型的简单方法, 其中存储了一系列可供随时复制的预生成对象。 最简单的注册表原型是一个 名称 → 原型的哈希表。 但如果需要使用名称以外的条件进行搜索, 你可以创建更加完善的注册表版本。

样例

原型模式可以生成完全相同的几何对象副本,无需代码与对象所属类耦合

prototype_3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 基础原型。
abstract class Shape is
  field X: int
  field Y: int
  field color: string

  // 常规构造函数。
  constructor Shape() is
    // ...

  // 原型构造函数。使用已有对象的数值来初始化一个新对象。
  constructor Shape(source: Shape) is
    this()
    this.X = source.X
    this.Y = source.Y
    this.color = source.color

  // clone(克隆)操作会返回一个形状子类。
  abstract method clone():Shape


// 具体原型。克隆方法会创建一个新对象并将其传递给构造函数。直到构造函数运
// 行完成前,它都拥有指向新克隆对象的引用。因此,任何人都无法访问未完全生
// 成的克隆对象。这可以保持克隆结果的一致。
class Rectangle extends Shape is
  field width: int
  field height: int
constructor Rectangle(source: Rectangle) is
// 需要调用父构造函数来复制父类中定义的私有成员变量。
super(source)
this.width = source.width
this.height = source.height

  method clone():Shape is
    return new Rectangle(this)


class Circle extends Shape is
  field radius: int

  constructor Circle(source: Circle) is
    super(source)
    this.radius = source.radius

  method clone():Shape is
    return new Circle(this)


// 客户端代码中的某个位置。
class Application is
  field shapes: array of Shape

  constructor Application() is
    Circle circle = new Circle()
    circle.X = 10
    circle.Y = 10
    circle.radius = 20
    shapes.add(circle)

    Circle anotherCircle = circle.clone()
    shapes.add(anotherCircle)
    // 变量 `anotherCircle(另一个圆)`与 `circle(圆)`对象的内
    // 容完全一样。

    Rectangle rectangle = new Rectangle()
    rectangle.width = 10
    rectangle.height = 20
    shapes.add(rectangle)

  method businessLogic() is
    // 原型是很强大的东西,因为它能在不知晓对象类型的情况下生成一个与
    // 其完全相同的复制品。
    Array shapesCopy = new Array of Shapes.

    // 例如,我们不知晓形状数组中元素的具体类型,只知道它们都是形状。
    // 但在多态机制的帮助下,当我们在某个形状上调用 `clone(克隆)`
    // 方法时,程序会检查其所属的类并调用其中所定义的克隆方法。这样,
    // 我们将获得一个正确的复制品,而不是一组简单的形状对象。
    foreach (s in shapes) do
      shapesCopy.add(s.clone())

    // `shapesCopy(形状副本)`数组中包含 `shape(形状)`数组所有
    // 子元素的复制品.

使用场景

  • 需要复制一些对象,同时希望代码独立于这些对象所属的具体类
    • 这一点考量通常出现在代码需要处理第三方代码通过接口传递过来的对象时。 即使不考虑代码耦合的情况, 你的代码也不能依赖这些对象所属的具体类, 因为你不知道它们的具体信息。
      原型模式为客户端代码提供一个通用接口, 客户端代码可通过这一接口与所有实现了克隆的对象进行交互, 它也使得客户端代码与其所克隆的对象具体类独立开来。实现方法
  • 如果子类的区别仅在于其对象的初始化方式, 那么你可以使用该模式来减少子类的数量
    • 在原型模式中, 你可以使用一系列预生成的、 各种类型的对象作为原型。
    • 客户端不必根据需求对子类进行实例化, 只需找到合适的原型并对其进行克隆即可。

实现方法

  1. 创建 Prototype接口,并在其中加入clone方法。
  2. 原型类必须另行定义一个以该类对象为参数的构造函数构造函数必须复制参数对象中的所有成员变量值到新建实体中。 如果你需要修改子类, 则必须调用父类构造函数, 让父类复制其私有成员变量值。
    如果编程语言不支持方法重载, 那么你可能需要定义一个特殊方法来复制对象数据。 在构造函数中进行此类处理比较方便, 因为它在调用 new运算符后会马上返回结果对象。
  3. 克隆方法通常只有一行代码: 使用 new运算符调用原型版本的构造函数。 注意, 每个类都必须显式重写克隆方法并使用自身类名调用 new运算符。 否则, 克隆方法可能会生成父类的对象。
  4. 可以创建一个中心化原型注册表, 用于存储常用原型。
    可以新建一个工厂类来实现注册表, 或者在原型基类中添加一个获取原型的静态方法。 该方法必须能够根据客户端代码设定的条件进行搜索。 搜索条件可以是简单的字符串, 或者是一组复杂的搜索参数。 找到合适的原型后, 注册表应对原型进行克隆, 并将复制生成的对象返回给客户端。
    最后还要将对子类构造函数的直接调用替换为对原型注册表工厂方法的调用。

单例模式 - Singleton

单例是一种能够保证一个类只有一个实例并且提供一个方位该实例的全局节点

现实问题

由于单例模式同时解决了两个问题,所以他其实「违反了」「单一指责原则」

  1. 保证一个类只有一个实例。为什么?一个可能的原因是想要控制某些共享资源的访问权限:

    1. 如果创建了一个对象,如果过一会决定再创建一个新对象,那么其实获得的是之前已经创建的对象,而不是一个新对象。

    2. singleton_1

    3. 假设有一个应用,需要频繁地访问数据库。如果每次访问数据库时都创建一个新的数据库连接,将会非常耗时且低效,因为建立数据库连接是一个资源密集型和时间密集型的操作。此外,同时打开过多的连接可能会超过数据库的连接限制,导致新的连接失败。

      使用单例模式,你可以创建一个DatabaseConnection类,它负责与数据库建立连接。这个类将被设计为单例,确保应用中的任何组件都使用相同的数据库连接实例,从而有效管理对数据库的访问。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      public class DatabaseConnection {
      private static DatabaseConnection instance;
      private Connection connection;

      private DatabaseConnection() {
      // 初始化数据库连接
      this.connection = // 创建数据库连接
      }

      public static DatabaseConnection getInstance() {
      if (instance == null) {
      synchronized (DatabaseConnection.class) {
      if (instance == null) {
      instance = new DatabaseConnection();
      }
      }
      }
      return instance;
      }

      public Connection getConnection() {
      return this.connection;
      }
      }
  2. 为该实例提供一个全局访问的节点

    1. 单例模式允许程序的任何地方访问特定的对象但是它可以「保护该实例被其他代码覆盖」

    2. 考虑一个应用需要读取一些全局配置信息,如配置文件中的设置。使用单例模式,可以创建一个Configuration类,它加载并存储配置信息,应用中的任何部分都可以访问这些信息,而不需要重复加载。并且无法修改内部的内容,因此是安全的。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      public class Configuration {
      private static Configuration instance;
      private Properties properties;

      private Configuration() {
      this.properties = new Properties();
      // 加载配置文件
      }

      public static Configuration getInstance() {
      if (instance == null) {
      synchronized (Configuration.class) {
      if (instance == null) {
      instance = new Configuration();
      }
      }
      }
      return instance;
      }

      public String getProperty(String key) {
      return this.properties.getProperty(key);
      }
      }

解决方案

所有的单例的实现都包含下面两个相同的步骤:

  • 设置默认的构造函数为「私有」,防止其他对象使用单例类的new运算符

  • 新建一个静态构建方法作为构造函数。该函数会调用私有的构造函数来创建对象并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这个缓存对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Singleton {
    // 静态成员变量,存储单例实例
    private static Singleton instance;

    // 私有构造函数,防止外部通过new直接实例化
    private Singleton() {} // <- 注意他是private的

    // 公开的静态方法,用于获取单例实例
    public static Singleton getInstance() {
    // 如果实例不存在,则创建实例
    if (instance == null) {
    instance = new Singleton();
    }
    // 返回实例
    return instance;
    }
    }

    根据代码不难看出,只要调用了这个类里面的getInstance,它总是返回已经创建的相同的对象

真实世界类比

比如政府,一个国家只有一个官方政府。不论组成政府的每个人的身份是什么,这个XX政府这个称谓总是鉴别那些掌权者的全局访问节点

结构

singleton_2

getInstance 来返回其所属类的一个相同实例。单例的构造函数必须对客户端代码隐藏。客户端只通过getInstance的方法来获取对象

代码样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 数据库类会对`getInstance(获取实例)`方法进行定义以让客户端在程序各处
// 都能访问相同的数据库连接实例。
class Database is
  // 保存单例实例的成员变量必须被声明为静态类型。
  private static field instance: Database

  // 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法。
private constructor Database() is
  // 部分初始化代码(例如到数据库服务器的实际连接)。
  // ...

  // 用于控制对单例实例的访问权限的静态方法。
  public static method getInstance() is
    if (Database.instance == null) then
      acquireThreadLock() and then
        // 确保在该线程等待解锁时,其他线程没有初始化该实例。
        if (Database.instance == null) then
          Database.instance = new Database()
    return Database.instance

  // 最后,任何单例都必须定义一些可在其实例上执行的业务逻辑。
  public method query(sql) is
    // 比如应用的所有数据库查询请求都需要通过该方法进行。因此,你可以
    // 在这里添加限流或缓冲逻辑。
    // ...

class Application is
  method main() is
    Database foo = Database.getInstance()
    foo.query("SELECT ...")
    // ...
    Database bar = Database.getInstance()
    bar.query("SELECT ...")
    // 变量 `bar` 和 `foo` 中将包含同一个对象。

使用场景

  • 如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。
    • 单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。
  • 如果你需要更加严格地控制全局变量, 可以使用单例模式。
    • 单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。
  • 我们可以随时调整限制并设定生成单例实例的数量, 只需修改 getInstance 方法, 即 getInstance 中的代码即可实现。

实现方式

  1. 在类中添加一个私有静态成员变量用于保存单例实例。
  2. 声明一个公有静态构建方法用于获取单例实例。
  3. 在静态方法中实现”延迟初始化”。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。
  4. 将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。
  5. 检查客户端代码, 将对单例的构造函数的调用替换为对其静态构建方法的调用。

与其他模式的关系

  • 外观 (Facade) 类通常可以转换为单例类, 因为在大部分情况下一个外观对象就足够了。
  • 如果你能将对象的所有共享状态简化为一个享元(cache,缓存)对象, 那么享元就和单例类似了。 但这两个模式有两个根本性的不同。
    • 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
    • 单例对象可以是可变的。 享元对象是不可变的。
    • 抽象工厂、 生成器和原型都可以用单例来实现。

创建型设计模式总结

创建型模式主要处理对象创建机制,帮助创建对象时保持系统的灵活性和可维护性。

  • 单例模式:当需要确保全局只有一个实例存在,并且提供一个访问它的全局访问点时使用。

    例子:数据库连接池的唯一性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class DatabaseConnectionPool {
    private static DatabaseConnectionPool instance = new DatabaseConnectionPool();

    private DatabaseConnectionPool() {
    // 私有构造器,防止外部直接创建实例
    }

    public static DatabaseConnectionPool getInstance() {
    return instance;
    }

    public Connection getConnection() {
    // 返回数据库连接
    }
    }
  • 工厂方法抽象工厂:当创建对象时需要考虑系统的灵活性和可扩展性,而不是直接实例化类时使用。工厂方法用于创建一个产品,抽象工厂提供一个接口来创建一系列相关或相互依赖的对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    // 按钮接口
    public interface Button {
    void render();
    }

    // Windows按钮实现
    public class WindowsButton implements Button {
    public void render() {
    System.out.println("Rendering Windows button");
    }
    }

    // MacOS按钮实现
    public class MacOSButton implements Button {
    public void render() {
    System.out.println("Rendering MacOS button");
    }
    }

    // 按钮工厂接口
    public abstract class ButtonFactory {
    abstract Button createButton(); // 工厂方法

    public void renderButton() {
    Button button = createButton();
    button.render();
    }
    }

    // Windows按钮工厂
    public class WindowsButtonFactory extends ButtonFactory {
    Button createButton() {
    return new WindowsButton();
    }
    }

    // MacOS按钮工厂
    public class MacOSButtonFactory extends ButtonFactory {
    Button createButton() {
    return new MacOSButton();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    // 按钮和文本框的接口
    public interface Button {
    void render();
    }

    public interface TextField {
    void render();
    }

    // Windows和MacOS的按钮实现
    public class WindowsButton implements Button {
    public void render() {
    System.out.println("Rendering Windows button");
    }
    }

    public class MacOSButton implements Button {
    public void render() {
    System.out.println("Rendering MacOS button");
    }
    }

    // Windows和MacOS的文本框实现
    public class WindowsTextField implements TextField {
    public void render() {
    System.out.println("Rendering Windows text field");
    }
    }

    public class MacOSTextField implements TextField {
    public void render() {
    System.out.println("Rendering MacOS text field");
    }
    }

    // 抽象工厂接口
    public interface GUIFactory {
    Button createButton();
    TextField createTextField();
    }

    // 具体工厂实现
    public class WindowsFactory implements GUIFactory {
    public Button createButton() {
    return new WindowsButton();
    }

    public TextField createTextField() {
    return new WindowsTextField();
    }
    }

    public class MacOSFactory implements GUIFactory {
    public Button createButton() {
    return new MacOSButton();
    }

    public TextField createTextField() {
    return new MacOSTextField();
    }
    }
    • 工厂方法模式着重于创建单一产品(在这个例子中是按钮)。每个具体的工厂类负责创建单一产品的一个具体实现。
    • 抽象工厂模式提供一个创建一系列相关或相互依赖产品的接口(在这个例子中是按钮和文本框)。每个具体的工厂类负责创建一系列产品的一组具体实现。
    工厂方法模式
    • 设计目的:让子类决定实例化哪一个类。工厂方法模式让类的实例化推迟到子类中进行。
    • 应用场景:当一个类不知道它所必须创建的对象的类的时候;当一个类希望由其子类来指定它创建的对象时;当类将创建对象的职责委托给多个帮助子类中的某一个,并且你想将哪一个帮助子类是代理者这一信息局部化时。
    • 关键实现:通过继承来改变实例化的类。子类实现抽象工厂方法以创建具体产品。
    抽象工厂模式
    • 设计目的:提供一个接口,用于创建相关或相互依赖对象的家族,而不需要明确指定具体类。
    • 应用场景:当需要创建的对象是一系列相关或相互依赖的对象时;当一个系统要独立于它的产品的创建、组合和表示时;当强调一系列相关的产品对象的设计以便进行联合使用时;当提供一个产品类库,只想显示它们的接口而不是实现时。
    • 关键实现:通过对象组合来创建抽象产品的家族。一个工厂实例创建一系列相互依赖的产品。
    核心区别
    • 产品范围:工厂方法模式通常用于一个产品等级结构,而抽象工厂模式用于多个产品等级结构。
    • 控制范围:工厂方法通过继承来改变实例化哪个类,控制的是生产单一产品的逻辑;抽象工厂通过对象组合来创建一系列产品,控制的是生产多个相互依赖的一系列产品的逻辑。
    • 实现方式:工厂方法模式依赖于继承,具体产品由子类创建;抽象工厂模式依赖于接口的实现,具体产品的创建委托给了具体工厂的实例。
  • 建造者模式:当需要创建一个包含多个组成部分的复杂对象时,尤其是当对象的创建过程需要多个步骤,且构造过程需要被细化时使用。

    构建复杂的HTML文档或SQL查询

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class HtmlDocument {
    private String title;
    private List<String> paragraphs = new ArrayList<>();

    public void setTitle(String title) {
    this.title = title;
    }

    public void addParagraph(String paragraph) {
    paragraphs.add(paragraph);
    }

    public String build() {
    // 将标题和段落组合成HTML文档
    }
    }

    public class HtmlDocumentBuilder {
    private HtmlDocument document;

    public HtmlDocumentBuilder() {
    document = new HtmlDocument();
    }

    public HtmlDocumentBuilder withTitle(String title) {
    document.setTitle(title);
    return this;
    }

    public HtmlDocumentBuilder addParagraph(String paragraph) {
    document.addParagraph(paragraph);
    return this;
    }

    public HtmlDocument build() {
    return document;
    }
    }
  • 原型模式:当创建一个对象的成本较高,且与其类型相似的对象可能会经常使用时,可以通过复制一个已存在对象来减少创建对象的开销。

    • 原型模式的目的是通过复制现有的实例来创建新的实例,避免了新实例创建的成本和复杂性。原型模式允许对象在不指定具体类的情况下创建副本。这在需要创建对象的状态与现有对象相似时非常有用。
    • 单例模式的目的是确保一个类只有一个实例,并提供一个全局访问点。单例模式主要用于全局状态或共享资源的情况,如配置管理器或数据库连接池。在单例模式中,实例是唯一的,且全局可访问。

    考虑一个后端开发中的场景,如在一个Web应用中管理配置信息的示例。假设你有一个配置对象,它在应用启动时从外部服务加载配置数据,这个过程可能包括网络请求获取数据库连接字符串、API密钥、日志设置等。这个配置对象在应用的不同部分被频繁访问,但是其初始化非常昂贵,因为它涉及到网络请求和数据解析。

    不使用原型模式的情况

    每次你需要配置信息时,都去创建一个新的配置对象并从外部服务加载数据。这不仅增加了网络负载,还可能因为外部服务的响应延迟而显著增加了应用的启动时间。

    使用原型模式的情况

    在应用启动时创建一次配置对象,并通过原型模式在需要时克隆它。由于配置数据通常在应用运行期间不会改变,所以这个方法可以避免重复的初始化成本。

    在这个例子中,使用原型模式可以显著减少从外部服务加载配置信息的次数,因为:

    • 初始化成本:原始配置对象的加载包括网络请求和数据解析,这是一次性的,并在首次创建对象时完成。

    • 克隆成本:通过克隆原始对象来创建新的配置对象实例,避免了重复的网络请求和数据解析。克隆操作主要是内存中的数据复制,其成本远低于外部服务的调用。

    • 运行效率:应用启动和运行过程中对配置数据的访问变得更快,因为避免了重复的耗时操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
      public class Configuration implements Cloneable {
    private Map<String, String> settings;

    public Configuration() {
    this.settings = new HashMap<>();
    // 假设这里包含从外部服务加载配置的耗时操作
    loadConfigurationFromExternalService();
    }

    private void loadConfigurationFromExternalService() {
    // 模拟网络请求和设置加载
    settings.put("dbConnectionString", "someConnectionString");
    settings.put("apiKey", "someAPIKey");
    // 更多配置...
    }

    public String getSetting(String key) {
    return settings.get(key);
    }

    @Override
    public Configuration clone() {
    try {
    // 浅克隆足够了,因为我们不修改settings内部的数据
    return (Configuration) super.clone();
    } catch (CloneNotSupportedException e) {
    throw new AssertionError(); // 不应该发生
    }
    }
    }

    // 调用
    public class Application {
    public static void main(String[] args) {
    Configuration originalConfig = new Configuration();
    Configuration clonedConfig = originalConfig.clone();

    System.out.println(clonedConfig.getSetting("dbConnectionString"));
    System.out.println(clonedConfig.getSetting("apiKey"));
    // 输出与originalConfig加载的相同的配置项,但没有重新从外部服务加载
    }
    }

    # Structural Pattern

    ## 适配器 - Adapter Pattern

    > 适配器是一种结构性设计模式,它能使接口不兼容的对象相互合作

    使一个老的方案通过`adapter`来实现新的方案

    ![adapter_guru_1](/2023/10/24/Design-Pattern/adapter_guru_1.png)

    ### 现实问题

    当想要整合老旧方案到一个新的方案时,在不修改老方案的内容的情况下,使用adapter:

    A special object that converts the interface of one object so that another object can understand it.

    假如正在开发一款股票市场监测程序,它会从不同来源下载XML格式的股票数据,然后像用户呈现图表。我们现在需要整合一个第三方的分析函数库。但是这个库只兼容JSON格式的数据。

    一个可能的解决方案有:修改程序库使其支持XML。但是这样就需要修改依赖这个程序库的现有代码。或者我们压根可能对这个程序库没有access。

    ![adapater_1](/2023/10/24/Design-Pattern/adapater_1.png)

    ### 解决方案

    此时就需要一个适配器,它是一个特殊的对象,主要目的是转换对象借口,使其能够与其他对象进行交互。

    适配器模式通过封装对象从而将一个复杂的转换过程隐藏起来。被封装的对象甚至不知道有适配器这个东西。

    适配器不仅可以转化不同格式的数据,还有助于采用不同接口的对象之间合作:

    1. 适配器实现与其中一个现有对象兼容的接口。
    2. 现有对象可以使用该接口安全地调用适配器方法。
    3. 适配器方法被调用后将以另一个对象兼容的格式和顺序将请求传递给该对象。

    甚至双向适配器也是可以的这样就可以双向转换调用

    ![adapter_2](/2023/10/24/Design-Pattern/adapter_2.png)

    对于先前的股票市场问题,为分析函数库中的每个类创建将 XML 转换为 JSON 格式的适配器, 然后让客户端仅通过这些适配器来与函数库进行交流。 当某个适配器被调用时, 它会将传入的 XML 数据转换为 JSON 结构, 并将其传递给被封装分析对象的相应方法。

    ### 结构

    #### 对象适配器:

    适配器实现其冲一个对象的借口,并对另一个对象进行封装。

    ![adapter_3](/2023/10/24/Design-Pattern/adapter_3.png)

    1. 客户端 (Client) 是包含当前程序业务逻辑的类。
    2. 客户端接口 (Client Inter­face) 描述了其他类与客户端代码合作时必须遵循的协议。
    3. 服务 (Ser­vice) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能。
    4. 适配器 (Adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时**封装了服务对象**。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。
    5. 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。

    #### 类适配器

    主要运用了继承机制,适配器同时继承了两个对象的接口(所以需要支持多重继承的语言 比如,C++)

    ![adapter_4](/2023/10/24/Design-Pattern/adapter_4.png)

    1. 类适配器不需要封装任何对象, 因为它同时继承了客户端和服务的行为。 适配功能在重写的方法中完成。 最后生成的适配器可替代已有的客户端类进行使用。

    ### 代码样例

    ![adapter_5](/2023/10/24/Design-Pattern/adapter_5.png)

    适配器假扮成一个圆钉 (Round­Peg), 其半径等于方钉 (Square­Peg) 横截面对角线的一半 (即能够容纳方钉的最小外接圆的半径)。

    ```java
    // 假设你有两个接口相互兼容的类:圆孔(Round­Hole)和圆钉(Round­Peg)。
    class RoundHole is
      constructor RoundHole(radius) { ... }

      method getRadius() is
        // 返回孔的半径。

      method fits(peg: RoundPeg) is
        return this.getRadius() >= peg.getRadius()

    class RoundPeg is
      constructor RoundPeg(radius) { ... }

      method getRadius() is
        // 返回钉子的半径。


    // 但还有一个不兼容的类:方钉(Square­Peg)。
    class SquarePeg is
      constructor SquarePeg(width) { ... }

      method getWidth() is
        // 返回方钉的宽度。


    // 适配器类让你能够将方钉放入圆孔中。它会对 RoundPeg 类进行扩展,以接收适
    // 配器对象作为圆钉。
    class SquarePegAdapter extends RoundPeg is
      // 在实际情况中,适配器中会包含一个 SquarePeg 类的实例。
      private field peg: SquarePeg

      constructor SquarePegAdapter(peg: SquarePeg) is
        this.peg = peg

      method getRadius() is
        // 适配器会假扮为一个圆钉,
        // 其半径刚好能与适配器实际封装的方钉搭配起来。
        return peg.getWidth() * Math.sqrt(2) / 2


    // 客户端代码中的某个位置。
    hole = new RoundHole(5)
    rpeg = new RoundPeg(5)
    hole.fits(rpeg) // true

    small_sqpeg = new SquarePeg(5)
    large_sqpeg = new SquarePeg(10)
    hole.fits(small_sqpeg) // 此处无法编译(类型不一致)。

    small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
    large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
    hole.fits(small_sqpeg_adapter) // true
    hole.fits(large_sqpeg_adapter) // false

使用场景

当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。

适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。

实现方式

  1. 确保至少有两个类的接口不兼容:
    • 一个无法修改 (通常是第三方、 遗留系统或者存在众多已有依赖的类) 的功能性服务类。
    • 一个或多个将受益于使用服务类的客户端类。
  2. 声明客户端接口, 描述客户端如何与服务交互。
  3. 创建遵循客户端接口的适配器类。 所有方法暂时都为空。
  4. 在适配器类中添加一个成员变量用于保存对于服务对象的引用。 通常情况下会通过构造函数对该成员变量进行初始化, 但有时在调用其方法时将该变量传递给适配器会更方便。
  5. 依次实现适配器类客户端接口的所有方法。 适配器会将实际工作委派给服务对象, 自身只负责接口或数据格式的转换。
  6. 客户端必须通过客户端接口使用适配器。 这样一来, 你就可以在不影响客户端代码的情况下修改或扩展适配器。

与其他模式的关系

桥接通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, 适配器通常在已有程序中使用, 让相互不兼容的类能很好地合作。

适配器可以对已有对象的接口进行修改, 装饰则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。

适配器能为被封装对象提供不同的接口, 代理能为对象提供相同的接口, 装饰则能为对象提供加强的接口。

外观为现有对象定义了一个新接口, 适配器则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。

桥接、 状态和策略 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。

桥接 - Bridge

桥接是一种结构型设计模式,可将一个「大类」或「一系列紧密相关的类」「拆分」为「抽象」和「实现」两个独立的层次结构,从而能在开发时分别使用。

现实问题

假如你有一个几何 「形状Shape类」, 从它能扩展出两个子类: 「圆形Cir­cle」 和 「方形Square」。 你希望对这样的类层次结构进行扩展以使其包含颜色, 所以你打算创建名为「红色Red」和「蓝色Blue」的形状子类。 但是, 由于你已有两个子类, 所以总共需要创建四个类才能覆盖所有组合, 例如「蓝色圆形Blue­Cir­cle」和「红色方形Red­Square」

bridge_1

在层次结构中新增形状和颜色将导致代码复杂程度指数增长。 例如添加三角形状, 你需要新增两个子类, 也就是每种颜色一个; 此后新增一种新颜色需要新增三个子类, 即每种形状一个。

解决方案

这个问题主要是由于「形状」以及「颜色」其实是两个维度的问题。但是放在一个形状类中来进行扩展。而这是继承时容易导致的问题。

桥接模式通过将「继承」改为「组合」的方式来解决问题

即抽取其中一个维度并使之成为独立的类层次,这样就可以在初始类中引用这个新层次的对象从而使得一个类中不必有所有的状态和行为。

bridge_2

图示中,我们把颜色相关的代码抽取到红色和蓝色的两个子类的颜色中。然后形状类中添加一个只想某一颜色对象的引用成员变量。那么现在形状类可以将所有与颜色有关的行为交给颜色对象,这种引用就是「形状」和「颜色」之间的桥梁。

抽象 和 实现

抽象部分 - 一般指接口,是一些实体的高阶控制层。该层本身不完成任何「具体」的工作,它需要将工作委派给「实现部分」层

这里的和语言里面的interface 或者 abstract class 没有关系

实际程序中抽象部分是图像用户界面(GUI),而实现部分则是底层操作系统代码(API),GUI层调用API层来对用户的各种操作做出回应。

一般来说, 你可以在两个独立方向上扩展这种应用:

  • 开发多个不同的 GUI (例如面向普通用户和管理员进行分别配置)
  • 支持多个不同的 API (例如, 能够在 Win­dows、 Linux 和 macOS 上运行该程序)。

在最糟糕的情况下, 程序可能会是一团乱麻, 其中包含数百种条件语句, 连接着代码各处不同种类的 GUI 和各种 API。

bridge_3

你可以将特定接口-平台的组合代码抽取到独立的类中, 以在混乱中建立一些秩序。 但是, 你很快会发现这种类的数量很多。 类层次将以指数形式增长, 因为每次添加一个新的 GUI 或支持一种新的 API 都需要创建更多的类。
让我们试着用桥接模式来解决这个问题。 该模式建议将类拆分为两个类层次结构:

抽象部分: 程序的 GUI 层。
实现部分: 操作系统的 API。

bridge_4

抽象对象控制程序的外观, 并将真实工作委派给连入的实现对象。 不同的实现只要遵循相同的接口就可以互换, 使同一GUI 可在 Win­dows 和 Linux 下运行。

最后的结果是: 你无需改动与 API 相关的类就可以修改 GUI 类。 此外如果想支持一个新的操作系统, 只需在实现部分层次中创建一个子类即可。

例如,在一个软件系统中,如果你有一个功能是需要在不同的操作系统(如Windows和Linux)上运行,并且每个操作系统都有自己的GUI接口。如果你直接在应用程序的主体中写入所有的GUI代码,那么这会导致代码与平台强绑定,难以维护。

为了解决这个问题,你可以将与操作系统相关的GUI部分抽象出来,创建独立的类或者接口来管理这些与平台相关的代码。这样,你的主应用程序就不需要直接处理不同操作系统的GUI细节,而是通过这些抽象的接口来与它们通信。当需要支持新的操作系统时,你只需要添加或修改这些独立的类即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 定义一个GUI接口,描述所有操作系统都会实现的GUI操作
interface GUI {
void drawButton();
void drawWindow();
}

// 为Windows实现GUI接口
class WindowsGUI implements GUI {
public void drawButton() {
System.out.println("绘制Windows风格的按钮");
}

public void drawWindow() {
System.out.println("绘制Windows风格的窗口");
}
}

// 为Linux实现GUI接口
class LinuxGUI implements GUI {
public void drawButton() {
System.out.println("绘制Linux风格的按钮");
}

public void drawWindow() {
System.out.println("绘制Linux风格的窗口");
}
}

// 主应用程序,不直接依赖于任何特定的操作系统GUI实现
class Application {
private GUI gui;

public Application(GUI gui) {
this.gui = gui;
}

public void draw() {
gui.drawButton();
gui.drawWindow();
}
}

// 客户端代码,选择适当的GUI实现
public class Main {
public static void main(String[] args) {
GUI windowsGUI = new WindowsGUI();
Application app = new Application(windowsGUI);
app.draw();

// 如果需要更换到Linux GUI,只需要更换GUI实现
GUI linuxGUI = new LinuxGUI();
app = new Application(linuxGUI);
app.draw();
}
}

结构

bridge_5

  1. 抽象部分 (Abstrac­tion) 提供高层控制逻辑, 依赖于完成底层实际工作的实现对象。
  2. 实现部分 (Imple­men­ta­tion) 为所有具体实现声明通用接口。 抽象部分仅能通过在这里声明的方法与实现对象交互。
    抽象部分可以列出和实现部分一样的方法, 但是抽象部分通常声明一些复杂行为, 这些行为依赖于多种由实现部分声明的原语操作。
  3. 具体实现 (Con­crete Imple­men­ta­tions) 中包括特定于平台的代码。
  4. 精确抽象 (Refined Abstrac­tion) 提供控制逻辑的变体。 与其父类一样, 它们通过通用实现接口与不同的实现进行交互。
  5. 通常情况下, 客户端 (Client) 仅关心如何与抽象部分合作。 但是, 客户端需要将抽象对象与一个实现对象连接起来。

从您提供的图片中,我们可以看到桥接模式的结构,它包含以下部分:

  1. 抽象化 (Abstraction): 定义高层的接口,它依赖于实现化角色提供的接口进行工作。这是一个核心的角色,它包含对实现化角色的引用。
  2. 实现化 (Implementation): 是接口或抽象类,定义了实现化角色必须实现的接口,但不负责具体的实现。
  3. 具体实现化 (Concrete Implementations): 实现化角色的具体实现,根据实现化接口来提供具体的操作方法。
  4. 改进抽象化 (Refined Abstraction): 是抽象化的子类,扩展或完善了父类定义的方法和属性。
  5. 客户端 (Client): 使用抽象化角色定义的接口操作。

我们可以将 Application 类看作是抽象化角色,GUI 接口是实现化角色,而 WindowsGUILinuxGUI 类是具体实现化角色。要更明确地映射到桥接模式,具体来说,我们可以创建一个 RefinedGUI 类,它继承自 GUI 接口并添加一些特定的功能。同时,我们可以创建一个客户端类来直接与 Application 类交互。以下是调整后的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 抽象化角色
abstract class ApplicationGUI {
protected GUI gui;

protected ApplicationGUI(GUI gui) {
this.gui = gui;
}

abstract void draw();
}

// 改进抽象化角色
class RefinedApplicationGUI extends ApplicationGUI {
protected RefinedApplicationGUI(GUI gui) {
super(gui);
}

void draw() {
// 使用GUI接口提供的方法
gui.drawButton();
gui.drawWindow();
}

// 可以添加一些改进的操作方法
void drawMenu() {
System.out.println("使用 " + gui.getClass().getSimpleName() + " 绘制菜单");
}
}

// 实现化角色
interface GUI {
void drawButton();
void drawWindow();
}

// 具体实现化角色
class WindowsGUI implements GUI {
public void drawButton() {
System.out.println("绘制Windows风格的按钮");
}

public void drawWindow() {
System.out.println("绘制Windows风格的窗口");
}
}

class LinuxGUI implements GUI {
public void drawButton() {
System.out.println("绘制Linux风格的按钮");
}

public void drawWindow() {
System.out.println("绘制Linux风格的窗口");
}
}

// 客户端角色
public class Client {
public static void main(String[] args) {
// 客户端决定使用的具体实现化角色
GUI windowsGUI = new WindowsGUI();
ApplicationGUI appGUI = new RefinedApplicationGUI(windowsGUI);
appGUI.draw();
appGUI.drawMenu();

// 可以轻松切换到Linux GUI
GUI linuxGUI = new LinuxGUI();
appGUI = new RefinedApplicationGUI(linuxGUI);
appGUI.draw();
appGUI.drawMenu();
}
}

在这个调整后的示例中,RefinedApplicationGUI 是一个改进的抽象化角色,它扩展了 ApplicationGUI 抽象化角色的功能。Client 类则充当了客户端角色,它决定使用哪个具体实现化角色,并通过改进的抽象化角色来使用这些功能。这样的结构更清晰地展现了桥接模式的所有组成部分。

代码样例

bridge_6

「设备Device类」作为实现部分, 而「遥控器Remote类」则作为抽象部分。

遥控器基类声明了一个指向设备对象的引用成员变量。 所有遥控器通过通用设备接口与设备进行交互, 使得同一个遥控器可以支持不同类型的设备。
我们可以开发独立于设备类的遥控器类, 只需新建一个遥控器子类即可。 例如, 基础遥控器可能只有两个按钮, 但你可在其基础上扩展新功能, 比如额外的一节电池或一块触摸屏。
客户端代码通过遥控器构造函数将特定种类的遥控器与设备对象连接起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// “抽象部分”定义了两个类层次结构中“控制”部分的接口。它管理着一个指向“实
// 现部分”层次结构中对象的引用,并会将所有真实工作委派给该对象。
class RemoteControl is
  protected field device: Device
  constructor RemoteControl(device: Device) is
    this.device = device
  method togglePower() is
    if (device.isEnabled()) then
      device.disable()
    else
      device.enable()
  method volumeDown() is
    device.setVolume(device.getVolume() - 10)
  method volumeUp() is
    device.setVolume(device.getVolume() + 10)
  method channelDown() is
    device.setChannel(device.getChannel() - 1)
  method channelUp() is
    device.setChannel(device.getChannel() + 1)


// 你可以独立于设备类的方式从抽象层中扩展类。
class AdvancedRemoteControl extends RemoteControl is
  method mute() is
    device.setVolume(0)


// “实现部分”接口声明了在所有具体实现类中通用的方法。它不需要与抽象接口相
// 匹配。实际上,这两个接口可以完全不一样。通常实现接口只提供原语操作,而
// 抽象接口则会基于这些操作定义较高层次的操作。
interface Device is
  method isEnabled()
  method enable()
  method disable()
  method getVolume()
  method setVolume(percent)
  method getChannel()
  method setChannel(channel)


// 所有设备都遵循相同的接口。
class Tv implements Device is
  // ...

class Radio implements Device is
// ...
// 客户端代码中的某个位置。
tv = new Tv()
remote = new RemoteControl(tv)
remote.togglePower()

radio = new Radio()
remote = new AdvancedRemoteControl(radio)

应用场景

  • 如果你想要拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类), 可以使用桥接模式。

    • 类的代码行数越多, 弄清其运作方式就越困难, 对其进行修改所花费的时间就越长。 一个功能上的变化可能需要在整个类范围内进行修改, 而且常常会产生错误, 甚至还会有一些严重的副作用。
    • 桥接模式可以将庞杂类拆分为几个类层次结构。 此后, 你可以修改任意一个类层次结构而不会影响到其他类层次结构。 这种方法可以简化代码的维护工作, 并将修改已有代码的风险降到最低。
  • 如果你希望在几个独立维度上扩展一个类, 可使用该模式。

    • 桥接建议将每个维度抽取为独立的类层次。 初始类将相关工作委派给属于对应类层次的对象, 无需自己完成所有工作。
  • 如果你需要在运行时切换不同实现方法, 可使用桥接模式。

    • 当然并不是说一定要实现这一点, 桥接模式可替换抽象部分中的实现对象, 具体操作就和给成员变量赋新值一样简单。

实现方式

  1. 明确类中独立的维度。 独立的概念可能是: 抽象/平台, 域/基础设施, 前端/后端或接口/实现。
  2. 了解客户端的业务需求, 并在抽象基类中定义它们。
  3. 确定在所有平台上都可执行的业务。 并在通用实现接口中声明抽象部分所需的业务。
  4. 为你域内的所有平台创建实现类, 但需确保它们遵循实现部分的接口。
  5. 在抽象类中添加指向实现类型的引用成员变量。 抽象部分会将大部分工作委派给该成员变量所指向的实现对象。
  6. 如果你的高层逻辑有多个变体, 则可通过扩展抽象基类为每个变体创建一个精确抽象。
  7. 客户端代码必须将实现对象传递给抽象部分的构造函数才能使其能够相互关联。 此后, 客户端只需与抽象对象进行交互, 无需和实现对象打交道。

与其他模式的关系

  • 桥接通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, 适配器通常在已有程序中使用, 让相互不兼容的类能很好地合作。
  • 桥接、 状态和策略 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
  • 将抽象工厂和桥接搭配使用。 如果由桥接定义的抽象只能与特定实现合作, 这一模式搭配就非常有用。 在这种情况下, 抽象工厂可以对这些关系进行封装, 并且对客户端代码隐藏其复杂性。

假设我们有一个应用程序,它可以在不同的数据库上运行。我们的抽象部分是数据库的使用,而具体实现是对各种数据库的操作。

首先,我们定义一个数据库操作的桥接接口(Database)和它的一些实现(MySQLDatabase, PostgreSQLDatabase):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 桥接接口
interface Database {
void connect();
void execute(String query);
}

// 具体实现MySQL
class MySQLDatabase implements Database {
public void connect() {
System.out.println("连接到MySQL数据库");
}

public void execute(String query) {
System.out.println("在MySQL执行: " + query);
}
}

// 具体实现PostgreSQL
class PostgreSQLDatabase implements Database {
public void connect() {
System.out.println("连接到PostgreSQL数据库");
}

public void execute(String query) {
System.out.println("在PostgreSQL执行: " + query);
}
}

现在我们使用抽象工厂模式来封装数据库的创建过程,以便为客户端代码隐藏复杂性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 抽象工厂
abstract class DatabaseFactory {
abstract Database createDatabase();
}

// 具体工厂MySQL
class MySQLDatabaseFactory extends DatabaseFactory {
Database createDatabase() {
return new MySQLDatabase();
}
}

// 具体工厂PostgreSQL
class PostgreSQLDatabaseFactory extends DatabaseFactory {
Database createDatabase() {
return new PostgreSQLDatabase();
}
}

// 客户端代码
public class Client {
private Database database;

public Client(DatabaseFactory factory) {
database = factory.createDatabase();
}

public void start() {
database.connect();
database.execute("SELECT * FROM table");
}
}

在客户端代码中,我们不直接与具体的数据库实现打交道,而是通过抽象工厂提供的接口来创建和使用数据库。

  • 你可以结合使用生成器和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。

假设我们正在构建一个报告生成系统,其中报告的内容(抽象部分)可以独立于报告的格式(具体实现部分)。

首先,我们定义一个报告的桥接接口(Report)和它的一些实现(PDFReport, HTMLReport):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 桥接接口
interface Report {
void generate();
}

// 具体实现PDF
class PDFReport implements Report {
public void generate() {
System.out.println("生成PDF报告");
}
}

// 具体实现HTML
class HTMLReport implements Report {
public void generate() {
System.out.println("生成HTML报告");
}
}

接下来,我们使用生成器模式来封装报告的构建过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 报告生成器
abstract class ReportBuilder {
protected Report report;

ReportBuilder buildPartA() {
// 构建报告的一部分
return this;
}

ReportBuilder buildPartB() {
// 构建报告的另一部分
return this;
}

abstract Report build();
}

// 具体生成器PDF
class PDFReportBuilder extends ReportBuilder {
PDFReportBuilder() {
report = new PDFReport();
}

Report build() {
return report;
}
}

// 具体生成器HTML
class HTMLReportBuilder extends ReportBuilder {
HTMLReportBuilder() {
report = new HTMLReport();
}

Report build() {
return report;
}
}

// 主管类
class ReportDirector {
private ReportBuilder builder;

public ReportDirector(ReportBuilder builder) {
this.builder = builder;
}

public Report construct() {
return builder.buildPartA().buildPartB().build();
}
}

// 客户端代码
public class Client {
public static void main(String[] args) {
ReportBuilder builder = new PDFReportBuilder();
ReportDirector director = new ReportDirector(builder);
Report report = director.construct();
report.generate();
}
}

在这个例子中,ReportBuilder 是桥接模式的一个组成部分,它将报告的创建过程从具体的报告格式中抽象出来。ReportDirector 作为主管类,负责根据客户端的需求使用不同的生成器构建报告。这种方式允许我们独立于报告的具体实现来构建报告的内容。

组合 - 对象树, Object Tree, Composite

将对象组合成树状结构,并且像独立使用对立对象一样使用

现实问题

如果应用的核心模型可以用树状结构表示,在应用中使用组合模式才有价值

主要是一些层级结构:产品和盒子,比如一个盒子可以包含多个产品或者几个较小的盒子。小盒子里又有一些其他的产品或者更小的盒子

我们现在开发一个订购系统。订单中可以包含无包装的简单产品,也可以包含装满产品的盒子。那么此时应该如何计算总价格就成了问题,一个简单的方法就是一直递归到最底层然后计算总价。

解决方案

组合模式建议使用一个通用接口来 「产品」 和「盒子」进行交互,并且在该接口中declare一个计算总价的方法

具体的,设计方法时候,对于产品,我们直接返回价格。对于盒子方法就会看盒子中间的所有内容,询问每个项目的价格, 然后返回该盒子的总价格。 如果其中某个项目是小一号的盒子, 那么当前盒子也会遍历其中的所有项目, 以此类推, 直到计算出所有内部组成部分的价格。 甚至可以在盒子的最终价格中增加额外费用, 作为该盒子的包装费用。

composite_1

通过使用这种方式,我们不需要知道树里面对象的具体类。只需要使用通用接口来进行相同的处理,化繁为简。

一个其他的例子包含:

大部分国家的军队都采用层次管理,军师旅团营连排

结构

composite_2

  1. 组件 (Com­po­nent) 接口描述了树中简单项目和复杂项目所共有的操作。

  2. 叶节点 (Leaf) 是树的基本结构, 它不包含子项目。
    一般情况下, 叶节点最终会完成大部分的实际工作, 因为它们无法将工作指派给其他部分。

  3. 容器 (Con­tain­er)——又名 “组合 (Com­pos­ite)”——是包含叶节点或其他容器等子项目的单位。 容器不知道其子项目所属的具体类, 它只通过通用的组件接口与其子项目交互。

    容器接收到请求后会将工作分配给自己的子项目, 处理中间结果, 然后将最终结果返回给客户端。

  4. 客户端 (Client) 通过组件接口与所有项目交互。 因此, 客户端能以相同方式与树状结构中的简单或复杂项目交互。

这里的 add remove getChildren就是comosite作用于自身field的方法,即比如加入/去掉元素到Component[]或者 返回 整个Component[]

代码样例

composite_3

组合图形 == 容器(组合)它由多个包含容器在内的字图形构成。它们有相同的方比如「move」「draw」但是组合图形本身不完成具体工作。而是一直将请求递归传递给子项目,汇总结果

通过所有图形类所共有的接口, 客户端代码可以与所有图形互动。 因此, 客户端不知道与其交互的是简单图形还是组合图形。 客户端可以与非常复杂的对象结构进行交互, 而无需与组成该结构的实体类紧密耦合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 组件接口会声明组合中简单和复杂对象的通用操作。
interface Graphic is
  method move(x, y)
  method draw()

// 叶节点类代表组合的终端对象。叶节点对象中不能包含任何子对象。叶节点对象
// 通常会完成实际的工作,组合对象则仅会将工作委派给自己的子部件。
class Dot implements Graphic is
  field x, y

  constructor Dot(x, y) { ... }

  method move(x, y) is
    this.x += x, this.y += y

  method draw() is
    // 在坐标位置(X,Y)处绘制一个点。

// 所有组件类都可以扩展其他组件。
class Circle extends Dot is
  field radius

  constructor Circle(x, y, radius) { ... }

  method draw() is
    // 在坐标位置(X,Y)处绘制一个半径为 R 的圆。

// 组合类表示可能包含子项目的复杂组件。组合对象通常会将实际工作委派给子项
// 目,然后“汇总”结果。
class CompoundGraphic implements Graphic is
  field children: array of Graphic

  // 组合对象可在其项目列表中添加或移除其他组件(简单的或复杂的皆可)。
  method add(child: Graphic) is
// 在子项目数组中添加一个子项目。

  method remove(child: Graphic) is
    // 从子项目数组中移除一个子项目。

  method move(x, y) is
    foreach (child in children) do
      child.move(x, y)
// 组合会以特定的方式执行其主要逻辑。它会递归遍历所有子项目,并收集和
  // 汇总其结果。由于组合的子项目也会将调用传递给自己的子项目,以此类推,
  // 最后组合将会完成整个对象树的遍历工作。
  method draw() is
    // 1. 对于每个子部件:
    //     - 绘制该部件。
    //     - 更新边框坐标。
    // 2. 根据边框坐标绘制一个虚线长方形。


// 客户端代码会通过基础接口与所有组件进行交互。这样一来,客户端代码便可同
// 时支持简单叶节点组件和复杂组件。
class ImageEditor is
  field all: CompoundGraphic

  method load() is
    all = new CompoundGraphic()
    all.add(new “new Dot(12))
    all.add(new Circle(5310))
    // ...

  // 将所需组件组合为复杂的组合组件。
  method groupSelected(components: array of Graphic) is
    group = new CompoundGraphic()
    foreach (component in components) do
      group.add(component)
      all.remove(component)
all.add(group)
// 所有组件都将被绘制。
all.draw()

应用场景

  • 树状对象结构,考虑使用组合模式
    • 组合模式有两种共享公共接口的基本元素类型:简单叶子结点和复杂组合容器。容器可以包含其他叶子结点或容器。从而组成树状嵌套递归对象结构
  • 客户端代码想要用相同方式处理简单和复杂元素,考虑使用组合模式
    • 所有元素公用同一个接口。在这个借口的帮助下客户端不需要在意所用的对象的具体类

实现方式

  1. 确保应用的核心模型能够以树状结构表示。 尝试将其分解为简单元素和容器。 记住, 容器必须能够同时包含简单元素和其他容器。
  2. 声明组件接口及其一系列方法, 这些方法对简单和复杂元素都有意义。
  3. 创建一个叶节点类表示简单元素。 程序中可以有多个不同的叶节点类。
  4. 创建一个容器类表示复杂元素。 在该类中, 创建一个数组成员变量来存储对于其子元素的引用。 该数组必须能够同时保存叶节点和容器, 因此请确保将其声明为组合接口类型
    实现组件接口方法时, 记住容器应该将大部分工作交给其子元素来完成。
  5. 最后, 在容器中定义添加和删除子元素的方法。
    记住, 这些操作可在组件接口中声明。 这将会违反_接口隔离原则_, 因为叶节点类中的这些方法为空。 但是, 这可以让客户端无差别地访问所有元素, 即使是组成树状结构的元素。

优缺点

  • 充分利用多态和递归
  • 开闭原则,无需更改已有代码,在应用中添加新元素,使其成为对象树的一部分
  • 缺点:对于功能差异比较的,找公共接口会很复杂

和其他模式的关系

桥接、 状态和策略 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。

Builder + Composite

你可以在创建复杂组合树时使用生成器, 因为这可使其构造步骤以递归的方式运行。

为了更好地体现“在创建复杂组合树时使用生成器,因为这可使其构造步骤以递归的方式运行”的思想,我们可以构建一个更为复杂的例子,比如一个公司组织结构的树形表示,其中包含多个层级的部门和员工。我们将使用组合模式来表示组织结构的层次,同时使用生成器模式来递归地构建这个结构。这个例子中,我们将特别注意在代码中添加注释,以解释关键步骤和设计选择。

组合模式部分

首先,我们定义组合模式相关的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 组织结构的组件接口
interface OrganizationComponent {
void printStructure();
}

// 叶节点:表示一个员工
class Employee implements OrganizationComponent {
private String name;
private String position;

public Employee(String name, String position) {
this.name = name;
this.position = position;
}

@Override
public void printStructure() {
System.out.println(position + ": " + name);
}
}

// 组合节点:表示一个部门,可以包含其他部门或员工
class Department implements OrganizationComponent {
private String name;
private List<OrganizationComponent> members = new ArrayList<>();

public Department(String name) {
this.name = name;
}

// 添加成员,可以是部门或员工
public void addMember(OrganizationComponent member) {
members.add(member);
}

@Override
public void printStructure() {
System.out.println("Department: " + name);
for (OrganizationComponent member : members) {
member.printStructure();
}
}
}

生成器模式部分

接下来,我们定义生成器模式相关的类,以支持递归地构建组织结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 组织结构的生成器
class OrganizationBuilder {
private Department root;
private Deque<Department> departmentStack = new ArrayDeque<>();

public OrganizationBuilder(String rootName) {
this.root = new Department(rootName);
departmentStack.push(root); // 将根部门压栈作为当前操作的部门
}

// 添加一个部门,新部门自动成为当前操作的部门
public OrganizationBuilder addDepartment(String name) {
Department newDepartment = new Department(name);
Department current = departmentStack.peek();
if (current != null) {
current.addMember(newDepartment);
departmentStack.push(newDepartment); // 将新部门压栈
}
return this;
}

// 添加一个员工到当前操作的部门
public OrganizationBuilder addEmployee(String name, String position) {
Department current = departmentStack.peek();
if (current != null) {
current.addMember(new Employee(name, position));
}
return this;
}

// 结束当前部门的构建,返回上一级部门
public OrganizationBuilder endDepartment() {
departmentStack.pop(); // 结束当前部门的构建,弹出栈顶部门
return this;
}

// 构建最终的组织结构,返回根部门
public Department build() {
return root;
}
}

示例使用

最后,我们使用这些类来构建一个示例公司的组织结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CompositeBuilderDemo {
public static void main(String[] args) {
// 使用生成器递归构建组织结构
OrganizationBuilder builder = new OrganizationBuilder("Head Office")
.addDepartment("R&D")
.addEmployee("Alice", "Engineer")
.addEmployee("Bob", "Engineer")
.endDepartment()
.addDepartment("HR")
.addEmployee("Charlie", "Recruiter")
.addEmployee("Dana", "Coordinator")
.endDepartment();

Department company = builder.build();
company.printStructure(); // 打印整个组织结构
}
}

在这个例子中,我们首先创建了一个OrganizationBuilder实例,指定了公司的根部门名为”Head Office”。然后,我们递归地添加了两个部门”R&D”和”HR”,以及它们各自的员工。addDepartment方法让我们可以添加一个新的部门,并自动将其设置为当前操作的部门,这支持了递归地构建部门结构。通过调用endDepartment方法,我们结束当前部门的构建,并回到上一级部门。最后,我们通过调用build方法完成整个组织结构的构建,并打印出来。

这个例子展示了如何结合使用组合模式和生成器模式来递归地构建和管理一个复杂的树形结构,同时保持代码的清晰和可维护性。

责任链通常和组合模式结合使用。 在这种情况下, 叶组件接收到请求后, 可以将请求沿包含全体父组件的链一直传递至对象树的底部。

你可以使用迭代器来遍历组合树。

你可以使用访问者对整个组合树执行操作。

你可以使用享元实现组合树的共享叶节点以节省内存。

组合和装饰的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。
装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。
但是, 模式也可以相互合作: 你可以使用装饰来扩展组合树中特定对象的行为。

大量使用组合和装饰的设计通常可从对于原型的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。

装饰 - 装饰者,装饰器,Decorator, Wrapper

装饰模式允许我们通过将对象放入包含行为的特殊服装对象中来为原对象绑定新行为

decorator-2x

现实问题

假设正在开发一个提供通知功能的库,其他程序使用它向用户发送关于重要事件的通知

库最初版本基于「通知器 Notifier」类,此时只有很少几个成员变量,一个构造函数以及一个「send」方法。这个方法可以接受来自客户端的消息作为参数。并将信息发送一个订阅邮箱。邮箱列表通过构造函数传递给通知器。创建和配置通知器仅被调用一次,然后发送给对应的邮箱当重要事情发生时

为了构建一个提供通知功能的库的最初版本,我们首先定义一个简单的Notifier类。这个类将包含一个用于存储订阅邮箱列表的成员变量,一个构造函数来初始化这个列表,以及一个send方法用于将消息发送给所有订阅的邮箱。这里是一个简单的Java实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.List;

// 通知器类
public class Notifier {
private List<String> emailList; // 存储订阅邮箱列表

// 构造函数,通过它传递邮箱列表
public Notifier(List<String> emailList) {
this.emailList = emailList;
}

// 发送消息到所有订阅的邮箱
public void send(String message) {
for (String email : emailList) {
// 模拟发送消息过程
System.out.println("Sending message to " + email + ": " + message);
// 实际应用中,这里会有发送邮件的逻辑
}
}
}

在这个例子中,Notifier类定义了基础的通知功能。构造函数Notifier(List<String> emailList)接受一个包含邮箱地址的列表,这些邮箱是消息的目标接收者。send(String message)方法遍历这个列表,并为每个邮箱打印一条消息,代表发送过程。在实际应用中,这里会包含实际发送邮件的代码逻辑。

使用这个类的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.Arrays;

public class NotificationService {
public static void main(String[] args) {
// 初始化订阅邮箱列表
List<String> emails = Arrays.asList("user1@example.com", "user2@example.com");

// 创建通知器实例
Notifier notifier = new Notifier(emails);

// 发送通知消息
notifier.send("Hello, this is an important notification message!");
}
}

在这个示例中,我们首先创建了一个包含两个邮箱地址的列表。然后,我们使用这个列表创建了一个Notifier实例。通过调用send方法,我们向所有订阅的邮箱发送了一个消息。

这个简单的实现展示了通知库最初版本的核心功能。在这个阶段,我们没有考虑使用装饰器模式来扩展功能,而是专注于实现基本的通知发送功能。之后,可以通过装饰器模式添加额外的功能,如日志记录、消息加密、错误处理等,而不影响现有的Notifier类。

然后未来我们发现我们不单单需要邮箱通知,我们还希望有手机短信通知,微信通知,QQ通知等等

decorator_1

这里的挑战是,客户端需要传入类型,通过类型来调用对应的发送方法。

而且如果我们需要组合一起的时候,比如腾讯用户,那么我们需要wechat 和 QQ,比如信息用户,SMS + Wechat 等等,此时就会发生子类梯度爆炸

decorator_2

解决方案

如果对每一个需要扩展的功能都是使用继承然后扩展一个子类那么很容易就会出现由于继承导致的子类数量爆炸的问题

我们可以使用组合:

两者的工作方式几乎一模一样: 一个对象包含指向另一个对象的引用, 并将部分工作委派给引用对象; 继承中的对象则继承了父类的行为, 它们自己能够完成这些工作。

组合是许多设计模式背后的关键原则

decorator_3

对于装饰模式,又称其为封装器模式。封装器是一个能与其他目标对象链接的对象。封装器包含与目标对象相同的一系列方法,他会把所有接收到的请求委派给目标对象。但是封装器也可以再把请求交给目标对象前后进行处理。

封装器需要实现和其封装对象相同的接口,我们可以将一个对象放入多个封装器中,并在对象中添加封装器的组合行为

在之前的例子中,我们可以将邮件通知这个default行为放在基类Notifier中然后把其他通知方法放在装饰器中

decorator_4

对于上述问题的客户端代码来说把