Java设计模式之装饰者模式
1. 介绍
通常我们有两种方式为一个类或者对象添加行为:
一是使用继承。继承是给一个类添加行为的比较有效的途径。通过使用继承,可以使得子类在拥有自身行为的同时,还可以拥有父类的行为。但是使用继承是静态的,在编译的时候就已经决定了子类的行为,我们不便于控制增加行为的方式和时机。
二是使用关联,即将一个对象嵌入到另一个对象中,由另一个对象来决定是否引用嵌入对象的行为来扩展自己的行为。这是一种动态的方式,我们可以在应用程序中动态的控制。这就是我们要说的装饰者模式。
装饰者(Decorator)模式又名包装(Wrapper)模式,以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案,它通过创建一个包装对象,也就是装饰,来包裹真实的对象。
装饰者模式能动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同,它可以在不创造更多子类的情况下,将对象的功能加以扩展。
装饰者模式把客户端的调用委派到被装饰类。装饰者模式的关键在于这种扩展是完全透明的。
上图是装饰者模式的类图关系,在装饰者模式中涉及到的角色有:
- 抽象构件角色(Component):抽象接口,规范准备接收附加责任的对象。
- 具体构件角色(ConcreteComponent):定义将要接收附加责任的类。
- 抽象装饰角色(Decorator):持有一个构件(Component)对象的引用,并定义一个与抽象构件接口一致的接口。
- 具体装饰角色(ConcreteDecorator):负责给构件对象“贴上”附加的责任。
2. 示例
在现实生活场景中,有不少可以抽象为装饰者模式的例子。
比如,我们房间里挂的图画,它的主要功能是为了美化我们房间,让进入房间的我们更加赏心悦目,心情愉快。通常为了更好地保存这样一幅图画,我们会为其制作一边框,防止它轻易地被损坏;另外,如果图画由于某种客观原因不适合挂在墙壁上,我们也会考虑为其制作一支架,将其固定在房间的某一位置上,防止图画被摔坏等意外事件。
从上面的例子中,我们可以看到,不管是为图画制作边框还是制作支架,都只是为图画自身添加额外的功能罢了,其本身的核心功能我们并没有改变,之所以添加这样那样的职能,主要是为了更方便我们对图画的使用和欣赏而已。
这里需要提醒的是,为图画添加边框或者支架两者是没有关联或者依赖的,彼此是独立的,也就是说完全可以分开添加亦可以一起添加,这只取决于我们自己的意愿而已。实际上,添加边框和支架相对于图画来说就是两个装饰器,用于装饰图画。
我们用示例代码来实现:
package com.demo;
/**
* 装饰者模式测试类
*
* @author 小明
*
*/
public class DecoratorDemo {
public static void main(String[] args) {
Picture pic = new EightHorses(); // 创建被装饰者对象
pic = new Frame(pic); // 使用框架装饰
pic = new Holder(pic); // 使用支架装饰
pic.show(); // 展示图画,因为加了装饰,所以增加了装饰内容的打印
}
}
/**
* 图画接口(相当于Component,抽象构件角色)
*
* @author 小明
*
*/
interface Picture {
/**
* 展示图画
*/
void show();
}
/**
* 八骏图,实现Picture接口,相当于ConcreteComponent,具体构件角色
*
* @author 小明
*
*/
class EightHorses implements Picture {
@Override
public void show() {
System.out.println("展示八骏图内容 -- 具体构件");
}
}
/**
* 装饰角色,定义一个与抽象构件接口一致的抽象类
*
* @author 小明
*
*/
abstract class Decorator implements Picture {
private Picture picture; // 持有一个构件(Component)对象的引用
public Decorator(Picture picture) {
super();
this.picture = picture;
}
@Override
public void show() {
picture.show();
}
}
/**
* 框架类,相当于具体装饰角色(ConcreteDecorator),负责给构件对象添加附加的责任
*
* @author 小明
*
*/
class Frame extends Decorator {
public Frame(Picture picture) {
super(picture);
}
/**
* 附加责任
*/
private void addFrame() {
System.out.println("为图画加上边框 -- 具体装饰一");
}
@Override
public void show() {
addFrame();
super.show();
}
}
/**
* 支架类,也相当于具体装饰角色
*
* @author 小明
*
*/
class Holder extends Decorator {
public Holder(Picture picture) {
super(picture);
}
/**
* 附加责任
*/
private void makeHolder() {
System.out.println("为图画制作支架 -- 具体装饰二");
}
@Override
public void show() {
makeHolder();
super.show();
}
}
运行结果:
为图画制作支架 -- 具体装饰二
为图画加上边框 -- 具体装饰一
展示八骏图内容 -- 具体构件
与继承相比,以上示例实现的方式优势就在于不会破坏类的封装性,且具有较好的松耦合性,可以使系统更加容易维护。但是它的缺点就是要创建比继承时更多的对象。
下面我们再来看一个示例,理解一下装饰者模式的使用。
我们来设计一个数据加密系统,这个系统可以对字符串进行加密操作。最简单的加密算法是将字符串字母进行移位来实现加密,同时系统还提供MD5加密和SHA1加密。我们可以首先使用最简单的加密算法为字符串加密,如果觉得强度还不够,则可以继续使用MD5进行二次加密,如果强度还不够,可以进行第三次加密……现在我们以装饰者模式来设计该系统:
package com.demo;
/**
* 使用装饰者模式设计加密系统
*
* @author 小明
*
*/
public class DecoratorDemo2 {
public static void main(String[] args) {
String text = "我是中国人";
CipherCode cipher = new SHA1(new MD5(new Simple())); // 装饰
String encrypt = cipher.encrypt(text);
System.out.println("加密处理:\n" + encrypt);
}
}
/**
* 密码加密接口(Component)
*
* @author 小明
*
*/
interface CipherCode {
String encrypt(String text);
}
/**
* 简单加密类(ConcreteComponent)
*
* @author 小明
*
*/
class Simple implements CipherCode {
@Override
public String encrypt(String text) {
return "使用字符串移位对 " + text + " 加密";
}
}
/**
* 装饰者角色
*
* @author 小明
*
*/
abstract class CipherDecorator implements CipherCode {
private CipherCode cipherCode; // 持有一个构件(Component)对象的引用
public CipherDecorator(CipherCode cipherCode) {
super();
this.cipherCode = cipherCode;
}
@Override
public String encrypt(String text) {
return cipherCode.encrypt(text);
}
}
/**
* MD5加密(ConcreteDecorator)
*
* @author 小明
*
*/
class MD5 extends CipherDecorator {
public MD5(CipherCode cipherCode) {
super(cipherCode);
}
@Override
public String encrypt(String text) {
String en = super.encrypt(text);
en += "\n继续使用MD5加密";
return en;
}
}
/**
* SHA1加密(ConcreteDecorator)
*
* @author 小明
*
*/
class SHA1 extends CipherDecorator {
public SHA1(CipherCode cipherCode) {
super(cipherCode);
}
@Override
public String encrypt(String text) {
String en = super.encrypt(text);
en += "\n继续使用SHA1加密";
return en;
}
}
运行结果:
加密处理:
使用字符串移位对 我是中国人 加密
继续使用MD5加密
继续使用SHA1加密
装饰模式在JDK中最经典的是Java IO,以InputStream为例:
在javax.swing包中,也可以通过装饰者模式改善组件的外观,如JTextArea本身没有滚动条,我们要创建带滚动条的文本框,可以使用如下方式:
JTextArea txt = new JTextArea();
JScrollPane jsp = new JScrollPane(txt);
3. 小结
装饰者模式主要优点在于可以提供比继承更多的灵活性,可以通过一种动态的方式来扩展一个对象的功能,并通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,而且具体构件类与具体装饰类可以自由独立变化,用户可以根据需要增加新的具体构件类和具体装饰类。
装饰者模式主要的缺点在于使用它进行系统设计时将产生很多小对象,而且装饰者模式比继承更易出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
装饰者模式主要适用的场合是:在不影响其它对象的情况下,以动态、透明的方式给单个对象添加职责;需要动态地给一个对象增加功能,这些功能也可以动态被撤销;当不能采用继承的方式对系统进行扩展或采用继承不利于系统扩展和维护时。