装饰模式(Decorator Pattern)是一种结构型设计模式,它允许在不改变原有对象结构的情况下,动态地给对象添加新功能。装饰模式通过创建 “装饰器” 类来包裹原始对象,从而在保持接口一致性的同时,实现功能的扩展。

打个比方,装饰模式就像 “手机壳”—— 手机本身的功能不变,但加上不同的手机壳(装饰器)可以获得防摔、美观、无线充电等额外功能,且可以自由组合多个装饰器。

装饰模式的UML类图如下:
image.png

从上面的类图中我们可以看出,在装饰模式中:

  1. Component:是一个抽象组件,它定义了被装饰对象的公共接口。也就是说,客户端能访问的被装饰组件的通用的方法。
  2. Concretecomponent:是实际上的被装饰对象,它实现了Component接口,也意味着每个实际的被装饰对象必须实现抽象的公共接口中的方法,也就是通用功能必须具备。
  3. Decorator:装饰器的抽象类,在这里它要持有某个被装饰的对象的抽象,然后通过后面的具体装饰器继承它来实现被装饰组件功能的叠加。
  4. ConcreteDecorator:具体的装饰器,它通过继承Decorator来给被装饰组件增加额外的功能。

我们还是通过一个具体的例子来理解它。比如,我们喝的咖啡有各种口味的,其实都是在基础的咖啡液中增加奶、糖、巧克力等配料来形成不同的口味,当然价格也会不一样。我们可以抽象出一个咖啡产品都有“什么口味和多少钱一杯”这两个信息。
那么,首先我们来创建咖啡这个被装饰组件的抽象接口:

1
2
3
4
5
// 抽象组件:咖啡
interface Coffee {
String getDescription(); // 获取描述
double getCost(); // 获取价格
}

那么,最基本的开发可能是浓缩咖啡,我们来实现它:

1
2
3
4
5
6
7
8
9
10
11
12
// 具体组件:基础咖啡
class Espresso implements Coffee {
@Override
public String getDescription() {
return "浓缩咖啡";
}

@Override
public double getCost() {
return 25.0; // 基础价格
}
}

接下来,加入不同的配料就有了不同口味和价格的咖啡,因此我们可以”装饰“这个咖啡了。
先来创建一个抽象的咖啡装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee; // 被装饰的咖啡

public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}

@Override
public String getDescription() {
return coffee.getDescription();
}

@Override
public double getCost() {
return coffee.getCost();
}
}

这个装饰器继承了Coffee类,也就是说,无论是怎么装饰,它本身都是一杯咖啡。
接下来我们创建牛奶咖啡的装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}

@Override
public String getDescription() {
return coffee.getDescription() + " + 牛奶";
}

@Override
public double getCost() {
return coffee.getCost() + 5.0; // 牛奶价格
}
}

从代码中我们不难看出,牛奶咖啡首先必须是一种咖啡,所以我们在构造的时候要把coffee参数传入进来,这部分是不变的。然后,牛奶咖啡的描述和价格是其特有的,这部分是变动的,我们在装饰器里完成这部分变动的内容。
以此类推,我们继续实现加糖加巧克力 的装饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 具体装饰器:糖
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}

@Override
public String getDescription() {
return coffee.getDescription() + " + 糖";
}

@Override
public double getCost() {
return coffee.getCost() + 2.0; // 糖价格
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 具体装饰器:巧克力
class ChocolateDecorator extends CoffeeDecorator {
public ChocolateDecorator(Coffee coffee) {
super(coffee);
}

@Override
public String getDescription() {
return coffee.getDescription() + " + 巧克力";
}

@Override
public double getCost() {
return coffee.getCost() + 8.0; // 巧克力价格
}
}

现在关于这个咖啡的装饰器我们已经做好了,客户来点咖啡的时候,就可以根据要求加不同的配料来。在实际的软件开发中,也就是给一个组件增加不同的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 客户端:咖啡店订单
public class CoffeeShop {
public static void main(String[] args) {
// 点一杯基础浓缩咖啡
Coffee coffee = new Espresso();
System.out.println(coffee.getDescription() + ",价格:" + coffee.getCost() + "元");

// 加牛奶
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + ",价格:" + coffee.getCost() + "元");

// 再加巧克力
coffee = new ChocolateDecorator(coffee);
System.out.println(coffee.getDescription() + ",价格:" + coffee.getCost() + "元");

// 再加点糖
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + ",价格:" + coffee.getCost() + "元");
}
}

从例子中我们可以看出,装饰器的优势在于:

  • 动态扩展:可以在运行时为对象添加功能,如示例中动态组合各种配料
  • 灵活组合:装饰器可以任意组合,实现多种功能组合(如牛奶 + 糖、巧克力 + 牛奶等)
  • 避免类爆炸:相比为每种组合创建单独的类(如加牛奶的咖啡、加牛奶和糖的咖啡),装饰模式大大减少了类的数量。