什么是装饰者模式?
装饰者模式,是面向对象编程领域中,一种动态地往一个类中添加新的行为的设计模式。就功能而言,装饰者模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。
认识装饰者模式
从一个例子准备了解装饰者模式
虽然笔者并不喜欢喝奶茶,但奶茶店在街道上随处可见。假如你要开一家奶茶店,会怎么设计奶茶相关的类呢?
最初的设计
下面为奶茶中涉及到的类的设计:
购买奶茶时,光有奶茶还不够,还可以要求在里面加入各种好吃的配料,如黑珍珠(
BlackPearl
),冰块(IceCake
),冰淇凌(IceCream
),芒果块(MongoBlock
)等等。奶茶店会根据所加入的配料收取不同的费用,所以订单系统必须考虑到这些配料部分。
因此原先设计的要适当变化:
很明显,这样设计会为我们后期维护带来相当大的困难,如果黑珍珠的价格上涨,怎么办?新增一种新的布丁配料时又怎么办?新手才会这么设计。
我们不应该设计这么多的类,如果使用实例变量和继承,就可以追踪这些配料。
改进后的奶茶类
因此,我们重新改造一下,将MilkyTea
基类加上实例变量,代表是否加上配料(黑珍珠、冰块、冰淇凌、芒果块等等):
MilkyTea
基类实现的代码:
public class MilkyTea {
protected String description;
private double blackPearlCost = 1.0;
private double iceCakeCost = 1.0;
private double iceCreamCost = 2.0;
private double MongoBlackCost = 3.0;
private boolean blackPearl;
private boolean iceCake;
private boolean iceCream;
private boolean MongoBlack;
public double cost() {
double condimentCost = 0.0;
if (isBlackPearl()) {
condimentCost += blackPearlCost;
}
if (isIceCake()) {
condimentCost += iceCakeCost;
}
if (isIceCream()) {
condimentCost += iceCreamCost;
}
if (isMongoBlack()) {
condimentCost += MongoBlackCost;
}
return condimentCost;
}
public String getDescription() {
return description;
}
public boolean isBlackPearl() {
return blackPearl;
}
public void setBlackPearl(boolean blackPearl) {
this.blackPearl = blackPearl;
}
public boolean isIceCake() {
return iceCake;
}
public void setIceCake(boolean iceCake) {
this.iceCake = iceCake;
}
public boolean isIceCream() {
return iceCream;
}
public void setIceCream(boolean iceCream) {
this.iceCream = iceCream;
}
public boolean isMongoBlack() {
return MongoBlack;
}
public void setMongoBlack(boolean mongoBlack) {
MongoBlack = mongoBlack;
}
}
具体实现的2种奶茶类:
//纯苹果奶茶什么也没加
public class AppleMilkyTea extends MilkyTea {
public AppleMilkyTea() {
description = "好喝的苹果奶茶";
}
//就花了2块钱
@Override
public double cost() {
return 2.0 + super.cost();
}
}
//苹果奶茶加了黑珍珠和冰块
public class AppleMilkyTeaWithBlackPearlAndIceCake extends MilkyTea {
public AppleMilkyTeaWithBlackPearlAndIceCake() {
setBlackPearl(true);
setIceCake(true);
description = "好喝的苹果奶茶还加了黑珍珠和冰块";
}
//花了2块钱加上配料的钱
@Override
public double cost() {
return 2.0 + super.cost();
}
}
虽然改进后的类比原来更具有弹性了,但是当有些需求或因素改变时还是会影响这个设计。
比如配料价钱的改变会使我们更改现有代码。一旦出现新的配料,我们就需要加上新的方法,并改变超类中的cost()
方法。而且以后可能会有新类型的奶茶,对某些奶茶而言,和有些配料是不能混在一起吃的(比如菠萝奶茶和芒果配料是不能一起吃的)。
但是在这个设计方式中,奶茶的子类仍将继承那些不合适的方法(虽然这些方法可能不会使用)。因此,我们需要使用装饰者模式。
着手解决问题
从前面我们知道利用继承无法完全解决问题:类数量爆炸、设计死板,以及超类加入的新功能并不适用于所有的子类。
所以,在这里我们要采用不一样的做法:以奶茶为主体,然后在运行时以配料来“装饰”奶茶。比如,顾客想要蓝莓奶茶加黑珍珠和芒果块,那么需要:
①拿一个蓝莓奶茶对象
②以黑珍珠对象装饰它
③以芒果块对象装饰它
④调用cost()
方法,并依赖委托将配料的价钱加上去。
那么如何“装饰”一个对象,“委托”又如何与此搭配使用?我们可以把装饰者对象当成“包装者”。
以装饰者构造奶茶订单
①以BlueberryMilkyTea
对象为开始(蓝莓奶茶继承自奶茶,且有一个计算价钱的cost()
方法):
②顾客还想要黑珍珠(BlackPearl
),所以建立一个BlackPearl
对象,并用它将BlueberryMilkyTea
对象包装起来(BlackPearl
对象是一个装饰者,它的类型“反映”了它所装饰的MilkyTea
对象。“反映”即指两者类型一致。通过多态可以把BlackPearl
所包裹的任何类型的MilkyTea
当成是MilkyTea
)。
③顾客又想要芒果块(MongoBlack
),所以需要建立一个MongoBlack
对象,并用它把BlackPearl
对象包装起来
④现在为顾客计算花费的价钱。通过调用最外圈装饰者(MongoBlack
)的cost()
就可以办得到。MongoBlack
的cost()
会先委托它装饰的对象也就是BlackPearl
计算出价钱,再加上芒果块的价钱,具体如下图:
从上面我们可以知道:
- 装饰者和被装饰对象有相同的父类
- 可以用一个或多个装饰者包装一个对象
- 既然装饰者和被装饰对象有相同的父类,那么可以在任何需要原始对象的场合,用装饰过的对象代替它。
- 装饰者可以在所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定目的
- 对象可以在任何时候被装饰,所以可以在运行时动态的、不限量地用你喜欢的装饰者来装饰对象
定义装饰者模式
装饰者模式动态地将责任附加到对象上。 若要扩展功能,装饰者提供了比继承更有弹性 的替代方案。
下面为装饰者的类图,我们后面会套用此结构:
装饰者模式改进的例子
现在让我们的奶茶符合上面定义的结构,类图如下:
从类图我们看到,CondimentDecorator
扩展自MilkyTea
类,用到了继承。这么做的重点在于,装饰者和被装饰者必须是一样的类型,也就是有共同的父类,这是相当关键的地方。这里的继承是达到“类型匹配”,而不是利用继承获得“行为”。
那么行为又是从哪里来的呢?
当我们将装饰者和组件组合时,就是在加入新的行为。所得到的新行为,并不是继承自父类,而是由组合对象得来的。也就是说,继承MilkyTea
类,是为了有正确的类型,而不是继承它的行为。行为来自装饰者和基础组件,或与其他装饰者之间的组合关系。
因为使用对象组合,就可以把所有奶茶和配料更有弹性地加以混合和匹配,非常方便。如果依赖继承,那么类的行为只能在编译时静态决定,行为不是来自父类,就是子类覆盖后的版本。反之,利用组合,可以把装饰者混合使用,而且是在“运行时”!!!
为什么不把MilkyTea
设计成一个接口,而是抽象类呢?
通常装饰者模式是采用抽象类,但是在Java中可以使用接口。尽管如此,我们都努力避免修改现有的代码,所以,如果抽象类运作的好好地,还是别去修改它。
代码具体实现
现在开始正在设计代码了,首先从MilkyTea
切入:
public abstract class MilkyTea {
String description = "白开水状态的奶茶。。。";
public String getDescription(){
return description;
}
public abstract double cost();
}
然后实现CondimentDecorator
抽象类,也就是装饰者类,黑珍珠冰块等配料都继承该类:
public abstract class CondimentDecorator extends MilkyTea {
//所以的配料都必须重新实现该方法
public abstract String getDescription();
}
接着开始写一些奶茶类吧,需要将具体的奶茶描述一下哦(类别描述和价钱花费),好让顾客选择,代码如下:
//苹果奶茶
public class AppleMilkyTea extends MilkyTea {
//描述奶茶具体的种类
public AppleMilkyTea() {
description = "这是苹果奶茶";
}
//奶茶的价钱花费
@Override
public double cost() {
return 1.0;
}
}
//蓝莓奶茶
public class BlueberryMilkyTea extends MilkyTea {
public BlueberryMilkyTea() {
description = "这是蓝莓奶茶";
}
@Override
public double cost() {
return 3.0;
}
}
完成了抽象组件(MIlkyTea
),有了具体组件(AppleMilkyTea
、BlueberryMilkyTea
),也有了装饰者(CondimentDecorator
),紧接着就是实现具体装饰者配料类了,顾客说:往我的奶茶里加点黑珍珠和冰块。好嘞:
//黑珍珠
public class BlackPearl extends CondimentDecorator {
//用一个实例变量记录奶茶,也就是被装饰者
MilkyTea milkyTea;
//想办法让被装饰者被记录到实例变量中,把奶茶当作构造器参数传入即可,再记录给实例变量
public BlackPearl(MilkyTea milkyTea) {
this.milkyTea = milkyTea;
}
/**
* 不仅仅描述了奶茶,我们还得完整地描述配料
* 因此首先利用委托的方法,在CondimentDecorator得到一个描述
* 再在其子类附加具体的描述
* @return
*/
@Override
public String getDescription() {
return milkyTea.getDescription() + ",并且加了黑珍珠";
}
//自然也要计算配料的价钱
@Override
public double cost() {
return 1.0 + milkyTea.cost();
}
}
//冰块
public class IceCake extends CondimentDecorator {
MilkyTea milkyTea;
public IceCake(MilkyTea milkyTea) {
this.milkyTea = milkyTea;
}
@Override
public String getDescription() {
return milkyTea.getDescription() + ",并且加了冰块";
}
@Override
public double cost() {
return 1.0 + milkyTea.cost();
}
}
最后就是测试类了:
//一个什么都没加的苹果奶茶
MilkyTea milkyTea = new AppleMilkyTea();
System.out.println(milkyTea.getDescription() + ",价钱人民币" + milkyTea.cost() + "块");
//创建一杯蓝莓奶茶
MilkyTea milkyTea1 = new BlueberryMilkyTea();
//用黑珍珠装饰它
milkyTea1 = new BlackPearl(milkyTea1);
//再用冰块装饰它
milkyTea1 = new IceCake(milkyTea1);
System.out.println(milkyTea1.getDescription() + ",价钱人民币" + milkyTea1.cost() + "块");
测试类运行结果如下:
这是苹果奶茶,价钱人民币1.0块
这是蓝莓奶茶,并且加了黑珍珠,并且加了冰块,价钱人民币5.0块
装饰者模式的缺点
硬币有正反两面,装饰者模式也有一个“缺点”:利用装饰者模式,常常造成设计中有大量的小类,数量实在太多,可能会造成使用相关API(如java.io
)程序员的困扰,不过知道了装饰者的工作原理,以后就能很容易地辨别出它们的装饰者类是如何组织的,以方便用包装方式取得想要的行为。
Java中的装饰者:I/O类
java.io
类非常多,其中许多类都是装饰者。下面是一个典型的对象集合,用装饰者来将功能结合起来,以读取文件数据:
BufferedInputStream
及LineNumberInputStream
都扩展自FilterInputStream
,而FilterInputStream
是一个抽象的装饰类。
让我们查看一下各种I/O类之间的关系:
可以发现,其和奶茶店的设计相比其实并没有多大的差异。将
java.io
API范围缩小,可以容易的查看它的文件,并组合各种“输入”流装饰者来符合我们的用途。
类似地,“输出”流的设计方式也是一样的,而且字符流的设计和字节流的设计也相当类似(有一点小差异),知道装饰者模式之后,可以更好地理解这些类。
开闭(Open Closed
)原则
开放-关闭原则:类应该对扩展开放,对修改关闭。
我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配 新的行为。如能实现这样的目标,有什么好处呢?这样的设计具有弹性,可以应对改变,可以接受新的功能来应对改变的需求。
对扩展开放,对修改关闭?乍听之 下,的确感到矛盾,毕竟,越难修改的事 物,就越难以扩展。但是,有一些聪明的OO技巧,允许系统在不修改代码的情况下,进行功能扩展。 如观察者模式,通过加 入新的观察者,我们可以在任何时候扩展 Subject(主题),而且不需向主题中添加代码。
装饰者模式完全遵循开放-关闭原则。
我们不需要让设计的每个部分都遵循开放-关闭原则,通常办不到,要让OO设计同时具备开放性和关闭性,又不修改现有的代码,需要花费许多时间和努力。一 般来说,我们实在没有闲工夫把设计的每 个部分都这么设计(而且,就算做得到, 也可能只是一种浪费)。遵循开放-关闭原 则,通常会引入新的抽象层次,增加代码的复杂度。只需要把注意力集中在设计中最有可能改变的地方,然后应用开放-关闭原则即可。
参考资料
《HeadFirst设计模式》