本篇我们将介绍一个有意思的设计模式,装饰器模式。如果你想快速记忆装饰器模式,那么只需要抓住一个特征:装饰器模式可以像俄罗斯套娃一样,将对象层层嵌套。

一、问题引入和解决方案

1.1 继承带来类爆炸

我家附件有很多早餐店:餐馆里有面条(8元/份)、米线(7元/份)、抄手(6元/份)、混沌(10元/份)等主食,有牛肉(3元/份)、海鲜(5元/份)、羊肉(5元/份)、酸菜(2元/份)等辅料。现在有客人进入餐馆,任意点餐,包含有(一种)主食和(一种或多种)辅料,如何计算出这份早餐花费的价格和描述这份早餐?

我们可以定义一个接口(接口中包含两个行为:计算价格,描述早餐),然后列举出每一种早餐,就像下面这样:
结构型 - 装饰器模式(Decorator) - 图1
我们很快就意识到一些问题:

(一)当前我们仅有 4 种主食,4 种辅料,那么任意一种主食和辅料的搭配组合就有 16 种了,如果某一天店里新开了一种新的主食或辅料,我们不得不为其新增多个类表示; (二)顾客在点餐时,很有可能需要多种辅料,比如客户点了一份酸菜牛肉米线。此时,我们不得不考虑同一种主食搭配多种辅料的情况了; (三)此时,进来一位客人:老板,我要一份羊肉酸菜面,我喜欢吃酸,酸菜要双倍!!!

正如上面这样,我们终于发现这个思路并不能解决实际问题。我们在处理主食和辅料的搭配时就已经引入了太多的类表示,更别提一种主食搭配多种辅料的情况,现在还不得不考虑客人在点餐时期望辅料加倍的需求。一旦我们增加一种主食或辅料,系统中类的数量将呈现几何式增长,这就是通俗说的类爆炸。那么我们该如何解决这个问题呢?

1.2 解决方案

让我们回想一下,厨师是如何制作一碗双倍酸菜的牛肉面条的呢?第一步,面条煮熟捞入碗中;第二步,加入一份牛肉;第三步,加入一份酸菜;第四步,再加入一份酸菜。让我们仔细分析这个过程:

  1. 在面条煮熟捞入碗中时,这已经就是一份早餐了,价格就是面条的价格;
  2. 厨师往碗中加入一份牛肉,它仍然是一份早餐,区别在于这份早餐中加了一份牛肉,而价格在原来的基础上增加了一份牛肉的价格;
  3. 厨师再往碗中加入一份酸菜,还是一份早餐,仅是在原来的早餐中又加了一份酸菜,而价格也是在原来的基础上增加了一份酸菜的价格;
  4. 以此往复。

对于一份早餐来说,每一次往其中加入辅料,变化的价格是确定的:此此加入辅料的价格。不管加何种辅料,加了多少次辅料,其本质都还是早餐。所以,如果将一份早餐看成一个对象 A,往早餐中放入一份辅料后的早餐是对象 B,那么对象 B 可以看成是在对象 A 的基础之上包装了一份辅料,所以价格就是 A 的价格和 B 中包含辅料的价格之和。
所以,如果我们想要计算一份早餐的价格,我们需要知道的是:每一个上一次往碗中加入了什么辅料。这是一个不断递归的过程,递归求解的终点是:上一次往碗中加入的是主食。
根据上面的分析,我们不难总结出以下几个要点:

  • 从碗中加入主食开始,每一次加入辅料都没有改变这是一份早餐的本质;
  • 食材分为两种:主食和辅料,一份早餐中主食有且仅有一份,辅料可无限叠加;
  • 对于一份早餐的价格,计算方式就是不断回溯,直到最原始的主食,再根据每一次加入的辅料,价格叠加。

1.3 求解过程

针对上面总结的分析,我们对一份早餐价格的计算过程就像下图所示:
aaa.png

二、实现解决方案

2.1 类图结构

根据解决方案中总结的要点,我们设计出如下的类图结构:
结构型 - 装饰器模式(Decorator) - 图3
在前面我们提到,每一次加入辅料都是在新对象中包装了原来的对象,所以在辅料类(BreakfastDecorator)中维护一个对于原来早餐对象的引用(breakfast)。例如 o3 表示在 o2 基础上加入一份辅料后的早餐,那么 o3 价格 = o3 所表示辅料的价格 + o2 的价格,以此类推。不管如何往碗中加入辅料,都是早餐,所以所有的对象继承自 Breakfast 类。主食总是第一个加入碗中,主食代表着递归的终点,所以主食没有对于维护另一个早餐对象的引用。这就是装饰器模式。

2.2 实现代码

为了简化代码,我们主食只考虑面条和粉条,辅料只包含牛肉、羊肉和酸菜。

2.2.1 早餐定义

  1. public interface Breakfast {
  2. /**
  3. * 描述
  4. * @return 早餐的内容
  5. */
  6. String getDescription();
  7. /**
  8. * 计算费用
  9. * @return 早餐的价格
  10. */
  11. int cost();
  12. }

2.2.2 主食

  1. public class Noodles implements Breakfast {
  2. @Override
  3. public String getDescription() {
  4. return "面条";
  5. }
  6. @Override
  7. public int cost() {
  8. return 8;
  9. }
  10. }
  1. public class Vermicelli implements Breakfast {
  2. @Override
  3. public String getDescription() {
  4. return "粉条";
  5. }
  6. @Override
  7. public int cost() {
  8. return 7;
  9. }
  10. }

2.2.3 辅料抽象

  1. public abstract class BreakfastDecorator implements Breakfast {
  2. /**
  3. * 包装的具体组件
  4. */
  5. protected Breakfast breakfast;
  6. public BreakfastDecorator(Breakfast breakfast) {
  7. this.breakfast = breakfast;
  8. }
  9. }

2.2.4 辅料

  1. public class BeefDecorator extends BreakfastDecorator {
  2. public BeefDecorator(Breakfast breakfast) {
  3. super(breakfast);
  4. }
  5. @Override
  6. public String getDescription() {
  7. return this.breakfast.getDescription() + " + 牛肉";
  8. }
  9. @Override
  10. public int cost() {
  11. return this.breakfast.cost() + 3;
  12. }
  13. }
  1. public class MuttonDecorator extends BreakfastDecorator {
  2. public MuttonDecorator(Breakfast breakfast) {
  3. super(breakfast);
  4. }
  5. @Override
  6. public String getDescription() {
  7. return this.breakfast.getDescription() + " + 羊肉";
  8. }
  9. @Override
  10. public int cost() {
  11. return this.breakfast.cost() + 5;
  12. }
  13. }
  1. public class SauerkrautDecorator extends BreakfastDecorator {
  2. public SauerkrautDecorator(Breakfast breakfast) {
  3. super(breakfast);
  4. }
  5. @Override
  6. public String getDescription() {
  7. return this.breakfast.getDescription() + " + 酸菜";
  8. }
  9. @Override
  10. public int cost() {
  11. return this.breakfast.cost() + 2;
  12. }
  13. }

2.2.5 客户端

  1. public class Client {
  2. public static void main(String[] args) {
  3. System.out.println("|==> Start ---------------------------------------------|");
  4. System.out.println(" Tom 点了一份牛肉面");
  5. Breakfast breakfast4Tom = new BeefDecorator(new Noodles());
  6. System.out.println(" 花费:" + breakfast4Tom.cost());
  7. System.out.println(" 食材包含有:" + breakfast4Tom.getDescription());
  8. System.out.println(" Jack 点了一份酸菜羊肉米粉,酸菜要了双份");
  9. Breakfast breakfast4Jack = new SauerkrautDecorator(new SauerkrautDecorator(new MuttonDecorator(new Vermicelli())));
  10. System.out.println(" 花费:" + breakfast4Jack.cost());
  11. System.out.println(" 食材包含有:" + breakfast4Jack.getDescription());
  12. }
  13. }
  1. |==> Start ---------------------------------------------|
  2. Tom 点了一份牛肉面
  3. 花费:11
  4. 食材包含有:面条 + 牛肉
  5. Jack 点了一份酸菜羊肉米粉,酸菜要了双份
  6. 花费:16
  7. 食材包含有:粉条 + 羊肉 + 酸菜 + 酸菜

三、装饰器模式

3.1 类图结构

让我们看一下更加通用的装饰器模式的类图结构:
结构型 - 装饰器模式(Decorator) - 图4
装饰器模式的参与角色如下:

  • Component:组件,定义一个对象接口。类比例子中的早餐接口,不管是主食还是辅料,都是早餐;
  • ConcreteComponent:具体的组件。类比例子中的面条;
  • Decorator:抽象的装饰器,本身是组件的实现,同时也包装了一个组件。类比例子中的辅料抽象类,本身是早餐,也装饰了另一份早餐;
  • ConcreteDecorator:具体的装饰器实现。类比例子中的酸菜、牛肉等辅料;

    3.2 意图

    指在不改变原有对象结构的基础情况下,动态地给该对象增加一些额外功能的职责。

装饰器模式的核心就是在运行时,不停的给一个对象添加一些功能,操作方式将一个包装好的对象作为原始对象再次进行包装。

想象一下,我们煮了一碗面条,往面条里面放入一勺牛肉,这样就变成了牛肉面;再往面条里面放入一勺酸菜,这样就变成了酸菜牛肉面。这就很像中文里面的形容词和名词的关系,一个名词可以被多个形容词进行修饰。

四、在源码中看装饰器模式

我们在各种源码中就能看到装饰器模式的影子。
(1)例如,java.io包下面的 InputStream 类图大致如下(篇幅原因,只放了部分类上去)
InputStream.png
(2)再比如,在 javax.servelet 包下,ServletRequestWrapper 是 ServletRequest 接口的装饰器实现,开发者可以继承 ServletRequestWrapper 去扩展原来的 ServletRequest。
image.png

附录

案例代码:…/decorator