设计模式学习笔记 - 装饰模式(Decorator)

前言

  装饰模式(decorator pattern)又名装饰器模式包装器模式(wrapper pattern)
  同样地,在初次学习一个从未接触过的设计模式,我们最好首先是从字面意思去进行思考:装饰很明显是个动作,抽象来说指的是给一个事物附加上另外一些事物。在生活中,“装饰”这种动作的具体形式是很常见的:给加一个名词加上形容词进行修饰一个人化妆打扮给毛胚房进行装修等等。

概念

意图

其实在设计模式描述中,装饰模式的描述和上面这些描述也差不了多少。

在《设计模式 - 可复用面向对象软件的基础》中是这样描述装饰模式的意图:

动态地给一个对象添加额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。

而《Thinking in Patterns》中是这样描述的:

用分层的对象(layered objects)动态地和透明地给单个对象添加职责功能(responsibilities)

至于网上大多博客文章里对它的描述都和上面这2个差不多,这里就不再一一列举。

综上所述,装饰模式主要的作用就是:一个对象添加额外功能

适用场景

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责,同时这些功能职责也能被动态透明地撤销。

  • 当不能继续采用继承这种方式进行功能扩展或继承这种方式不利于功能的扩展和后期模块的维护。这分2种情况:

    • 有大量独立的功能,为了支持每一种功能组合而产生大量的子类,使得子类数目呈爆炸式增长。
    • 类定义被隐藏,或类定义不能用于生成子类。

优点

  • 灵活性:就扩展一个对象的功能来说,装饰模式比继承更加灵活,它不像继承那样会导致类的数量急剧增加。
    具体来说就是利用组合对象的方式进行功能扩展

  • 动态性:在运行时,可以根据当前需求给一个对象附加或撤销某些功能,并且功能数量以及先后顺序都没有限制。
    就Java来说,表示我们可通过配置文件决定在运行时创建什么功能附加到某个对象上,并且这些功能数量也没有限制,可以在运行时任意进行组合。

缺点

  • 在使用装饰模式时会创建比较多小对象,所以比较占用系统资源。

  • 更灵活的解决方案也意味着内部代码的复杂性提高了,这也意味着调试排错将会更加困难。

必须弄懂的一个词

  或许你看到透明的这一个词,根本不知道它到底要表达什么意思,只能是一脸懵逼。
  其实你不用苦思冥想,只要想想最简单的意思立马就豁然开朗了:生活中的空气,一眼望去,对于空气的实体我们什么都看不到,因为空气本身的构造是透明的,但空气又是客观存在于我们的周围,并且我们还在使用它们。换成编程思想来说的就是实现细节是被隐藏的(即透明的),只有接口是被暴露的(我们可以使用的)
  其实说白了就是面向对象里封装这一回事。

结构、组成与实现

结构

其实装饰模式的结构也算比较简单的,它的样子看起来其实和代理模式、适配器模式,下面是装饰模式抽象描述下的UML图。

组成

最经典的装饰模式含有以下这些元素。

  • Component(抽象组件)
    它是具体组件和抽象装饰类的共同接口,其中声明了具体组件中实现的业务方法。
    它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。

  • ConcreteComponent(具体组件)
    它是实现抽象组件接口的子类,用于定义具体组件对象,装饰器可以给它增加额外的职责(功能)。

  • Decorator(抽象装饰器)—— 抽象装饰器在实际应用过程中并非必须的。
    它是实现抽象组件接口的抽象类,它用于给具体构建增加职责,但是具体职责在其子类中实现。
    它维护了一个指向抽象组件对象的引用,通过该引用可以调用装饰之前组件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。

  • ConcreteDecorator(具体装饰器)
    它是继承抽象装饰器类的子类,负责向组件添加新的职责。
    每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法以扩展对象的行为。
    就是说:具体装饰器将请求转发给自己所维护的组件,或许在转发请求之前或之后执行一些额外的操作。

实现

抽象组件

1
2
3
public interface Component {
void operation();
}

正因为有了这个接口的抽象描述,所以在客户端可以透明而动态地给一个对象附加任意多的责任。

具体组件

1
2
3
4
5
6
public class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("正在实现最基本的需求");
}
}

一个具体组件代表着一种基本功能,所以真实情况下应用装饰模式,组件可以说有多个。

抽象装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Decorator implements Component {
private Component component; // 维护了一个抽象组件

// 在构造装饰器时传入抽象组件
public Decorator(Component component) {
this.component = component;
}

@Override
public void operation() {
component.operation(); // 只调用抽象组件所具有的功能
}
}

这个抽象类Decorator正是整个装饰模式核心。
从实现来看,它也只不过是调用了组件原有的功能,并没有增添新功能,所以这个类并没有做出装饰这一个动作,它只是整个模式的框架。

具体装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConcreteDecoratorA extends Decorator {
private int addedState;

public ConcreteDecoratorA(Component component) {
super(component);
addedState = 3;
}

@Override
public void operation() {
System.out.println("被具体装饰器A增加了额外功能: 多增加了一个数据成员 -> addedState = " + addedState);
super.operation();
System.out.println("被具体装饰器A装饰结束");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}

@Override
public void operation() {
addedBehavior();
super.operation();
System.out.println("被具体装饰器B装饰结束");
}

private void addedBehavior() {
System.out.println("被具体装饰器B增加了额外功能: 增加了一个方法 -> addedBehavior()");
}
}

两个具体装饰器类都是继承了抽象装饰器类,在调用原有功能的同时又增加了新功能,所以说,具体装饰器中就为组件作出装饰这一个动作
A、B两个不同的装饰器给组件”装饰”上不同的功能,这些新功能都默认加到了原功能之后。

现在装饰模式该有的元素都有了,剩下的工作就是如何来使用它们。

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
public class Client {
public static void main(String[] args) {
// 逐步添加功能
// 创建将要被装饰的具体组件对象
Component component = new ConcreteComponent();
component.operation();

System.out.println("------------------------");

// 给具体组件对象透明地增加功能A
component = new ConcreteDecoratorA(component);
component.operation();

System.out.println("------------------------");

// 给具体组件对象透明地增加功能B
component = new ConcreteDecoratorB(component);
component.operation();

System.out.println("------------------------");

// 一步到位添加所有功能
// 透明地给具体组件对象增加功能B, 然后增加功能A
component = new ConcreteDecoratorA(new ConcreteDecoratorB(new ConcreteComponent()));
component.operation();
}
}

输出结果:

正在实现最基本的需求
------------------------
被具体装饰器A增加了额外功能: 增加了一个数据成员 -> addedState = 3
正在实现最基本的需求
被具体装饰器A装饰结束
------------------------
被具体装饰器B增加了额外功能: 增加了一个方法 -> addedBehavior()
被具体装饰器A增加了额外功能: 增加了一个数据成员 -> addedState = 3
正在实现最基本的需求
被具体装饰器A装饰结束
被具体装饰器B装饰结束
------------------------
被具体装饰器A增加了额外功能: 增加了一个数据成员 -> addedState = 3
被具体装饰器B增加了额外功能: 增加了一个方法 -> addedBehavior()
正在实现最基本的需求
被具体装饰器B装饰结束
被具体装饰器A装饰结束


程序过程是非常简单,我想你也是一眼就能看明白的,但分析还是必要的:

  • 首先创建了一个具体组件对象,并调用了它所具有的功能。
    这个简单具体组件所具有的功能很简单,就输出一句话正在实现最基本的需求然后就没了。

  • 然后,我用具体装饰器AConcreteDecoratorA来对这个具体组件component进行“装饰”(实际上就是将具体组件对象传递进去让它维护)。
    经过“装饰”后的组件又重新赋值给component(把它读作被装饰器A装饰后的具体组件),然后调用它的功能方法,然后就有如下输出信息:

    被具体装饰器A增加了额外功能: 增加了一个数据成员 -> addedState = 3
    正在实现最基本的需求
    被具体装饰器A装饰结束e = 3
    

    这个输出结果很显然表明了原来那个具体组件的功能被“装饰”过了。
    从程序客户端的角度出发,我们并没有改动组件对象,但“装饰”后再次使用它同一个功能时,就发现功能被加强了——这正是装饰模式达到的效果。

  • 接着,我又用具体装饰器BConcreteDecoratorB将刚才已经被ConcreteDecoratorA装饰过的组件再“装饰”一遍,
    经过再次“装饰”后的组件又重新赋值给component

    被具体装饰器B增加了额外功能: 增加了一个方法 -> addedBehavior()
    被具体装饰器A增加了额外功能: 增加了一个数据成员 -> addedState = 3
    正在实现最基本的需求
    被具体装饰器A装饰结束
    被具体装饰器B装饰结束
    

    对于一个组件对象,我们可以给它附加上很多很多的功能(即多次“装饰”)。

  • 最后,我将之前分开的“装饰”步骤都合并成一步来做,但“装饰”过程是倒过来的,之前是先A后B,现在是先B后A,也是调用同样的功能方法:

    被具体装饰器A增加了额外功能: 增加了一个数据成员 -> addedState = 3
    被具体装饰器B增加了额外功能: 增加了一个方法 -> addedBehavior()
    正在实现最基本的需求
    被具体装饰器B装饰结束
    被具体装饰器A装饰结束
    

    我们可以非常清楚看到输出信息的顺序改变了。
    附加功能执行的顺序与“装饰”顺序是有关系的,不过想必你也清楚这同样也和方法调用的顺序相关。

并非一定要循规蹈矩

上面那个是最典型的装饰模式抽象实现,但这只是最典型的模版,在实际应用中我们并非一定要照搬这套模版来使用。

在实际开发应用中,如果无法套用模版,那么一般会用以下这些做法:

  • 如果并没有抽象组件,那么就直接让装饰器继承具体组件
  • 抽象装饰器并非必须的,所以如果发现一个装饰器体系中没有抽象装饰器你也不必感到意外。

实际应用

装饰模式抽象描述就看完了,那到底怎么来应用它呢?

现在就来实现一个小小的应用例子,代码所实现的功能非常非常简单,这例子只是为了让咱比较轻松地感受下装饰模式的作用。

先来看一个小程序,它读取该程序的源码文件,每次都读取一行,并输出到控制台上。

直接对实现进行改动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.makwan.c_decorator_pattern.example;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class Client {
public static void main(String[] args) throws IOException {
String fileName = "src\\com\\makwan\\c_decorator_pattern\\example\\Client.java";
BufferedReader reader = new BufferedReader(new FileReader(fileName));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
}
}

此刻老板有个需求:在输出每一行内容的开头都必须带上行号

那解决方案非常简单啊,我们不就是继续修改main方法内的代码,多加一个变量用于存储行号即可,程序成了这样。

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) throws IOException {
String fileName = "src\\com\\makwan\\c_decorator_pattern\\example\\Client.java";
BufferedReader reader = new BufferedReader(new FileReader(fileName));
String line;
int lineNumber = 1;
while ((line = reader.readLine()) != null) {
System.out.println(lineNumber++ + " " + line);
}
reader.close();
}
}

好了现在输出源码文件每一行内容前都加上行号了,但老板又改变了想法:不要行号了,不如用双引号把每一行的内容都包裹起来

好的,还是直接对main方法进行改动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) throws IOException {
String fileName = "src\\com\\makwan\\c_decorator_pattern\\example\\Client.java";
BufferedReader reader = new BufferedReader(new FileReader(fileName));
String line;
while ((line = reader.readLine()) != null) {
if (line.equals("")) {
System.out.println();
} else {
System.out.println("\"" + line + "\"");
}
}
reader.close();
}
}

现在可好了,在输出每一行内容时,有内容一行用双引号包裹起来,没内容的一行则直接换行。

但老板的需求真的是说变就变,在你改动演示过后下一秒需求又变了:不如还是加在一行的最前面上行号吧,然后内容还是用双引号括起来

我们作为员工当然只能是默默地再改啊,老板需要你做事,照做就是了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
public static void main(String[] args) throws IOException {
String fileName = "src\\com\\makwan\\c_decorator_pattern\\example\\Client.java";
BufferedReader reader = new BufferedReader(new FileReader(fileName));
String line;
int lineNumber = 1;
while ((line = reader.readLine()) != null) {
if (line.equals("")) {
System.out.println(lineNumber++);
} else {
System.out.println(lineNumber++ + " \"" + line + "\"");
}
}
reader.close();
}
}

现在,输出结果是这样的:

1 "package com.makwan.c_decorator_pattern.example;"
2
3 "import java.io.BufferedReader;"
4 "import java.io.FileReader;"
5 "import java.io.IOException;"
6
7 "public class Client {"
8 "    public static void main(String[] args) throws IOException {"
9 "        String fileName = "src\\com\\makwan\\c_decorator_pattern\\example\\Client.java";"
10 "        BufferedReader reader = new BufferedReader(new FileReader(fileName));"
11 "        String line;"
12 "        int lineNumber = 1;"
13 "        while ((line = reader.readLine()) != null) {"
14 "            if (line.equals("")) {"
15 "                System.out.println(lineNumber++);"
16 "            } else {"
17 "                System.out.println(lineNumber++ + " \"" + line + "\"");"
18 "            }"
19 "        }"
20 "        reader.close();"
21 "    }"
22 "}"

老板看到结果后也暂时满意了,工作也暂时也告一段落。

使用继承的方式进行代码复用

  工作告一段落后静下来想想,之前那样来解决问题只能是解决一时的燃眉之急,需求以后肯定还会千变万化的,如果老板又要求改回之前的需求咋办?所以如果一直直接修改实现,那代码就得不到复用,随着需求越来越多,我们所做的工作将会是大量重复的。有没有不用修改客户端的核心功能部分,而又能执行新添加的新功能呢?

核心功能正是这一小段代码:读取一行内容,输出一行

1
2
3
4
5
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();

要解决这个问题的最直接办法就是利用继承这一手段进行代码复用:
直接继承BufferedReader,并重写readLine方法,将每个特殊需求的实现都抽取到子类所重写的readLine方法中

单纯只在一行内容的开头加行号的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.makwan.c_decorator_pattern.example;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;

public class LineNumberReader extends BufferedReader {
private int lineNumer = 0;

public LineNumberReader(Reader in) {
super(in);
}

@Override
public String readLine() throws IOException {
String line = super.readLine();
if (line == null)
return null;
return ++lineNumer + " " + line;
}
}

用双引号包裹每一行内容的实现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.makwan.c_decorator_pattern.example;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;

public class WithinDoubleQuotation extends BufferedReader {
public WithinDoubleQuotation(Reader in) {
super(in);
}

@Override
public String readLine() throws IOException {
String line = super.readLine();
if (line == null)
return null;
if (line.equals(""))
return "";
return "\"" + line + "\"";
}
}

现在使用继承来实现最后一个需求:即在每一行的开头加上行号同时行中的内容要用双引号包裹起来。
但Java中并不能直接用多重继承来复用之前的代码(间接方式还是有的,就是很麻烦),所以说只能是再定义一个子类的同时,将之前所实现过的内容再实现一遍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.makwan.c_decorator_pattern.example;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;

public class WithLNAndWithinDQ extends BufferedReader {
private int lineNumber = 0;

public WithLNAndWithinDQ(Reader in) {
super(in);
}

@Override
public String readLine() throws IOException {
String line = super.readLine();
if (line == null)
return null;
if (line.equals(""))
return ++lineNumber + "";
return ++lineNumber + " \"" + line + "\"";
}
}

所以在客户端中核心部分并不用变动,变也只是根据需求创建对象那句要变。

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) throws IOException {
String fileName = "src\\com\\makwan\\c_decorator_pattern\\example\\Client.java";
BufferedReader reader = new WithLNAndWithinDQ(new FileReader(fileName));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
}
}

  现在你觉得毫无问题了,每种需求的实现被抽取到各个子类的方法中,以后也能随时复用了。确实,这挺好的,看上去没问题了,但也只是暂时的。如果以后老板还要添加各种想法新奇的需求:有时让我们增加单一功能点,有时候让我们组合之前所增加的单一功能点。这会发生什么?增加单一功能点会让子类增加一点点,但如果还要给它们进行组合,那么子类的数量会爆炸式地增长。并且,组合功能越多,重复代码也会越多。
  看完这描述后,你不觉得是如此?那么有一个奇葩的需求:行号不放在最前了,不如放到最后吧,如果还是用继承,那么子类又要多一个了。组合可以说是无穷无尽的,所以只用继承这种手段并不是最佳的解决方案。

运用装饰模式

其实JavaI/O体系也是运用到装饰模式的,所以我们可以效仿这种做法便可。

1
2
3
4
public class BufferedReader extends Reader {
private Reader in;
...
}

按照之前我们的说法就是BufferedReader就是一个具体装饰器,Reader是一个