结构型模式:桥接模式与装饰模式

从「适配」到「解耦」与「增强」

上一节我们认识了适配器和组合模式——一个是软件世界的「转接头」,一个是递归构建树形结构的利器。它们分别解决了「接口不匹配」和「个体与整体的统一处理」两个问题。

这一节,我们继续在结构型模式的世界中探索,面对两个新的设计挑战:

  • 当一个系统有两个独立变化的维度时,继承带来的类爆炸怎么办?——桥接模式
  • 当需要在运行时动态地给对象添加职责时,继承太僵硬怎么办?——装饰模式

两个模式看似不相关,却有一个共同的哲学:用组合代替继承

桥接模式

蜡笔的烦恼:当两个维度纠缠在一起

设想你要开发一个绘图程序,需要绘制矩形、圆形、椭圆、正方形这 4 种形状,同时这些形状可以有红色、绿色、蓝色 3 种颜色。你会怎么设计类结构?

方案一:为每种组合创建一个类。 红色矩形、绿色矩形、蓝色矩形、红色圆形……一共需要 4×3=124 \times 3 = 12 个类。如果再加一种颜色?4×4=164 \times 4 = 16 个。再加一种形状?5×4=205 \times 4 = 20 个。类的数量随着维度的扩展呈乘法增长

flowchart TD
    S[Shape] --> R[矩形]
    S --> C[圆形]
    S --> E[椭圆]
    R --> RR[红色矩形]
    R --> RG[绿色矩形]
    R --> RB[蓝色矩形]
    C --> CR[红色圆形]
    C --> CG[绿色圆形]
    C --> CB[蓝色圆形]
    E --> ER[红色椭圆]
    E --> EG[...]
    E --> EB[...]

    classDef base fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    classDef mid fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px
    classDef leaf fill:#fff3e0,stroke:#ef6c00,stroke-width:2px

    class S base
    class R,C,E mid
    class RR,RG,RB,CR,CG,CB,ER,EG,EB leaf

这就是继承爆炸——每增加一个维度的选项,所有已有组合都要翻倍。更致命的是,这种设计违背了单一职责原则:每个子类同时承担了「形状」和「颜色」两个变化的原因。

方案二:将两个维度分开管理。 形状是形状,颜色是颜色,各自独立变化,需要时再组合。4+3=74 + 3 = 7 个类就够了。

这就是桥接模式的核心思想:将继承关系转换为关联关系,把纠缠在一起的两个变化维度拆开,让它们沿各自的方向独立演化。

课件中有一个绝妙的类比——蜡笔与毛笔:

  • 蜡笔:每支蜡笔的颜色和粗细是绑定的。3 种粗细 × 5 种颜色 = 15 支蜡笔,对应 15 个类
  • 毛笔:笔本身只管粗细,颜料另外蘸。3 支毛笔 + 5 盒颜料 = 8 个类,功能完全相同

M×NM \times N 降到 M+NM + N,这就是桥接模式带来的类数量优化。

模式定义

桥接模式

桥接模式(Bridge Pattern)将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式。

Decouple an abstraction from its implementation so that the two can vary independently.

别名:柄体模式(Handle and Body)、接口模式(Interface)。

初次读到这个定义,你可能会困惑:「抽象部分」和「实现部分」到底指什么?这里的「抽象」和「实现」并不是编程语言意义上的抽象类和实现类,而是指系统中两个独立变化的维度

  • 抽象化(Abstraction):忽略一些信息,把不同的实体当作同样的实体对待的过程。在上面的例子中,「形状」就是抽象化维度
  • 实现化(Implementation):针对抽象化给出具体实现。在上面的例子中,「颜色」就是实现化维度
  • 脱耦(Decoupling):将两个维度之间的继承关系改为关联关系(组合或聚合),使它们可以相对独立地变化

模式结构

classDiagram
    class Abstraction {
        <<abstract>>
        #impl : Implementor
        +setImpl(Implementor impl) void
        +operation()* void
    }
    class RefinedAbstraction {
        +operation() void
    }
    class Implementor {
        <<interface>>
        +operationImpl()* void
    }
    class ConcreteImplementorA {
        +operationImpl() void
    }
    class ConcreteImplementorB {
        +operationImpl() void
    }

    Abstraction <|-- RefinedAbstraction
    Implementor <|.. ConcreteImplementorA
    Implementor <|.. ConcreteImplementorB
    Abstraction --> Implementor : impl

四个核心角色:

角色 职责
Abstraction(抽象类) 定义抽象维度的接口,持有 Implementor 的引用——这个引用就是连接两个维度的「桥」
RefinedAbstraction(扩充抽象类) 继承 Abstraction,实现抽象维度的具体变体
Implementor(实现类接口) 定义实现维度的接口,仅提供基本操作
ConcreteImplementor(具体实现类) 实现 Implementor 接口,提供基本操作的具体实现

注意类图中 Abstraction 与 Implementor 之间的连线——不是继承(实线三角箭头),而是关联(实线箭头)。这条关联线就是「桥」:它将两个原本通过继承纠缠在一起的维度,用组合关系连接起来,使双方可以独立扩展。

代码实现

实现类接口仅声明基本操作:

1
2
3
4
// 实现类接口——定义实现维度的操作
public interface Implementor {
    void operationImpl();
}

抽象类持有实现类接口的引用,这是桥接的关键:

1
2
3
4
5
6
7
8
9
10
// 抽象类——持有实现类接口的引用
public abstract class Abstraction {
    protected Implementor impl;  // 桥!连接两个维度

    public void setImpl(Implementor impl) {
        this.impl = impl;
    }

    public abstract void operation();
}

扩充抽象类在自己的业务逻辑中调用实现类接口的方法:

1
2
3
4
5
6
7
8
9
// 扩充抽象类——在业务方法中调用实现类的方法
public class RefinedAbstraction extends Abstraction {
    @Override
    public void operation() {
        // 抽象维度的业务逻辑...
        impl.operationImpl();  // 委托给实现维度
        // 更多业务逻辑...
    }
}

关键在于 impl.operationImpl() 这一行——抽象类不关心 impl 到底是哪个具体实现,它只知道 Implementor 接口定义了 operationImpl() 方法。这样,抽象维度和实现维度就可以各自独立扩展。

实例:模拟毛笔

现在需要提供大、中、小 3 种型号的画笔,能够绘制 5 种不同的颜色。如果使用蜡笔(继承方案),需要 3×5=153 \times 5 = 15 个类;如果使用毛笔(桥接方案),只需要 3+5=83 + 5 = 8 个类。

将「笔的粗细」作为抽象维度,「颜色」作为实现维度:

classDiagram
    class Pen {
        <<abstract>>
        #color : Color
        +setColor(Color c) void
        +draw(String content)* void
    }
    class BigPen {
        +draw(String content) void
    }
    class MiddlePen {
        +draw(String content) void
    }
    class SmallPen {
        +draw(String content) void
    }
    class Color {
        <<interface>>
        +paint(String penType, String content)* void
    }
    class Red {
        +paint(String penType, String content) void
    }
    class Green {
        +paint(String penType, String content) void
    }
    class Blue {
        +paint(String penType, String content) void
    }

    Pen <|-- BigPen
    Pen <|-- MiddlePen
    Pen <|-- SmallPen
    Color <|.. Red
    Color <|.. Green
    Color <|.. Blue
    Pen --> Color : color
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
// 实现类接口——颜色
public interface Color {
    void paint(String penType, String content);
}

// 具体实现类——红色
public class Red implements Color {
    @Override
    public void paint(String penType, String content) {
        System.out.println(penType + "绘制红色的" + content);
    }
}
// Green、Blue、White、Black 类似...

// 抽象类——毛笔
public abstract class Pen {
    protected Color color;

    public void setColor(Color color) {
        this.color = color;
    }

    public abstract void draw(String content);
}

// 扩充抽象类——大号毛笔
public class BigPen extends Pen {
    @Override
    public void draw(String content) {
        color.paint("大号毛笔", content);  // 委托给颜色
    }
}

// MiddlePen、SmallPen 类似...

客户端使用时,两个维度自由组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
Color red = new Red();
Color green = new Green();

Pen pen = new BigPen();
pen.setColor(red);
pen.draw("矩形");    // 大号毛笔绘制红色的矩形

pen.setColor(green);  // 运行时随时切换颜色——这就是组合的灵活性
pen.draw("圆形");    // 大号毛笔绘制绿色的圆形

pen = new SmallPen(); // 切换笔型也不影响颜色维度
pen.setColor(red);
pen.draw("三角形");  // 小号毛笔绘制红色的三角形

扩展时,新增一种颜色只需添加一个 Color 实现类,新增一种笔型只需添加一个 Pen 子类——两边互不影响,完全符合开闭原则。

实例:跨平台视频播放器

另一个典型的双维度场景:开发一个跨平台视频播放器,需要在 Windows、Linux、Unix 等操作系统上播放 MPEG、RMVB、AVI、WMV 等多种格式。

将「操作系统」作为抽象维度,「视频格式」作为实现维度:

classDiagram
    class OperatingSystem {
        <<abstract>>
        #videoFile : VideoFile
        +setVideoFile(VideoFile vf) void
        +play(String fileName)* void
    }
    class WindowsOS {
        +play(String fileName) void
    }
    class LinuxOS {
        +play(String fileName) void
    }
    class UnixOS {
        +play(String fileName) void
    }
    class VideoFile {
        <<interface>>
        +decode(String fileName)* void
    }
    class MPEGFile {
        +decode(String fileName) void
    }
    class RMVBFile {
        +decode(String fileName) void
    }
    class AVIFile {
        +decode(String fileName) void
    }
    class WMVFile {
        +decode(String fileName) void
    }

    OperatingSystem <|-- WindowsOS
    OperatingSystem <|-- LinuxOS
    OperatingSystem <|-- UnixOS
    VideoFile <|.. MPEGFile
    VideoFile <|.. RMVBFile
    VideoFile <|.. AVIFile
    VideoFile <|.. WMVFile
    OperatingSystem --> VideoFile : videoFile

没有桥接模式,3×4=123 \times 4 = 12 个类(WindowsMPEGPlayer、WindowsRMVBPlayer、LinuxMPEGPlayer……)。使用桥接模式后,3+4=73 + 4 = 7 个类。新增一种操作系统或一种视频格式,各只需一个新类。

优缺点与适用场景

优点:

优点 说明
分离抽象与实现 两个维度可以独立扩展,互不影响
优于多继承 多继承让一个类承担多个变化原因(违反 SRP),类数量呈乘法增长;桥接模式用组合代替继承,类数量呈加法增长
高可扩展性 任意扩展一个维度都不需要修改原有系统,符合开闭原则
对客户透明 实现细节隐藏在实现维度中,客户端只面向抽象编程

缺点:

  • 增加系统的理解与设计难度——聚合关联关系建立在抽象层,需要开发者有较强的抽象能力
  • 要求正确识别出系统中两个独立变化的维度,这本身并不容易

适用场景:

  • 一个类存在两个(或更多)独立变化的维度,且都需要扩展
  • 不希望使用继承,或因多层次继承导致类的个数急剧增加
  • 需要在运行时动态将抽象维度和实现维度的对象进行组合

实际应用

JVM——跨平台的桥。 Java 语言通过 Java 虚拟机实现了平台无关性。Java 程序(抽象)和操作系统(实现)是两个独立变化的维度,JVM 就是连接它们的桥——Java 代码不关心底层是 Windows 还是 Linux,操作系统也不需要为 Java 做特殊适配。

AWT Peer 架构。 Java 桌面应用在不同操作系统上会呈现不同的本地外观(LookAndFeel)。在 Unix 上看到 Motif 风格,在 Windows 上看到 Windows 风格,在 Mac 上看到 Macintosh 风格。Java 为 AWT 中的每个 GUI 构件都提供了一个 Peer 构件,这套 Peer 架构就是桥接模式的应用——GUI 构件(抽象)与平台原生组件(实现)通过 Peer 桥接。

JDBC——另一个视角。 在上一节我们从适配器的角度理解了 JDBC:驱动程序将 JDBC 接口适配为数据库原生 API。换一个视角来看,JDBC 同样体现了桥接模式——使用 JDBC 的应用程序是抽象角色,数据库是实现角色,JDBC 驱动动态地将一个特定类型的数据库与 Java 应用程序绑定在一起。

同一系统可以同时体现多种模式

JDBC 既是适配器(转换接口)又是桥接(分离应用与数据库两个维度)。设计模式不是互斥的标签,同一个系统从不同角度分析,可以看到不同模式在起作用。

桥接与适配器的联用

桥接模式和适配器模式用于设计的不同阶段:桥接模式用于系统初步设计,从一开始就将两个变化维度分离;适配器模式用于系统完成后的集成,当发现已有类接口不兼容时进行转换。

但两者也常常联用——当桥接模式的实现维度需要接入接口不兼容的第三方类时,就需要适配器来做接口转换。例如,视频播放器的桥接结构已经设计好了,但新来的某个视频格式库的接口和 VideoFile 不兼容——这时用适配器包装一下就好了。

桥接模式和策略模式有什么区别?

两者的类图看起来非常相似——都是一个类持有另一个接口的引用并委托调用。区别在于:

  • 桥接模式是结构型模式,关注的是两个独立变化的类层次如何通过组合连接。Abstraction 自身也有子类层次(RefinedAbstraction),两个维度都在扩展
  • 策略模式是行为型模式,关注的是一组可互换的算法。Context 通常没有子类层次,它只是使用不同的 Strategy

简单规则:如果两边都有类层次在变化,用桥接;如果只有一边(算法)在变化,用策略。

装饰模式

当继承力不从心

假设你手上有一辆汽车,它可以在陆地上移动。现在你希望它还能说话——变成一个机器人。如果需要,还想让它能飞——变成一架飞机。

最直觉的方案是继承:创建 RobotCar extends CarAirplaneCar extends Car。但这种方案有两个硬伤:

  1. 继承是静态的——一个对象的类型在创建时就确定了,无法在运行时切换。你不能让一辆已经造好的 Car 在运行过程中「变身」为 Robot
  2. 组合爆炸——如果还想让它又能说话又能飞呢?是不是得再来一个 FlyingRobotCar extends Car?每多一种能力,子类数量可能翻倍

给一个类或对象增加行为,一般有两种方式:

  • 继承机制:子类在拥有父类方法的同时可以新增方法。但这是静态的——编译时就确定了,运行时无法改变
  • 关联机制(组合):将一个对象嵌入另一个对象中,由外层对象决定是否调用内层对象的行为来扩展自己。这是动态的——可以在运行时自由组合

装饰模式就是基于关联机制的方案。它以对客户透明的方式动态地给一个对象附加更多的责任——客户端不会觉得装饰前和装饰后的对象有什么不同。

模式定义

装饰模式

装饰模式(Decorator Pattern)动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。它是一种对象结构型模式。

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

别名:包装器(Wrapper)——和适配器模式的别名相同,但它们适用于不同的场合。适配器的包装是为了转换接口,装饰的包装是为了增加职责

模式结构

classDiagram
    class Component {
        <<abstract>>
        +operation()* void
    }
    class ConcreteComponent {
        +operation() void
    }
    class Decorator {
        <<abstract>>
        -component : Component
        +Decorator(Component c)
        +operation() void
    }
    class ConcreteDecoratorA {
        +operation() void
        +addedBehavior() void
    }
    class ConcreteDecoratorB {
        +operation() void
        -addedState : String
    }

    Component <|-- ConcreteComponent
    Component <|-- Decorator
    Decorator <|-- ConcreteDecoratorA
    Decorator <|-- ConcreteDecoratorB
    Decorator --> Component : component

四个核心角色:

角色 职责
Component(抽象构件) 定义对象的接口,装饰器和具体构件都实现这个接口
ConcreteComponent(具体构件) 被装饰的原始对象,实现了基本功能
Decorator(抽象装饰类) 继承 Component 的同时持有一个 Component 的引用——既是 Component,又包含 Component
ConcreteDecorator(具体装饰类) 继承 Decorator,在调用被装饰对象方法的基础上添加新的行为

装饰的本质:递归包装

装饰模式最精妙之处在于 Decorator 的双重身份:它既是一个 Component(通过继承),又持有一个 Component(通过组合)。这意味着:

  • 因为 Decorator IS-A Component,装饰后的对象可以在任何需要 Component 的地方使用——客户端无需感知装饰的存在
  • 因为 Decorator HAS-A Component,它可以把调用转发给被持有的对象,并在转发前后添加自己的行为
  • 因为被持有的 Component 本身也可以是一个 Decorator,就形成了递归包装——像俄罗斯套娃一样,一层套一层

抽象装饰类的典型代码——转发一切,不添加任何行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 抽象装饰类——IS-A Component + HAS-A Component
public class Decorator extends Component {
    private Component component;  // 被装饰的对象

    public Decorator(Component component) {
        this.component = component;
    }

    @Override
    public void operation() {
        component.operation();  // 纯转发
    }
}

具体装饰类——在转发的基础上添加新行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 具体装饰类——调用 super 的同时添加新职责
public class ConcreteDecorator extends Decorator {
    public ConcreteDecorator(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        super.operation();  // 先执行原有行为
        addedBehavior();    // 再添加新行为
    }

    public void addedBehavior() {
        // 新增的职责
    }
}

实例:变形金刚

变形金刚在变形之前是一辆汽车,可以在陆地上移动。当它变成机器人后,除了移动还可以说话;变成飞机后,除了移动还可以飞翔。

classDiagram
    class Transform {
        <<interface>>
        +move()* void
    }
    class Car {
        +move() void
    }
    class Changer {
        <<abstract>>
        #transform : Transform
        +Changer(Transform t)
        +move() void
    }
    class Robot {
        +move() void
        +say() void
    }
    class Airplane {
        +move() void
        +fly() void
    }

    Transform <|.. Car
    Transform <|.. Changer
    Changer <|-- Robot
    Changer <|-- Airplane
    Changer --> Transform : transform
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
// 抽象构件——变形金刚接口
public interface Transform {
    void move();
}

// 具体构件——汽车
public class Car implements Transform {
    @Override
    public void move() {
        System.out.println("在陆地上移动");
    }
}

// 抽象装饰类
public abstract class Changer implements Transform {
    protected Transform transform;

    public Changer(Transform transform) {
        this.transform = transform;
    }

    @Override
    public void move() {
        transform.move();  // 委托给被装饰对象
    }
}

// 具体装饰类——机器人
public class Robot extends Changer {
    public Robot(Transform transform) {
        super(transform);
    }

    @Override
    public void move() {
        super.move();  // 保留原有移动能力
        System.out.println("  变形为机器人形态");
    }

    // 新增方法
    public void say() {
        System.out.println("  我是机器人,我能说话!");
    }
}

// 具体装饰类——飞机
public class Airplane extends Changer {
    public Airplane(Transform transform) {
        super(transform);
    }

    @Override
    public void move() {
        super.move();  // 保留原有移动能力
        System.out.println("  变形为飞机形态");
    }

    // 新增方法
    public void fly() {
        System.out.println("  在天空中飞翔!");
    }
}

使用时:

1
2
3
4
5
6
7
8
9
10
Transform camaro = new Car();
camaro.move();
// 输出:在陆地上移动

Robot bumblebee = new Robot(camaro);
bumblebee.move();
// 输出:在陆地上移动
//       变形为机器人形态
bumblebee.say();
// 输出:  我是机器人,我能说话!

注意这里 bumblebee 被声明为 Robot 类型而不是 Transform 类型——因为客户端需要调用 say() 这个新增方法。这属于半透明装饰(后面会详细讨论)。

实例:多重加密系统

一个更能体现装饰模式「层层叠加」威力的例子。系统提供数据加密功能:最简单的是移位加密,可以在此基础上叠加逆序加密,还可以继续叠加求模加密。用户可以自由组合加密层次。

classDiagram
    class Cipher {
        <<abstract>>
        +encrypt(String text)* String
    }
    class SimpleCipher {
        +encrypt(String text) String
    }
    class CipherDecorator {
        <<abstract>>
        #cipher : Cipher
        +CipherDecorator(Cipher c)
    }
    class ComplexCipher {
        +encrypt(String text) String
    }
    class AdvancedCipher {
        +encrypt(String text) String
    }

    Cipher <|-- SimpleCipher
    Cipher <|-- CipherDecorator
    CipherDecorator <|-- ComplexCipher
    CipherDecorator <|-- AdvancedCipher
    CipherDecorator --> Cipher : cipher
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
// 抽象构件
public abstract class Cipher {
    public abstract String encrypt(String text);
}

// 具体构件——最基础的移位加密
public class SimpleCipher extends Cipher {
    @Override
    public String encrypt(String text) {
        StringBuilder sb = new StringBuilder();
        for (char c : text.toCharArray()) {
            if (Character.isLetter(c)) {
                sb.append((char) (c + 3));  // 每个字母移位 3
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }
}

// 抽象装饰类
public abstract class CipherDecorator extends Cipher {
    protected Cipher cipher;

    public CipherDecorator(Cipher cipher) {
        this.cipher = cipher;
    }
}

// 具体装饰类——逆序加密
public class ComplexCipher extends CipherDecorator {
    public ComplexCipher(Cipher cipher) {
        super(cipher);
    }

    @Override
    public String encrypt(String text) {
        String result = cipher.encrypt(text);           // 先用内层加密
        return new StringBuilder(result).reverse().toString();  // 再逆序
    }
}

// 具体装饰类——求模加密
public class AdvancedCipher extends CipherDecorator {
    public AdvancedCipher(Cipher cipher) {
        super(cipher);
    }

    @Override
    public String encrypt(String text) {
        String result = cipher.encrypt(text);  // 先用内层加密
        StringBuilder sb = new StringBuilder();
        for (char c : result.toCharArray()) {
            sb.append(c % 7);                  // 再对每个字符求模
        }
        return sb.toString();
    }
}

关键在于使用方式——装饰器可以自由嵌套,所有变量都声明为 Cipher 类型:

1
2
3
4
5
6
Cipher sc, cc, ac;
sc = new SimpleCipher();           // 仅移位
cc = new ComplexCipher(sc);        // 移位 + 逆序
ac = new AdvancedCipher(cc);       // 移位 + 逆序 + 求模

System.out.println(ac.encrypt("Hello"));  // 三层加密

如果觉得一层够用,就只用 SimpleCipher;如果觉得不够,就套上 ComplexCipher;还不够?再套一层 AdvancedCipher。层层包裹,每一层都在前一层的基础上增加新的加密逻辑——而客户端始终面向 Cipher 类型编程。

优缺点与适用场景

优点:

优点 说明
比继承更灵活 通过组合实现动态扩展,可以在运行时通过配置文件选择不同的装饰器
排列组合的威力 不同的具体装饰类可以自由排列组合,创造出丰富多样的行为
独立变化 具体构件类与具体装饰类可以独立变化,增加新的构件或装饰都不影响已有代码
符合开闭原则 无需修改原有代码就能增加新功能

缺点:

  • 使用装饰模式会产生很多小对象——它们的区别不在于属性不同,而在于相互连接方式不同。这增加了系统复杂度
  • 比继承更容易出错,调试困难——对于多次装饰的对象,寻找错误需要逐级排查,链条越长越烦琐

适用场景:

  • 需要在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
  • 需要动态地给对象增加功能,这些功能也可以动态地被撤销
  • 当不能采用继承的方式扩展系统时——可能是因为组合太多导致子类数量爆炸,也可能是因为类被声明为 final 不允许继承

实际应用:Java IO

装饰模式在 JDK 中最经典的应用当属 Java IO 的流体系。以 InputStream 为例:

classDiagram
    class InputStream {
        <<abstract>>
        +read()* int
        +read(byte[] b) int
        +close() void
    }
    class FileInputStream {
        +read() int
    }
    class ByteArrayInputStream {
        +read() int
    }
    class FilterInputStream {
        <<abstract>>
        #in : InputStream
        +FilterInputStream(InputStream in)
        +read() int
    }
    class BufferedInputStream {
        +read() int
    }
    class DataInputStream {
        +read() int
        +readInt() int
        +readDouble() double
    }

    InputStream <|-- FileInputStream
    InputStream <|-- ByteArrayInputStream
    InputStream <|-- FilterInputStream
    FilterInputStream <|-- BufferedInputStream
    FilterInputStream <|-- DataInputStream
    FilterInputStream --> InputStream : in

角色分配一目了然:

装饰模式角色 Java IO
抽象构件 InputStream
具体构件 FileInputStreamByteArrayInputStream
抽象装饰类 FilterInputStream
具体装饰类 BufferedInputStreamDataInputStream

FilterInputStream 的实现正是典型的抽象装饰类——持有一个 InputStream 引用,所有方法都转发给它:

1
2
3
4
5
6
7
8
9
10
11
12
// JDK 源码(简化)
public class FilterInputStream extends InputStream {
    protected volatile InputStream in;  // 被装饰的流

    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

    public int read() throws IOException {
        return in.read();  // 纯转发
    }
}

具体装饰类在此基础上增加功能:BufferedInputStream 增加了缓冲,DataInputStream 增加了读取基本类型的能力。使用时层层包装:

1
2
3
4
5
6
7
// 从文件读取 → 加上缓冲 → 读取基本类型
FileInputStream fis = new FileInputStream("data.bin");
BufferedInputStream bis = new BufferedInputStream(fis);  // 装饰:加缓冲
DataInputStream dis = new DataInputStream(bis);          // 装饰:读基本类型

int value = dis.readInt();    // 享受缓冲 + 基本类型读取的能力
dis.close();

每一层装饰都是透明的——DataInputStream 不关心它包的是 BufferedInputStream 还是裸的 FileInputStreamBufferedInputStream 也不关心它包的是文件流还是网络流。这种设计让 IO 组件可以像积木一样自由拼装。

Swing 中的装饰

Swing 组件也使用了装饰模式。例如 JList 本身不支持滚动,想要滚动效果,只需用 JScrollPane 装饰一下:

1
2
JList list = new JList();
JScrollPane sp = new JScrollPane(list);  // 装饰:加滚动条

JScrollPane 就是 JList 的装饰器,它在不修改 JList 的前提下为其增加了滚动功能。

透明装饰与半透明装饰

在实际使用装饰模式时,有一个重要的设计选择:客户端应该声明什么类型?

透明装饰模式

在透明装饰模式中,客户端完全针对抽象构件编程——所有对象都声明为抽象构件类型,客户端不知道也不关心对象是否被装饰过。

多重加密系统就是透明装饰的典型:

1
2
3
4
5
Cipher sc, cc, ac;                    // 全部声明为 Cipher 类型
sc = new SimpleCipher();
cc = new ComplexCipher(sc);
ac = new AdvancedCipher(cc);
ac.encrypt("Hello");                  // 客户端只看到 Cipher 接口

透明装饰的好处是客户端代码完全统一,装饰的添加和移除不影响客户端。缺点是客户端无法调用装饰类新增的方法——因为变量类型是抽象构件,编译器看不到具体装饰类的新方法。

半透明装饰模式

半透明装饰模式(Semi-transparent Decorator)允许客户端声明具体装饰类型,从而可以调用装饰类中新增的方法。

变形金刚就是半透明装饰的典型:

1
2
3
4
5
6
Transform camaro = new Car();          // 抽象类型
camaro.move();

Robot bumblebee = new Robot(camaro);   // 具体装饰类型——半透明
bumblebee.move();
bumblebee.say();                       // 调用新增方法

如果把 bumblebee 声明为 Transform 类型,就无法调用 say() 了。半透明装饰牺牲了一部分透明性,但换来了使用新功能的能力。

维度 透明装饰 半透明装饰
客户端声明类型 抽象构件类型 具体装饰类型
新增方法 不可调用 可调用
一致性 装饰前后完全一致 客户端需要知道具体装饰类
适用场景 只需增强原有方法 需要添加全新方法

简化的装饰模式

如果系统中只有一个具体构件类而没有抽象构件类,可以对装饰模式进行简化——让抽象装饰类直接作为具体构件类的子类:

classDiagram
    class ConcreteComponent {
        +operation() void
    }
    class Decorator {
        <<abstract>>
        -component : ConcreteComponent
        +Decorator(ConcreteComponent c)
        +operation() void
    }
    class ConcreteDecoratorA {
        +operation() void
    }

    ConcreteComponent <|-- Decorator
    Decorator <|-- ConcreteDecoratorA
    Decorator --> ConcreteComponent : component

省去了 Component 抽象层,结构更简洁。但这种简化降低了灵活性——如果未来要增加新的具体构件类,就需要重构。

使用装饰模式的注意事项

  • 装饰类的接口必须与被装饰类保持一致——对客户端来说,装饰前后的对象应该可以互换使用
  • 保持具体构件类「轻量」——不要在具体构件类中放太多逻辑和状态,复杂功能应该通过装饰器添加

桥接与装饰的对比

本节的两种模式都属于结构型模式,都使用了组合代替继承的思想,但解决的问题截然不同:

维度 桥接模式 装饰模式
核心问题 两个独立维度的组合爆炸 动态地给对象增加职责
关键机制 将继承拆成两个独立的层次结构,用组合连接 递归包装,一层套一层
类数量优化 M×NM \times NM+NM + N 避免为每种功能组合创建子类
组合关系 Abstraction 持有 Implementor Decorator 持有 Component
扩展方式 在任意维度新增子类 新增装饰器类
别名 Handle and Body Wrapper
典型场景 跨平台系统、双维度变化 Java IO、动态功能叠加

两者有一个有趣的交叉点:桥接模式中的「Abstraction 持有 Implementor 引用」和装饰模式中的「Decorator 持有 Component 引用」在结构上非常相似。但意图完全不同——桥接是为了让两个维度独立变化,装饰是为了动态叠加功能。结构相似不等于模式相同,设计模式的核心在于意图

设计工具箱更新:

层次 内容
OO 基础 抽象、封装、多态、继承
OO 原则 封装变化、面向接口编程、组合优于继承、好莱坞原则
OO 模式 策略、简单工厂、工厂方法、抽象工厂、建造者、原型、状态、命令、观察者、中介者、模板方法、适配器、组合、桥接装饰