复合模式:模式之模式

当模式们开始合作

前面九节课,我们学了二十多种设计模式——创建型的工厂三兄弟、建造者、原型,行为型的策略、状态、命令、观察者、中介者、模板方法,结构型的适配器、组合、桥接、装饰、外观、享元、代理。每种模式都是一把解决特定问题的钥匙。

但现实中的软件系统很少只有一个设计问题。你可能同时面对「接口不匹配」「需要动态添加功能」「统一管理一群对象」「解耦通知机制」等多个挑战。这时候,模式不再是孤军作战,而是协同出击。

这就引出了本节的主题——

复合模式

复合模式(Compound Pattern)是将两种或多种设计模式整合在一起,形成一个能解决常见或通用问题的综合方案。

复合模式不是简单地把几个模式「堆」在一起。它要求模式之间存在有机的协作关系:一个模式解决的问题自然地引出另一个模式的应用场景,模式之间相互配合而非相互干扰。

我们接下来通过一个完整的案例——鸭子模拟器——来体验这一过程。这个案例不会一次性抛出所有模式,而是随着需求的逐步演进,每一步都自然地引入一种新模式。到最后你会发现,五六种模式在同一个系统中各司其职、浑然一体。

鸭子模拟器:从零构建

需求分析

假设我们要开发一款鸭子模拟器。先前的模拟大鹅游戏大受好评,公司决定做一个模拟鸭子的版本。需求如下:

  • 能够方便地增加新种类的鸭子,如野鸭、绿头鸭、红头鸭、橡皮鸭等
  • 其他禽类(如鹅)也可能混进来
  • 呱呱叫学家想要统计鸭群的总叫声次数
  • 需要对鸭子进行统一装配,确保行为一致
  • 需要支持鸭子「家族」,单个鸭子和一族鸭子都能统一管理
  • 兼具可扩展性与灵活性

这些需求看起来各不相同,但正是这种多维度的需求,催生了多种模式的协作。

起点:Quackable 接口

我们从最简单的设计开始——定义一个 Quackable 接口,所有鸭子类都实现这个接口:

1
2
3
public interface Quackable {
    void quack();
}

这个接口非常简洁:所有能「呱呱叫」的对象都实现 quack() 方法。接下来创建几个具体的鸭子类——除了真实的鸭子,还有 DuckCall(鸭鸣器,猎人用来模仿鸭叫的工具)和 RubberDuck(橡皮鸭):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MallardDuck implements Quackable {
    public void quack() {
        System.out.println("Quack");  // 绿头鸭的标准呱呱叫
    }
}

public class RedheadDuck implements Quackable {
    public void quack() {
        System.out.println("Quack");  // 红头鸭也是呱呱叫
    }
}

public class DuckCall implements Quackable {
    public void quack() {
        System.out.println("Kwak");   // 鸭鸣器——模仿鸭叫的工具
    }
}

public class RubberDuck implements Quackable {
    public void quack() {
        System.out.println("Squeak"); // 橡皮鸭的吱吱叫
    }
}

有了鸭子,还需要一个模拟器来让它们叫起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DuckSimulator {
    public static void main(String[] args) {
        DuckSimulator simulator = new DuckSimulator();
        simulator.simulate();
    }

    void simulate() {
        Quackable mallardDuck = new MallardDuck();
        Quackable redheadDuck = new RedheadDuck();
        Quackable duckCall = new DuckCall();
        Quackable rubberDuck = new RubberDuck();

        System.out.println("Duck Simulator");
        simulate(mallardDuck);
        simulate(redheadDuck);
        simulate(duckCall);
        simulate(rubberDuck);
    }

    void simulate(Quackable duck) {
        duck.quack();
    }
}

一切正常工作。但现在,需求开始逐步涌来。

第一步:适配器模式——让鹅混入鸭群

问题

模拟器里突然闯进了一只鹅。鹅也会叫、会飞、会游泳,凭什么不能加入模拟器?但问题是,鹅不会呱呱叫——它们是咯咯叫的,它根本没有实现 Quackable 接口:

1
2
3
4
5
public class Goose {
    public void honk() {
        System.out.println("Honk");  // 鹅的咯咯叫
    }
}

模拟器只认 Quackable,鹅的 honk() 方法名字都不对。我们不想修改鹅类(它可能是第三方代码),也不想修改模拟器——我们需要的是一个「翻译官」。

解决方案

还记得适配器模式吗?它的职责就是把一个接口转换成另一个接口。我们给鹅套上一个适配器,让它看起来像一只鸭子:

1
2
3
4
5
6
7
8
9
10
11
12
// 鹅的适配器——把 Goose 包装成 Quackable
public class GooseAdapter implements Quackable {
    Goose goose;

    public GooseAdapter(Goose goose) {
        this.goose = goose;
    }

    public void quack() {
        goose.honk();  // 把 quack() 的调用委托给 honk()
    }
}

现在鹅可以在模拟器中和鸭子们一起工作了:

1
2
3
4
5
void simulate() {
    // ... 鸭子们 ...
    Quackable gooseDuck = new GooseAdapter(new Goose());
    simulate(gooseDuck);  // 输出 "Honk"
}
classDiagram
    direction LR
    class Quackable {
        <<interface>>
        +quack()* void
    }
    class MallardDuck {
        +quack() void
    }
    class GooseAdapter {
        -goose : Goose
        +quack() void
    }
    class Goose {
        +honk() void
    }

    Quackable <|.. MallardDuck
    Quackable <|.. GooseAdapter
    GooseAdapter --> Goose : 委托

    classDef iface fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    classDef impl fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
    classDef adaptee fill:#fff3e0,stroke:#ef6c00,stroke-width:2px

    class Quackable iface
    class MallardDuck impl
    class GooseAdapter impl
    class Goose adaptee

模式回顾:适配器模式通过组合(composition)持有被适配对象的引用,将不兼容的接口转换为目标接口。客户端(模拟器)完全不知道它面对的其实是一只鹅。

第二步:装饰模式——叫声计数

问题

呱呱叫学家对鸭子的叫声充满研究热情,他们想要统计鸭群的总叫声次数。但有一个关键约束:不能修改鸭子类

这正是装饰模式的用武之地——在不修改原有对象的前提下,动态添加新功能。

解决方案

我们创建一个 QuackCounter 装饰器,它包裹住任何 Quackable 对象,在每次调用 quack() 时记录计数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class QuackCounter implements Quackable {
    Quackable duck;           // 被装饰的鸭子
    static int numberOfQuacks; // 静态变量——所有装饰器共享同一个计数

    public QuackCounter(Quackable duck) {
        this.duck = duck;
    }

    public void quack() {
        duck.quack();          // 委托给被装饰的鸭子
        numberOfQuacks++;      // 计数 +1
    }

    public static int getQuacks() {
        return numberOfQuacks;
    }
}

使用方式很直观——用 QuackCounter 包裹每一只鸭子:

1
2
3
4
5
6
7
8
9
10
11
12
13
void simulate() {
    Quackable mallardDuck = new QuackCounter(new MallardDuck());
    Quackable redheadDuck = new QuackCounter(new RedheadDuck());
    Quackable duckCall = new QuackCounter(new DuckCall());
    Quackable rubberDuck = new QuackCounter(new RubberDuck());
    // 鹅不需要计数——它不是鸭子
    Quackable gooseDuck = new GooseAdapter(new Goose());

    simulate(mallardDuck);
    // ... 其他鸭子 ...

    System.out.println("Total quacks: " + QuackCounter.getQuacks());
}

装饰器和被装饰对象实现同一个接口,所以对模拟器来说完全透明——它甚至不知道鸭子被包了一层。

新的隐患

叫声计数功能很棒,但存在一个严重问题——如果有人创建鸭子时忘了用 QuackCounter 包裹,那只鸭子的叫声就不会被统计。大量叫声可能因此未被准确统计。

这就是装饰模式的痛点:必须确保每个对象都被正确包装,否则就无法获得增强功能。

解决这个问题的思路是:把鸭子的创建过程集中管理——将实例化和装饰逻辑封装在一起。这不就是工厂模式吗?

第三步:抽象工厂模式——确保装饰

问题

手动创建并装饰每只鸭子,既麻烦又容易遗漏。我们需要一个「质量控制机制」来确保所有鸭子都被正确装饰。

解决方案

由于工厂需要生产一个「产品族」——各种类型的鸭子——我们采用抽象工厂模式。先定义抽象工厂:

1
2
3
4
5
6
public abstract class AbstractDuckFactory {
    public abstract Quackable createMallardDuck();
    public abstract Quackable createRedheadDuck();
    public abstract Quackable createDuckCall();
    public abstract Quackable createRubberDuck();
}

然后分别实现两个具体工厂:

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
// 基础工厂——生产未经装饰的「裸」鸭子
public class DuckFactory extends AbstractDuckFactory {
    public Quackable createMallardDuck() {
        return new MallardDuck();
    }
    public Quackable createRedheadDuck() {
        return new RedheadDuck();
    }
    public Quackable createDuckCall() {
        return new DuckCall();
    }
    public Quackable createRubberDuck() {
        return new RubberDuck();
    }
}

// 计数工厂——每只鸭子出厂时自动包裹 QuackCounter
public class CountingDuckFactory extends AbstractDuckFactory {
    public Quackable createMallardDuck() {
        return new QuackCounter(new MallardDuck());
    }
    public Quackable createRedheadDuck() {
        return new QuackCounter(new RedheadDuck());
    }
    public Quackable createDuckCall() {
        return new QuackCounter(new DuckCall());
    }
    public Quackable createRubberDuck() {
        return new QuackCounter(new RubberDuck());
    }
}

模拟器方法接收抽象工厂作为参数,通过多态决定生产哪种鸭子:

1
2
3
4
5
6
7
void simulate(AbstractDuckFactory duckFactory) {
    Quackable mallardDuck = duckFactory.createMallardDuck();
    Quackable redheadDuck = duckFactory.createRedheadDuck();
    Quackable duckCall = duckFactory.createDuckCall();
    Quackable rubberDuck = duckFactory.createRubberDuck();
    // ...
}

只需传入不同的具体工厂,就能在「计数」与「不计数」之间自由切换,而模拟器代码不需要任何修改。更重要的是,使用 CountingDuckFactory 时,任何通过工厂创建的鸭子都自动带有计数装饰——彻底消除了遗漏包装的风险。

三种模式的协作

到目前为止,我们已经用了三种模式:

  • 适配器让不兼容的鹅融入鸭子体系
  • 装饰器在不修改鸭子类的前提下添加计数功能
  • 抽象工厂把创建和装饰逻辑集中管理,确保不遗漏

每种模式的引入都是由前一步遗留的问题驱动的,而非提前规划好的。这正是复合模式的精髓——模式之间自然衔接。

第四步:组合模式——鸭群管理

问题

随着鸭子种类不断增加,在模拟器中逐一管理每只鸭子变得越来越繁琐。呱呱叫学家还提出了新需求:除了管理所有鸭子,还需要追踪特定的鸭子族群。我们需要一种方式来统一管理鸭群集合(包括子集合),并且能够批量执行操作

这不就是组合模式的典型场景?统一处理「个体」和「整体」。

解决方案

创建一个 Flock(鸭群)类,它自身也实现 Quackable 接口——这样单只鸭子和一群鸭子可以被同等对待:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Flock implements Quackable {
    List<Quackable> quackers = new ArrayList<>();

    public void add(Quackable quacker) {
        quackers.add(quacker);
    }

    public void quack() {
        // 迭代调用每个成员的 quack()
        for (Quackable quacker : quackers) {
            quacker.quack();
        }
    }
}

Flock 既是容器(持有一组 Quackable),又是 Quackable(自己也能 quack())——调用鸭群的 quack() 会递归地让每只鸭子都叫一声。而且鸭群里还可以嵌套鸭群,实现层次化管理:

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
void simulate(AbstractDuckFactory duckFactory) {
    // 创建鸭子
    Quackable mallardDuck = duckFactory.createMallardDuck();
    Quackable redheadDuck = duckFactory.createRedheadDuck();
    Quackable duckCall = duckFactory.createDuckCall();
    Quackable rubberDuck = duckFactory.createRubberDuck();
    Quackable gooseDuck = new GooseAdapter(new Goose());

    // 创建主鸭群,包含所有鸭子
    Flock flockOfDucks = new Flock();
    flockOfDucks.add(mallardDuck);
    flockOfDucks.add(redheadDuck);
    flockOfDucks.add(duckCall);
    flockOfDucks.add(rubberDuck);
    flockOfDucks.add(gooseDuck);

    // 创建绿头鸭子鸭群——追踪特定族群
    Flock flockOfMallards = new Flock();
    flockOfMallards.add(duckFactory.createMallardDuck());
    flockOfMallards.add(duckFactory.createMallardDuck());
    flockOfMallards.add(duckFactory.createMallardDuck());

    // 把绿头鸭群加入主鸭群
    flockOfDucks.add(flockOfMallards);

    // 让整个主鸭群叫一声——所有鸭子(包括绿头鸭群)都会叫
    simulate(flockOfDucks);
}

安全性 vs 透明性

组合模式在设计时有一个经典的取舍:叶节点(单个鸭子)要不要也拥有 add() 方法?

设计取向 做法 优势 代价 此处选择
透明组合 叶节点和组合节点有完全相同的方法集(包括 add() 客户端无需区分叶节点和组合节点,代码统一 叶节点的 add() 没有实际意义,可能被误调用
安全组合 只有组合节点(Flock)有 add() 方法,叶节点没有 不可能对叶节点执行无意义操作 客户端必须知道是 Flock 才能添加成员

在之前学习组合模式的菜单系统例子中,叶节点和组合节点拥有完全相同的方法集——你甚至可以对菜单项调用 add()(虽然没有实际意义),那是「透明组合」的设计。

而在鸭子模拟器中,我们选择了安全组合:只有 Flock 拥有 add() 方法,普通鸭子没有。这意味着客户端必须知道某个 QuackableFlock 才能向它添加成员——透明性降低了,但安全性提升了:你不可能意外地向一只橡皮鸭「添加子鸭子」。

选择哪种取向?

没有标准答案。透明组合让客户端代码更简洁统一,安全组合避免了无意义的操作。在不同场景下根据需求权衡即可——这里我们选择安全组合,是因为「给单只鸭子添加成员」这个操作在语义上完全没有意义。

第五步:观察者模式——追踪个体

问题

组合模式让我们能批量管理鸭群,但呱呱叫学家又提出了新需求:除了统计总叫声次数,还需要实时追踪单只鸭子的叫声——每当某只鸭子叫了一声,学家就想收到通知。

在不修改现有鸭子类的核心逻辑的前提下,如何实现这种「一叫就通知」的机制?

解决方案

这正是观察者模式的用武之地。我们需要让每一个 Quackable 对象都具备「被观察」的能力。

定义可观察接口

首先,定义一个 QuackObservable 接口,让所有 Quackable 都支持注册和通知观察者:

1
2
3
4
public interface QuackObservable {
    void registerObserver(Observer observer);
    void notifyObservers();
}

然后让 Quackable 继承这个接口——这意味着所有实现 Quackable 的类都必须支持观察者功能:

1
2
3
public interface Quackable extends QuackObservable {
    void quack();
}

封装观察者逻辑

如果让每个鸭子类都重复实现 registerObserver()notifyObservers() 的逻辑,代码会大量重复。更优雅的做法是将核心的注册和通知机制封装到一个专门的 Observable 辅助类中,各个鸭子类通过组合持有它的引用,将观察者相关的方法调用委托给它处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Observable implements QuackObservable {
    List<Observer> observers = new ArrayList<>();
    QuackObservable duck;  // 持有被观察者的引用,用于在通知时告诉观察者「是谁叫的」

    public Observable(QuackObservable duck) {
        this.duck = duck;  // 创建时传入实际的鸭子对象
    }

    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(duck);  // 通知时传入实际的鸭子对象
        }
    }
}

为什么用辅助类而不是继承?

如果用一个抽象基类来实现观察者逻辑,那么所有鸭子都必须继承它——但 Java 是单继承的,这会严重限制类的设计。通过组合的方式,每个鸭子类内部持有一个 Observable 对象,只需编写最小化的委托代码即可获得完整的观察者能力。

这也体现了我们在第二节学过的设计原则:多用组合,少用继承

将观察者整合到鸭子类中

每个 Quackable 实现类只需做两件事:

  1. 在构造器中创建 Observable 辅助对象
  2. registerObserver()notifyObservers() 委托给辅助对象

MallardDuck 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MallardDuck implements Quackable {
    Observable observable;

    public MallardDuck() {
        observable = new Observable(this);  // 把自己传给辅助类
    }

    public void quack() {
        System.out.println("Quack");
        notifyObservers();  // 叫完之后通知观察者
    }

    public void registerObserver(Observer observer) {
        observable.registerObserver(observer);  // 委托
    }

    public void notifyObservers() {
        observable.notifyObservers();  // 委托
    }
}

RedheadDuckDuckCallRubberDuck 的结构完全相同——只有 quack() 中打印的声音不同。

装饰器和适配器的适配

QuackCounter(装饰器)和 GooseAdapter(适配器)也需要适配观察者功能。它们的做法是将观察者相关的调用透传给被包装的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// QuackCounter 装饰器
public class QuackCounter implements Quackable {
    Quackable duck;
    static int numberOfQuacks;

    public QuackCounter(Quackable duck) {
        this.duck = duck;
    }

    public void quack() {
        duck.quack();      // 委托给被装饰对象(会触发 notifyObservers)
        numberOfQuacks++;
    }

    // 观察者注册透传给被装饰对象
    public void registerObserver(Observer observer) {
        duck.registerObserver(observer);
    }

    public void notifyObservers() {
        duck.notifyObservers();
    }
}

装饰器不需要自己管理观察者列表,它把注册请求直接转发给底层的鸭子对象。这样观察者收到通知时,能拿到的是真实的鸭子而非装饰器。

Flock 的观察者处理

Flock(鸭群)的观察者注册也很巧妙——它不维护自己的观察者列表,而是将注册请求递归分发给每一个成员:

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 Flock implements Quackable {
    List<Quackable> quackers = new ArrayList<>();

    public void add(Quackable quacker) {
        quackers.add(quacker);
    }

    public void quack() {
        for (Quackable quacker : quackers) {
            quacker.quack();
        }
    }

    // 向鸭群注册 = 向每只鸭子注册
    public void registerObserver(Observer observer) {
        for (Quackable quacker : quackers) {
            quacker.registerObserver(observer);
        }
    }

    public void notifyObservers() {
        // 由各个成员自行通知,Flock 不需要做额外的事
    }
}

这意味着:向一个鸭群注册观察者,等价于向鸭群里的每一只鸭子都注册了这个观察者。之后不管哪只鸭子叫了,观察者都会收到通知。

观察者实现

最后,创建一个「呱呱叫学家」观察者:

1
2
3
4
5
6
7
8
9
public interface Observer {
    void update(QuackObservable duck);
}

public class Quackologist implements Observer {
    public void update(QuackObservable duck) {
        System.out.println("Quackologist: " + duck + " just quacked!");
    }
}

最终的鸭子模拟器

五种模式全部就位,来看看最终版本的模拟器如何将它们串联在一起:

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
public class DuckSimulator {
    public static void main(String[] args) {
        DuckSimulator simulator = new DuckSimulator();
        AbstractDuckFactory duckFactory = new CountingDuckFactory();
        simulator.simulate(duckFactory);
    }

    void simulate(AbstractDuckFactory duckFactory) {
        // 1. 抽象工厂创建鸭子(自动包裹 QuackCounter 装饰器)
        Quackable mallardDuck = duckFactory.createMallardDuck();
        Quackable redheadDuck = duckFactory.createRedheadDuck();
        Quackable duckCall = duckFactory.createDuckCall();
        Quackable rubberDuck = duckFactory.createRubberDuck();

        // 2. 适配器让鹅融入鸭群
        Quackable gooseDuck = new GooseAdapter(new Goose());

        // 3. 组合模式管理鸭群
        Flock flockOfDucks = new Flock();
        flockOfDucks.add(mallardDuck);
        flockOfDucks.add(redheadDuck);
        flockOfDucks.add(duckCall);
        flockOfDucks.add(rubberDuck);
        flockOfDucks.add(gooseDuck);

        // 创建绿头鸭子族群
        Flock flockOfMallards = new Flock();
        flockOfMallards.add(duckFactory.createMallardDuck());
        flockOfMallards.add(duckFactory.createMallardDuck());
        flockOfMallards.add(duckFactory.createMallardDuck());
        flockOfDucks.add(flockOfMallards);

        // 4. 观察者模式追踪叫声
        Quackologist quackologist = new Quackologist();
        flockOfDucks.registerObserver(quackologist);

        // 5. 启动模拟——一行代码驱动整个系统
        System.out.println("Duck Simulator: With Composite - Flocks");
        simulate(flockOfDucks);

        System.out.println("The ducks quacked " + QuackCounter.getQuacks() + " times");
    }

    void simulate(Quackable duck) {
        duck.quack();
    }
}

注意代码中的注释标号 1-5,每一步对应一种模式的应用。仅仅三十多行代码,就融合了五种设计模式——而每一种都在解决一个具体的问题。

全景回顾:五种模式的协作

让我们退后一步,俯瞰整个系统中模式的分工:

flowchart TD
    subgraph 创建层["创建层"]
        AF["AbstractDuckFactory\n(抽象工厂)"]
        CF["CountingDuckFactory"]
        AF --> CF
    end

    subgraph 结构层["结构层"]
        QC["QuackCounter\n(装饰器)"]
        GA["GooseAdapter\n(适配器)"]
        FK["Flock\n(组合)"]
    end

    subgraph 行为层["行为层"]
        OB["Observable\n(观察者辅助类)"]
        QL["Quackologist\n(观察者)"]
    end

    subgraph 核心["核心接口"]
        QI["Quackable"]
    end

    CF -->|"生产并包装"| QC
    QC -->|"包裹"| QI
    GA -->|"适配"| QI
    FK -->|"聚合"| QI
    QI -->|"被观察"| OB
    OB -->|"通知"| QL

    classDef create fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    classDef structure fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
    classDef behavior fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
    classDef core fill:#f3e5f5,stroke:#4a148c,stroke-width:2px

    class AF,CF create
    class QC,GA,FK structure
    class OB,QL behavior
    class QI core
模式 角色 解决的问题
适配器 GooseAdapter 让接口不兼容的鹅融入鸭子体系
装饰器 QuackCounter 在不修改鸭子类的前提下添加叫声计数
抽象工厂 CountingDuckFactory 集中管理创建逻辑,确保所有鸭子都被正确装饰
组合 Flock 统一管理单只鸭子和鸭群,支持层次化结构
观察者 Observable + Quackologist 实时追踪个体鸭子的叫声,解耦通知机制

这五种模式各自独立,却在同一个系统中协同工作:工厂创建并装饰鸭子,适配器引入外来物种,组合统一管理群体,观察者追踪个体行为。它们之间没有冲突,反而相互增强——这就是复合模式的力量。

理解了模式如何协作之后,更值得思考的是它们为何能协作——背后有哪些可以复用的设计原则。

复合模式的设计启示

模式的自然涌现

鸭子模拟器最值得学习的一点是:五种模式不是一开始就规划好的,而是随着需求的演进逐步引入的。

  • 鹅闯入 → 适配器
  • 要计数 → 装饰器
  • 怕遗漏 → 工厂
  • 要分群 → 组合
  • 要追踪 → 观察者

每一步都是在解决当前的具体问题,而不是为了「用上某个模式」而用。好的设计不是预先堆砌模式,而是让模式在需求驱动下自然涌现

MVC:经典的复合模式

日常开发中最常见的复合模式实例当属 MVC(Model-View-Controller)。它本身就是三种设计模式的组合:

MVC 组件 使用的模式 作用
Model ↔ View 观察者模式 Model 状态变化时通知 View 更新,两者保持松耦合
View ↔ Controller 策略模式 Controller 充当 View 的「策略」,可以替换不同的控制器实现
View 内部 组合模式 界面由嵌套的组件(面板、按钮、文本框)组成树形结构

MVC 之所以成为最广泛使用的架构模式之一,正是因为它将三种模式有机地融合在一起,各自解决一个维度的问题,共同构建出一个灵活、可维护的系统架构。

何时使用复合模式

复合模式不是目标

不要为了「使用复合模式」而强行将多个模式塞进系统。复合模式是结果而非目标——它是多个设计问题同时存在时,各自对应的模式自然组合的产物。

正确的做法是:针对每个具体问题选择合适的模式,然后检查这些模式之间是否能协同工作。如果它们自然配合,那就是一个复合模式;如果需要强行拼凑,那说明设计可能出了问题。

思考题

回顾你学过的所有设计模式,还能想到哪些实际场景中会出现多种模式协作的情况?

比如:一个电商系统的订单处理流程中,可能同时用到哪些模式?