第十章 装饰模式 - 扩展系统功能
如果你买了一个毛坯房,那么剩下的就是装修。装修并没有改变原有房屋用于居住的本质,但是增加了实用性、美观性等特征。
在软件设计中,装饰模式就是一种类似装修的技术,能对已有对象的功能进行扩展,以获得更符合用户需求的对象。
一、设计一款图形界面库
设计一款图形界面构件库,该库提供了大量基础构件,如窗体、文本框、列表框等。由于在使用时,需要定制一些特效,如带滚动条的窗体、带黑色边框的文本框、即带滚动条又带黑色边框的列表框等。即对原有的基础构件进行扩展,以增强其功能。
1.1 传统继承方式实现
1.2 问题与缺陷
按照 1.1 中的实现方式,虽然可以满足系统的设计需求。但是,存在的问题也十分严重:
- 系统扩展十分麻烦。当我们需要 “带滚动条和黑色边框的窗体类” 时,需要同时继承两种类型的窗体类。这在
不支持多重继承
的语言中是无法使用的 - 代码重复。从设计图中可以看出,不只是 窗体类 需要滚动和边框,文本框类和列表框类同样需要。而设计滚动条与黑色边框的过程基本相同,代码重复。不利于对系统进行修改和维护。
- 系统庞大,类的数目非常多。如果增加新的控件或者新的扩展功能,系统都需要增加大量的具体类,这将导致系统变得非常庞大。
- 如增加一个透明边框(基本控件)的功能,则需要对窗体、文本框、列表框各加一个实现类。
- 如果需要组合各个功能的话,3种扩展方式则存在7种组合关系。
1.3 解决方案
直接继承的设计方法的问题在于 类的扩展十分不便,而且会导致类数目的急剧增加。
其根本原因在于复用机制的不合理:上文采用了继承复用,如“带滚动的窗体”通过继承的方式来复用“窗体类”的“显示功能”,又增加了特定的方法。在复用父类的方法后再增加新的方法来扩展功能。
根据“合成复用原则”,在实现功能复用时,要多用关联,少用继承。如将 “滚动功能” 抽离,封装在单独的类中,在这个类中定义一个 Component 类型的对象,通过调用 Comonent 的 “显示方法”,在通过调用“滚动功能”的方法来实现功能的扩展。
根据“里氏替换原则”,只需要在程序运行时,向独立的类中注入具体的 Component 子类对象即可实现功能扩展。
这个独立的类一般称为“装饰器 Decorator” 或装饰类。
二、装饰模式
装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为。
装饰模式是一种用于替代继承的技术,它通过一种无须定义子类的方式来给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系。通过装饰类调用待装饰的原有类的方法,还可以增加新的方法,以扩充原有类的功能。
2.1 定义
装饰模式:动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更加灵活。装饰模式是一种对象结构型模式。
2.2 装饰模式中的几个角色
- Component - 抽象构件
- 具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法
- 引入抽象构件,可以使客户端以一致的方式处理未被装饰的对象,以及装饰之后的对象
- 实现客户端的透明操作
- ConcreteComponent - 具体构件
- 抽象构件的子类,用于定义具体的构件对象
- 装饰器可以给它增加额外的职责(方法)
- Decorator - 抽象装饰类
- 抽象构件的子类,用于给具体构件增加职责,但是具体职责在其子类中实现
- 维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法
- 通过子类扩展该方法,以达到装饰的目的
- ConcreteDecorator - 具体装饰类
- 抽象装饰类的子类,负责向构件添加新的职责
- 每个具体装饰类都定义了一些新的行为,可以调用抽象装饰类中定义的方法,并增加新的方法用以扩充对象的行为
2.3 装饰模式的核心 - 抽象装饰类 - 代码
class Decorator implements Component {
private Component component; // 维护一个对抽象构件对象的引用
public Decorator(Component component) { // 注入一个抽象构件类型的对象
this.component = component;
}
public void operation(){ // 调用原有业务方法
component.operation();
}
}
抽象装饰类可以做到对装饰类进行再装饰,如对一张图表增加一个相框,还能继续在小相框外套大相框。因为它们都是 Component 的子类。
抽象装饰类只是调用原有的 component 对象的 operation() 方法,它并没有真正的实施装饰,而是提供一个统一的接口,将具体装饰过程交给子类完成。
class ConcreteDecorator extends Decorator {
public ConcreteDecorator(Component component){
super(component);
}
public void operation(){
super.operation(); // 调用原有业务方法
addedBehavior(); // 调用新增业务方法
}
// 新增的业务方法
public void addedBehavior(){
// ...
}
}
装饰模式中是否存在独立变化的两个维度? 试比较装饰模式和桥接模式的相同之处和不同之处?
三、完整实现
/**
* 装饰模式
*/
// 抽象界面构件类 - 抽象构件类
abstract class Component {
public abstract void display();
}
// 窗体类 - 具体构件类
class Window extends Component {
@Override
public void display() {
System.out.println("显示窗体!");
}
}
// 文本框类 - 具体构件类
class TextBox extends Component {
@Override
public void display() {
System.out.println("显示文本框!");
}
}
// 列表框类 - 具体构件类
class ListBox extends Component {
@Override
public void display() {
System.out.println("显示列表框!");
}
}
// 构件装饰类 - 抽象装饰类
class ComponentDecorator extends Component {
// 维持对抽象构件类型对象的引用
private Component component;
// 注入抽象构件类型的对象
public ComponentDecorator(Component component) {
this.component = component;
}
@Override
public void display() {
component.display();
}
}
// 滚动条装饰类 - 具体装饰类
class ScrollBarDecorator extends ComponentDecorator {
public ScrollBarDecorator(Component component) {
super(component);
}
@Override
public void display() {
this.setScrollBar();
super.display();
}
public void setScrollBar() {
System.out.println("为构件增加滚动条!");
}
}
// 黑色边框装饰类 - 具体装饰类
class BlackBorderDecorator extends ComponentDecorator {
public BlackBorderDecorator(Component component) {
super(component);
}
@Override
public void display() {
setBlackBorder();
super.display();
}
public void setBlackBorder() {
System.out.println("为构件增加黑色边框");
}
}
public class DecoratorPattern {
public static void main(String[] args) {
Component window = new Window();
Component scrollBarWindow = new ScrollBarDecorator(window);
scrollBarWindow.display();
}
}
四、透明装饰模式和半透明装饰模式
如果我们需要在 “黑色边框装饰类” 中增加一个方法用于设置边框宽度。则修改后的类如下:
// 黑色边框装饰类 - 具体装饰类
class BlackBorderDecorator extends ComponentDecorator {
public BlackBorderDecorator(Component component) {
super(component);
}
@Override
public void display() {
setBlackBorder();
super.display();
}
public void setBlackBorder() {
System.out.println("为构件增加黑色边框");
}
public void setBorderWidth(Integer width){
// ...
}
}
如果我们继续使用 抽象构件类
,则客户端无法调用新增的业务方法 setBorderWidth(Integer width)
。因为在抽象构建类中,没有对该方法的声明。
在实际使用中,必须使用具体装饰类对象来调用该方法,这种装饰模式就是 半透明(Semi-transparent)装饰模式
,而标准的装饰模式是透明装饰模式。
透明装饰模式与半透明装饰模式的区别
透明装饰模式要求客户端完全针对抽象编程,所有的装饰类必须声明为抽象构件类型。
- 优点:客户端无需关心具体构件类型,可以让客户端透明地使用装饰之前的对象和装饰之后的对象,无须关系它们的区别
- 优点:能够对装饰过的对象进行多次装饰,得到更复杂、功能更强大的对象。
而半透明装饰模式的设计难度较大,对于新增特有的方法,必须使用具体装饰类型来定义装饰后的对象。
- 优点:半透明装饰模式可以给系统带来更多灵活性,设计相对简单,使用也比较方便
- 缺点:不能对使用同一对象的多次装饰,且客户端需要区别对待装饰之前和装饰之后的对象
五、总结
优点
- 对于扩展对象的功能,装饰模式比继承更加灵活,且类的个数不会急剧增加
- 能够动态扩展对象功能,比如通过配置文件决定运行时的具体装饰类
- 能够对一个对象进行多次装饰,通过使用不同的具体装饰类来获得不同行为的组合
- 具体构件类和具体装饰类可以独立变化,符合“开闭原则”
缺点
- 装饰模式在使用时,会产生许多的对象。
- 例如通过对一个对象的多次包装,会产生多个具体包装类
- 大量对象势必会占用更多的系统资源,一定程度上影响程序的性能
- 装饰模式虽然比继承更加灵活,但也意味着更加容易出错,排查问题更加困难。
- 对于多次装饰后的对象,调试问题可能需要逐级排查,较为繁琐
适用场景
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
- 不能采用继承方式对系统扩展时。
- 系统中存在大量独立的扩展,增加扩展时会导致子类爆炸性增长
- 类被定义为不能继承(如final修饰的类)
六、练习
Sunny软件公司欲开发了一个数据加密模块,可以对字符串进行加密。最简单的加密算法通过对字母进行移位来实现,同时还提供了稍复杂的逆向输出加密,还提供了更为高级的求模加密。用户先使用最简单的加密算法对字符串进行加密,如果觉得还不够可以对加密之后的结果使用其他加密算法进行二次加密,当然也可以进行第三次加密。试使用装饰模式设计该多重加密系统。