Decorator 设计模式是典型的结构型模式(在 GOF 的那本模式的 Bible 中将模式分为: 1. 创建型模式; 2. 结构型模式; 3. 行为模式三种)。它的主要用意是:动态地 为对象添加 一些额外的功能 。(记住上面两种颜色的词汇,理解装饰器模式的精髓所在!)下面是 GOF 的《 Element of reusable Object-Oriented Software 》中对Decorator 用意的概述:
Decorator Pattern ――Attaches additional responsibilities to an object dynamically . Decorators provide a flexible alternative to subclassing for extending functionality .
1 何时需要使用装饰器模式
GOF 的那本 Bible 中关于装饰器模式列举的是一个文本组件与边框的例子(在这里我就不举了,主要是因为我会在书中举一个相似的,但却非常有说服力的例子,它对 Swing 中的某些本来应该使用 Decorator 却没有使用的对象的改进。同时会提出内包装、外包装的概念。看到这个例子后大家仔细体会吧!通过例子告诉大家一点:任何设计不是一成不变的、模式的应用是极其灵活的 …… )。下面我举一个“三明治”的例子!
很 多人都吃过三明治(我除外!“没吃过猪肉,俺可听过猪叫”),都会知道三明治必不可少的是两块面包片,然后可以在夹层里加上蔬菜、沙拉、咸肉等等,外面可 以涂上奶油之类的。假如现在你要为一个三明治小店构造一个程序,其中要设计各种三明治的对象。可能你已经创建了一个简单的 Sandwich 对象,现在要产生带蔬菜的就是继承原有的 Sandwich 添加一个蔬菜的成员变量,看起来很“正点”的做法,以后我还要带咸肉的、带奶油的、带蔬菜的又分为带青菜的、带芹菜的、生菜的 …… 还是一个一个继承是吧!假如我们还需要即带蔬菜又带其它肉类,设置我们还要求这些添加成分的任意组合,那你就慢慢继承吧!
读过几年书的会下面这个算术,我们有 n 种成分,在做三明治的时候任意搭配,那么有多少种方案呢?!算算吧!你会有惊人的发现。 N 种成分,什么都不要是 Cn0 种方案吧!要 1 种是 Cn1 吧! ….. 要 n 种是 Cnn 吧!加起来不就是吗? Cn0+Cn1+……+Cnn-1+Cnn 还不会啊!牛顿莱布尼兹公式记得吧!(可惜 Word 的公式编辑器安装不了)总共 2 的 n 次方案。有可能前面 10 天写了 K 个类,老板让你再加一种成分你就得再干 10 天,下一次再加一种你可得干 20 天哦!同时你可以发现你的类库急剧地膨胀!(老板可能会说你: XXX 前 K 天你加了 n 个成分,怎么现在这么不上进呢?后 K 天只加了 1 个成分啊?!!可能你会拿个比给老板算算,老板那么忙会睬你吗?!有可能你的老板会说:不管怎么样我就要你加, K 天你还给我加 n 个成分!!呵呵,怎么办啊!跳槽啊 ! 跳槽了也没人要你!!人家一看就知道你没学设计模式)。下面我们就使用装饰器模式来设计这个库吧!下图是我们的设计图:
下面是以上各个类的意义:
1. Ingredient (成分):所有类的父类,包括它们共有的方法,一般为抽象类且方法都有默认的实现,也可以为接口。它有 Bread 和 Decorator 两个子类。这种实际不存在的,系统需要的抽象类仅仅表示一个概念,图中用红色表示。
2. Bread (面包):就是我们三明治中必须的两片面包。它是系统中最基本的元素,也是被装饰的元素,和 IO 中的媒质流(原始流)一个意义。在装饰器模式中属于一类角色,所以其颜色为紫色。
3. Decorator (装饰器):所有其它成分的父类,这些成分可以是猪肉、羊肉、青菜、芹菜。这也是一个实际不存在的类,仅仅表示一个概念,即具有装饰功能的所有对象的父类。图中用蓝色表示。
4. Pork (猪肉):具体的一个成分,不过它作为装饰成分和面包搭配。
5. Mutton (羊肉):同上。
6. Celery (芹菜):同上。
7. Greengrocery (青菜):同上。
总结一下装饰器模式中的四种角色: 1. 被装饰对象( Bread ); 2. 装饰对象(四种); 3. 装饰器( Decorator ); 4. 公共接口或抽象类( Ingredient )。其中 1 和 2 是系统或者实际存在的, 3 和 4 是实现装饰功能需要的抽象类。
写段代码体会其威力吧!(程序很简单,但是实现的方法中可以假如如何你需要的方法,意境慢慢体会吧!)
//Ingredient.java
public abstract class Ingredient {
public abstract String getDescription();
public abstract double getCost();
public void printDescription(){
System.out.println(" Name "+ this.getDescription());
System.out.println(" Price RMB "+ this.getCost());
}
}
所有成分的父类,抽象类有一个描述自己的方法和一个得到价格的方法,以及一个打印自身描述和价格的方法(该方法与上面两个方法构成模板方法哦!)
//Bread.java
public class Bread extends Ingredient {
private String description ;
public Bread(String desc){
this.description=desc ;
}
public String getDescription(){
return description ;
}
public double getCost(){
return 2.48 ;
}
}
面包类,因为它是一个具体的成分,因此实现父类的所有的抽象方法。描述可以通过构造器传入,也可以通过 set 方法传入。同样价格也是一样的,我就很简单地返回了。
//Decorator.java
public abstract class Decorator extends Ingredient {
Ingredient ingredient ;
public Decorator(Ingredient igd){
this.ingredient = igd;
}
public abstract String getDescription();
public abstract double getCost();
}
装饰器对象,所有具体装饰器对象父类。它最经典的特征就是: 1. 必须有一个它自己的父类 为自己的成员变量; 2. 必须继承公共父类。这是因为装饰器也是一种成分,只不过是那些具体具有装饰功能的成分的公共抽象罢了。在我们的例子中就是有一个 Ingredient 作为其成员变量。 Decorator 继承了 Ingredient 类。
//Pork.java
public class Pork extends Decorator{
public Pork(Ingredient igd){
super(igd);
}
public String getDescription(){
String base = ingredient.getDescription();
return base +"/n"+"Decrocated with Pork !";
}
public double getCost(){
double basePrice = ingredient.getCost();
double porkPrice = 1.8;
return basePrice + porkPrice ;
}
}
具体的猪肉成分,同时也是一个具体的装饰器,因此它继承了 Decorator 类。猪肉装饰器装饰可以所有的其他对象,因此通过构造器传入一个 Ingredient 的实例,程序中调用了父类的构造方法,主要父类实现了这样的逻辑关系。同样因为方法是具体的成分,所以 getDescription 得到了实现,不过由于它是具有装饰功能的成分,因此它的描述包含了被装饰成分的描述和自身的描述。价格也是一样的。价格放回的格式被装饰成分与猪肉成分的种价格哦!
从上面两个方法中我们可以看出,猪肉装饰器的功能得到了增强,它不仅仅有自己的描述和价格,还包含被装饰成分的描述和价格。主要是因为被装饰成分是它的成员变量,因此可以任意调用它们的方法,同时可以增加自己的额外的共同,这样就增强了原来成分的功能。
//Mutton.java
public class Mutton extends Decorator{
public Mutton(Ingredient igd){
super(igd);
}
public String getDescription(){
String base = ingredient.getDescription();
return base +"/n"+"Decrocated with Mutton !";
}
public double getCost(){
double basePrice = ingredient.getCost();
double muttonPrice = 2.3;
return basePrice + muttonPrice ;
}
}
羊肉的包装器。
//Celery.java
public class Celery extends Decorator{
public Celery(Ingredient igd){
super(igd);
}
public String getDescription(){
String base = ingredient.getDescription();
return base +"/n"+"Decrocated with Celery !";
}
public double getCost(){
double basePrice = ingredient.getCost();
double celeryPrice =0.6;
return basePrice + celeryPrice ;
}
}
芹菜的包装器。
//GreenGrocery.java
public class GreenGrocery extends Decorator{
public GreenGrocery (Ingredient igd){
super(igd);
}
public String getDescription(){
String base = ingredient.getDescription();
return base +"/n"+"Decrocated with GreenGrocery !";
}
public double getCost(){
double basePrice = ingredient.getCost();
double greenGroceryPrice = 0.4;
return basePrice + greenGroceryPrice ;
}
}
青菜的包装器。
下面我们就领略装饰器模式的神奇了!我们有一个测试类,其中建立夹羊肉的三明治、全蔬菜的三明治、全荤的三明治。(感觉感觉吧!很香的哦!)
public class DecoratorTest{
public static void main(String[] args){
Ingredient compound = new Mutton(new Celery(new Bread("Master24's Bread")));
compound.printDescription();
compound = new Celery(new GreenGrocery(new Bread("Bread with milk")));
compound.printDescription();
compound = new Mutton(new Pork(new Bread("Bread with cheese")));
compound.printDescription();
}
- }
以上就是一个简单的装饰器类!假如你对想中国式的吃法,可以将加入馒头、春卷皮、蛋皮 …… 夹菜可以为肉丝 …… 突然想到了京酱肉丝。
2 装饰器模式的结构
在谈及软件中的结构,一般会用 UML 图表示( UML 和 ANT 、 JUnit 等都是软件设计中基本的工具,会了没有啊!)。下面是一个我们经常看到的关于 Decorator 模式的结构图。
1. Component 就是装饰器模式中公共方法的类,在装饰器模式结构图的顶层。
2. ConcreateComponent 是转换器模式中具体的被装饰的类,IO 包中的媒体流就是此种对象。
3. Decorator 装饰器模式中的核心对象,所有具体装饰器对象的父类,完成装饰器的部分职能。在上面的例子中Decorator 类和这里的对应。该类可以只做一些简单的包裹被装饰的对象,也可以还包含对Component 中方法的实现…… 他有一个鲜明的特点:继承至Component ,同时包含一个Component 作为其成员变量。装饰器模式动机中的 动态地 增加功能是在这里实现的。
4. ConcreteDecoratorA 和ConcreteDecoratorB 是两个具体的装饰器对象,他们完成具体的装饰功能。装饰功能的实现是通过调用被装饰对象对应的方法,加上装饰对象自身的方法。这是装饰器模式动机中的 添加额外功能 的关键。
从上面图中你可能还会发现: ConcreteDecoratorA 和 ConcreteDecoratorB 的方法不一样,这就是一般设计模式中谈及装饰器模式的“透明装饰器”和“不透明装饰器”。“透明装饰器”就是整个 Decorator 的结构中所有的类都保持同样的“接口”(这里是共同方法的意思),这是一种极其理想的状况,就像餐饮的例子一样。现实中绝大多数装饰器都是“不透明装饰器”,他们的“接口”在某些子类中得到增强,主要看这个类与顶层的抽象类或者接口是否有同样的公共方法。 IO 中的 ByteArrayInputStream 就比 Inputstrem 抽象类多一些方法,因此 IO 中的装饰器是一个“不通明装饰器”。下面是 IO 中输入字节流部分的装饰器的结构图。
1. InputStream 是装饰器的顶层类,一个抽象类!包括一些共有的方法,如: 1. 读方法―― read ( 3 个); 2. 关闭流的方法―― close ; 3.mark 相关的方法―― mark 、 reset 和 markSupport ; 4. 跳跃方法―― skip ; 5. 查询是否还有元素方法―― available 。图中红色的表示。
2. FileInputStream 、 PipedInputStream… 五个紫色的,是具体的被装饰对象。从他们的“接口”中可以看出他们一般都有额外的方法。
3. FilterInputStream 是装饰器中的核心, Decorator 对象,图中蓝色的部分。
4. DataInputStream 、 BufferedInputStream… 四个是具体的装饰器,他们保持了和 InputStream 同样的接口。
5. ObjectInputStream 是 IO 字节输入流中特殊的装饰器,他不是 FilterInputStream 的子类(不知道 Sun 处于何种意图不作为 FileterInputStream 的子类,其中流中也有不少的例子)。他和其他 FilterInputStream 的子类功能相似都可以装饰其他对象。
IO 包中不仅输入字节流是采用装饰器模式、输出字节流、输入字符流和输出字符流都是采用装饰器模式。关于 IO 中装饰器模式的实现可以通过下面的源代码分析从而了解细节。