0%

设计模式之Decorator模式

转自
感觉这位博主讲的很清楚了

前言

装饰者模式动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活.其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合.

第一次听说装饰器还是在学python的时候, 装饰器模式在java以及程序设计中也是占据着重要的地位.比如Java的数据流处理,我们可能看到数据流经过不同的类的包装和包裹,最终形成了我们需要的流,比如说从二进制到字节流再到字符流,这中间其实就是经过了装饰器的处理,在不改变原来的数据的基础上,加入属于自己的特征,就像是在一块蛋糕上加上一些水果等装饰品,这样输出的结果就不同了,我们将这种能产生类似于洋葱一样层层包裹的数据格式的设计模式成为装饰器模式.

那么为什么装饰器这样神奇呢,几乎可以让我们无穷尽的包裹自身,在计算机中凡是能够无穷尽的重复某一件事物,必然不能设定固定的数据,只能按照用户的随意设置来计算,那么递归的魅力就在此彰显出来了,通过递归可以实现这一点,那么如何递归呢,我们想到一层套一层的数据结构(链表、二叉树等),就必须在“自身”中包含“自身”;前一个“自身”可以是我们的子类,因为子类有着父类的所有特点,后一个“自身”就是具有最抽象资格的“自身”,正是因为足够抽象使得任何继承与这个抽象类的类都能通过里氏代换原则转换到这个抽象类.实现了递归,我们只需要停止条件就可以了,停止条件就是用户在Main中对原数据包裹的层数,肯定是有限的,因此停止是必然的.这样我们仿似看到一个原始数据,被一层层的进行包裹,加入新的内容,最终使用最顶层的输出方法将这个结果呈现给我们.同样的我们还可以反着看,当递归开始的时候,在最顶层等待下一层的数据,然后使用顶层的方式来封装,而下一层被启动执行到关键步骤时会等待下下一层的数据返回给自身,然后是用自己的方式来封装,就这样一直等待下去,直到最底层的数据(本来就有)得到之后,然后一步步的返回过来,在相应的层次进行相应的封装,最后得到了最终的数据.这就是装饰器模式,所有的类其实最终都是同源(一致性)的,有最终的祖先,如下图所示.

示例


上图中,StringDisplay是保存原始数据的,而Border中将父类的引用组合进入自身,形成了递归的必然条件,之后让子类(SideBorder和FullBorder)来使用这个引用,从而根据自身的实际情况(要怎样装饰)来进行包装,将原始的数据getRowText(rowID)进行包裹,最终通过同源祖先类的show()方法来现实,这里祖先类Display使用了面向抽象编程的模板方法,相信大家都能看出来.对比与上一个组合模式,我们可以看到上面的部分还是很相似的,但是在composite中,都实现了add()方法,通过容器来进行组织,并且使用委托机制来存储所有的有着共同父类的元素,在显示的时候使用了树的深度优先遍历.而在装饰器模式中,我们使用的递归从上到下,沿着display的指向一点点的走到了底部,并且最终返回了过来.遍历的方式有着相似之处也有着不同之处.这里要说明的是,建议大家在show()的地方打个断点(大家自己实验把),然后跟踪进去,一点点的看看我们的程序到底是怎么组织起来的,只有这样我们才能理解递归的含义,对装饰器有一个更深层次的认识.

Display.java

1
2
3
4
5
6
7
8
9
package com.edu.tju.GOF.Decorator;

public abstract class Border extends Display {
protected Display display; // 表示被装饰物

protected Border(Display display) { // 在生成实例时通过参数指定被装饰物
this.display = display;
}
}

StringDisplay.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.edu.tju.GOF.Decorator;

public class StringDisplay extends Display {
private String string; // 要显示的字符串
public StringDisplay(String string) { // 通过参数传入要显示的字符串
this.string = string;
}
public int getColumns() { // 字符数
return string.getBytes().length;
}
public int getRows() { // 行数是1
return 1;
}
public String getRowText(int row) { // 仅当row为0时返回值
if (row == 0) {
return string;
} else {
return null;
}
}
}

Border.java

1
2
3
4
5
6
7
8
9
package com.edu.tju.GOF.Decorator;

public abstract class Border extends Display {
protected Display display; // 表示被装饰物

protected Border(Display display) { // 在生成实例时通过参数指定被装饰物
this.display = display;
}
}

SideBorder.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.edu.tju.GOF.Decorator;

public class SideBorder extends Border {
private char borderChar; // 表示装饰边框的字符

public SideBorder(Display display, char ch) { // 通过构造函数指定Display和装饰边框字符
super(display);
this.borderChar = ch;
}

public int getColumns() { // 字符数为字符串字符数加上两侧边框字符数
return 1 + display.getColumns() + 1;
}

public int getRows() { // 行数即被装饰物的行数
return display.getRows();
}

public String getRowText(int row) { // 指定的那一行的字符串为被装饰物的字符串加上两侧的边框的字符
return borderChar + display.getRowText(row) + borderChar;
}
}

FullBorder.java

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
package com.edu.tju.GOF.Decorator;

public class FullBorder extends Border {
public FullBorder(Display display) {
super(display);
}

public int getColumns() { // 字符数为被装饰物的字符数加上两侧边框字符数
return 1 + display.getColumns() + 1;
}

public int getRows() { // 行数为被装饰物的行数加上上下边框的行数
return 1 + display.getRows() + 1;
}

public String getRowText(int row) { // 指定的那一行的字符串
if (row == 0) { // 上边框
return "+" + makeLine('-', display.getColumns()) + "+";
} else if (row == display.getRows() + 1) { // 下边框
return "+" + makeLine('-', display.getColumns()) + "+";
} else { // 其他边框
return "|" + display.getRowText(row - 1) + "|";
}
}

private String makeLine(char ch, int count) { // 生成一个重复count次字符ch的字符串
StringBuffer buf = new StringBuffer();
for (int i = 0; i < count; i++) {
buf.append(ch);
}
return buf.toString();
}
}

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.edu.tju.GOF.Decorator;

public class Main {
public static void main(String[] args) {
Display b1 = new StringDisplay("Hello, world.");
Display b2 = new SideBorder(b1, '#');
Display b3 = new FullBorder(b2);
b1.show();
b2.show();
b3.show();
Display b4 = new SideBorder(
new FullBorder(new FullBorder(new SideBorder(new FullBorder(new StringDisplay("Hello,Jason")), '*'))), '/');
b4.show();
}
}

总结

继承保证了父类和子类的一致性(有共同的方法),委托保证了使用委托的类和被委托对象的一致性.正如Border和Display有着一些相同的方法名称,以及一些委托处理方法.可以看到装饰模式中,保证了装饰边框与被装饰物体的一致性(有共同父类),使用了模板方法,这个方法几乎无处不在呀,同样使用了委托(组合),通过在原始数据上面一层层的包裹,最终得到了我们想要的输出,有着非常广泛的用处.

我的理解:装饰者模式就是, 你要出去玩. 你穿上新买的球鞋,新买的裤子,新买的外套…然后 你感觉头型也需要整整,就用个整理头型的装饰器, 脸上有痘痘,再用个遮瑕的装饰器.就这样无限的装饰(递归的过程),最后你觉得非常完美了(退出递归). ok现在你就可以出门了.
当我们买了新的口红,我们只需要新写一个口红装饰器来装饰,不需要对 已经化好的装(已有的类)进行修改.

装饰者模式 结构(出场角色)

  • 部件 (Component) 声明封装器和被封装对象的公用接口.
    Display
  • 具体部件 (Concrete Component) 类是被封装对象所属的类. 它定义了基础行为, 但装饰类可以改变这些行为.
    StringDisplay
  • 基础装饰 (Base Decorator) 类拥有一个指向被封装对象的引用成员变量. 该变量的类型应当被声明为通用部件接口, 这样它就可以引用具体的部件和装饰. 装饰基类会将所有操作委派给被封装的对象.
    Border
  • 具体装饰类 (Concrete Decorators) 定义了可动态添加到部件的额外行为. 具体装饰类会重写装饰基类的方法, 并在调用父类方法之前或之后进行额外的行为.
    FullBorderSideBorder
  • 客户端 (Client) 可以使用多层装饰来封装部件, 只要它能使用通用接口与所有对象互动即可.
    Main

    最后再举个例子

    在本例中, 装饰模式 能够对敏感数据进行压缩和加密, 从而将数据从使用数据的代码中独立出来.

程序使用一对装饰来封装数据源对象. 这两个封装器都改变了从磁盘读写数据的方式:

  • 当数据即将被写入磁盘前, 装饰对数据进行加密和压缩. 原始类在对改变毫无察觉的情况下, 将加密后的受保护数据写入文件.
  • 当数据刚从磁盘读出后, 同样通过装饰对数据进行解压和解密. 装饰和数据源类实现同一接口, 从而能在客户端代码中相互替换.

伪代码

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// 装饰可以改变组件接口所定义的操作.
interface DataSource is
method writeData(data)
method readData():data

// 具体组件提供操作的默认实现.这些类在程序中可能会有几个变体.
class FileDataSource implements DataSource is
constructor FileDataSource(filename) { ... }

method writeData(data) is
// 将数据写入文件.

method readData():data is
// 从文件读取数据.

// 装饰基类和其他组件遵循相同的接口.该类的主要任务是定义所有具体装饰的封
// 装接口.封装的默认实现代码中可能会包含一个保存被封装组件的成员变量,并
// 且负责对其进行初始化.
class DataSourceDecorator implements DataSource is
protected field wrappee: DataSource

constructor DataSourceDecorator(source: DataSource) is
wrappee = source

// 装饰基类会直接将所有工作分派给被封装组件.具体装饰中则可以新增一些
// 额外的行为.
method writeData(data) is
wrappee.writeData(data)

// 具体装饰可调用其父类的操作实现,而不是直接调用被封装对象.这种方式
// 可简化装饰类的扩展工作.
method readData():data is
return wrappee.readData()

// 具体装饰必须在被封装对象上调用方法,不过也可以自行在结果中添加一些内容.
// 装饰必须在调用封装对象之前或之后执行额外的行为.
class EncryptionDecorator extends DataSourceDecorator is
method writeData(data) is
// 1. 对传递数据进行加密.
// 2. 将加密后数据传递给被封装对象 writeData(写入数据)方法.

method readData():data is
// 1. 通过被封装对象的 readData(读取数据)方法获取数据.
// 2. 如果数据被加密就尝试解密.
// 3. 返回结果.

// 你可以将对象封装在多层装饰中.
class CompressionDecorator extends DataSourceDecorator is
method writeData(data) is
// 1. 压缩传递数据.
// 2. 将压缩后数据传递给被封装对象 writeData(写入数据)方法.

method readData():data is
// 1. 通过被封装对象的 readData(读取数据)方法获取数据.
// 2. 如果数据被压缩就尝试解压.
// 3. 返回结果.


// 选项 1:装饰组件的简单示例
class Application is
method dumbUsageExample() is
source = new FileDataSource("somefile.dat")
source.writeData(salaryRecords)
// 已将明码数据写入目标文件.

source = new CompressionDecorator(source)
source.writeData(salaryRecords)
// 已将压缩数据写入目标文件.

source = new EncryptionDecorator(source)
// 源变量中现在包含:
// Encryption > Compression > FileDataSource
source.writeData(salaryRecords)
// 已将压缩且加密的数据写入目标文件.


// 选项 2:客户端使用外部数据源.SalaryManager(工资管理器)对象并不关心
// 数据如何存储.它们会与提前配置好的数据源进行交互,数据源则是通过程序配
// 置器获取的.
class SalaryManager is
field source: DataSource

constructor SalaryManager(source: DataSource) { ... }

method load() is
return source.readData()

method save() is
source.writeData(salaryRecords)
// ...其他有用的方法...


// 程序可在运行时根据配置或环境组装不同的装饰堆桟.
class ApplicationConfigurator is
method configurationExample() is
source = new FileDataSource("salary.dat")
if (enabledEncryption)
source = new EncryptionDecorator(source)
if (enabledCompression)
source = new CompressionDecorator(source)

logger = new SalaryManager(source)
salary = logger.load()
// ...

装饰者模式 使用场景

  • 如果你希望在无需修改代码的情况下即可使用对象, 且希望在运行时为对象新增额外的行为, 可以使用装饰模式.
    装饰能将业务逻辑组织为层次结构, 你可为各层创建一个装饰, 在运行时将各种不同逻辑组合成对象. 由于这些对象都遵循通用接口, 客户端代码能以相同的方式使用这些对象.
  • 如果用继承来扩展对象行为的方案难以实现或者根本不可行, 你可以使用该模式.
    许多编程语言使用 final最终关键字来限制对某个类的进一步扩展. 复用最终类已有行为的唯一方法是使用装饰模式: 用封装器对其进行封装.

优缺点

  • 优点
    • 你无需创建新子类即可扩展对象的行为.
    • 你可以在运行时添加或删除对象的功能.
    • 你可以用多个装饰封装对象来组合几种行为.
    • 单一职责原则. 你可以将实现了许多不同行为的一个大类拆分为多个较小的类.
  • 缺点
    • 在封装器栈中删除特定封装器比较困难.
    • 实现行为不受装饰栈顺序影响的装饰比较困难.
    • 各层的初始化配置代码看上去可能会很糟糕.

与其他模式的关系

  • 适配器模式可以对已有对象的接口进行修改, 装饰者模式则能在不改变对象接口的前提下强化对象功能. 此外, 装饰还支持递归组合, 适配器则无法实现.

  • 适配器能为被封装对象提供不同的接口,代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口.

  • 装饰可让你更改对象的外表, 策略模式则让你能够改变其本质.

  • 装饰和代理有着相似的结构, 但是其意图却非常不同. 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象. 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制.

为什么要使用装饰者模式

新增加一个功能,导致新增子类很多, 会产生”类爆炸”

这里我们看到如果使用子类继承父类,那么我们要写很多子类,这么做无疑增加了程序的臃肿性,并不是很灵活.这时,装饰者模式就诞生了.

有多少种辅料就有多少个子类
以后再怎增加辅料就加一个子类就好

动态的给一个对象添加一些额外的职责.就扩展功能而言,它比生成子类的方式更为灵活.

适应场景

  • 以动态的方式给对象添加之策
  • 处理那些可以撤销的职责
  • 当采用生成子类的方法进行扩充时,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长.

装饰者模式的接口

  • 抽象组件(Component): 给出一个抽象接口,以规范准备
  • 被装饰这()
  • 装饰者组件(Decorator): 持有组件(Component)对象的实例引用,该类的职责就是为了装饰具体组件对象,定义的基类.
  • 具体装饰(ConcreteDecorator)


还可以装饰其他饮品

真实场景案例

用装饰着模式解决乱码

案例所设计的知识点

  • 装饰者模式
  • HTML
  • Request/Resonse
  • Servlet
  • Filter

IO体系中装饰者模式

抽象组件:InputStream
被装饰者: FileInputStream, Byte..
装饰器: FilterInputStream

  • django
  • flask
  • tornado
Thank you for your support