我曾经以为应该用继承处理一切。后来领教到运行时扩展,远比编译时期的继承威力大。
本章可以称为“给爱用继承的人一个全新的设计眼界”。我们即将再度探讨典型的继承滥用问题。在本章将会学到如何使用对象组合的方式,做到在运行时装饰类。一旦你熟悉了装饰的技巧,你讲能在不修改任何底层代码的情况下,给你的(或别人的)对象赋予新的职责。
新的例题:星巴兹是以狂战速度最快而闻名的咖啡连锁店。因为扩张速度实在太快,他们最北更新订单系统,以合乎他们的饮料供应要求。
他们原来的类设计是这样:
购买咖啡时,也可以要求子啊其中加入各种调料,例如:蒸奶,豆浆,摩卡或覆盖奶泡。星巴兹会根据所加入的调料收取不同的费用。所以订单系统必须考虑到这些调料部分。
这是他们的第一个尝试:
很明显,星巴兹为自己制造了一个维护噩梦,如果牛奶的价钱上扬,怎么办?新增一种焦糖调料风味时,怎么办?
我们已经了解到利用组合和委托可以在运行时具有继承行为的效果。
利用集成设计子类的行为,实在编译时静态决定的,而且所有的子类都会继承到相同的行为,然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态的进行扩展。
我们可以利用此技巧把多个新职责,甚至是设计超类时还没有想到的职责加在对象上,而且,可以不用修改原来的代码。
可以看出:
开放-关闭原则:类应该对扩展开放,对修改关闭。
认识装饰者模式:
我们已经了解利用继承无法完全解决问题,在星巴兹遇到的问题有:类数量爆炸、设计死板、以及基类加入的新功能并不适用于所有的子类。
所以在这里我们要以饮料为主体,然后在运行时以调料来“装饰”饮料。那么。要做的是:
1:拿一个深焙咖啡(DarkRoast)对象。
2:以摩卡(Mocha)对象装饰它。
3:以奶泡(Whip)对象装饰它。
4:调用cost()方法,并依赖委托(delegate)将调料的价钱加上去。
但是如何“装饰”一个对象,而“委托”又要如何与此搭配使用?(提示:把装饰者对象当成“包装者”)
以装饰者构造饮料订单
1:以DarkRoast对象开始。DarkRoast继承自Beverage,且有一个用来计算饮料价钱的cost()方法。
2:顾客想要摩卡(Mocha),所以建立一个Mocha对象,并用它将DarkRoast对象包(wrap)起来。Mocha对象是一个装饰者,他的类型“反应”了它所装饰的对象(本例中就是Beverage)。所谓的“反应”,指的就是两者类型一致。
3:顾客也想要奶泡(Whip),所以需要建立一个Whip装饰者,并用它将Mocha对象包装起来。别忘了,DarkRoast继承自Beverage,并有一个cost()方法,用来计算饮料价钱。所以,被Mocha和Whip包起来的DarkRoast对象仍然可以具有DarkRoast的一切行为,包括调用它的cost()方法。
4:现在,该是顾客算钱的时候了,通过调用最外圈装饰者(Whip)的cost()就可以办得到。Whip的cost()会先委托它装饰的对象(也就是Mocha)计算出价钱,然后再加上奶泡的价钱。
到此。我们知道了:
1.装饰者和被装饰对象有相同的超类型。
2.你可以用一个或多个装饰者包装一个对象。
3.几人装饰者和被装饰者对象有相同的超类型,所有在任何需要原始对象(被包装的)的场合,可以用装饰过的对象代替它。
4.装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,以达到特定的目的。(关键点)
5.对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象。
定义装饰者模式:动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
看看它的类图:
装饰者我们的饮料
让星巴兹饮料也能符合此框架
在继承和组合之间,观念有一些混淆,下面有一组问答:
问:看看类图。CondimentDecorator扩展自Beverage类,这里用到了继承,不是吗?
答:的确如此。这么做重点在于,装饰者和被装饰者必须是一样的类型,也就是有共同的超类,这是相当关键的地方,在这里,我们利用继承达到“类型匹配”,而不是利用继承获得“行为”。
问:我知道为何装饰者需要和被装饰者(即被包装的组件)有相同的“接口”,因为装饰者必须能取代被装饰者,但是行为又是从哪里来的?
答:当我们将装饰者与组件组合时,就是在加入新的行为,所得到的新行为,并不是集成自超类,而是由组合对象得到的。
问:继承Beverage抽象类,是为了有正确的类型,而不是集成它的行为,行为来自装饰者和基础组件,或与其他装饰者之间的组合关系。
答:正是如此。
问:如果我们西药继承的是component类型,为什么不Beverage类设计成一个接口,而是设计成一个抽象类呢?
答:当初我们从星巴兹拿到这个程序时,Beverage已经是一个抽象类。通常装饰者模式是采用抽象类,但是在java中可以使用接口,通常都努力避免修改现有的代码,所以,如果抽象类运作得好好的,还是别去修改它。
写下星巴兹的代码
先从Beverage类下手:
abstract class Beverage{
String description="Unknown Beverage";
public String getDescription(){
return description;
}
public abstract double cost();
}
让我们也来实现Condiment(调料)抽象类,也就是装饰者类:
//首先必须让CondimentDecorator能够取代Beverage,所以将CondimentDecorator扩展自Beverage
abstract class CondimentDecorator extends Beverage{
//所有的调料装饰者都必须重新实现getDescription方法,稍后我们会解释
@Override
public abstract String getDescription();
}
写饮料的代码
现在,已经有了基类,让我们开始实现一些饮料吧,先从浓缩咖啡(Espresso)开始,别忘了,我们需要为具体的饮料设置描述,而且还必须实现cost()方法。
//首先,让Espresso扩展自Beverage类,因为Espresso是一种饮料。
class Espresso extends Beverage{
//为了要设置饮料的描述,我们写了一个构造器,description实例变量继承自Beverage
public Espresso(){
description="Espresso";
}
//最后,需要计算Espresso的价钱,现在不需要管调料的价钱,直接把Espresso的价格返回即可
@Override
public double cost() {
return 1.99;
}
}
class HouseBlend extends Beverage{
public HouseBlend(){
description="House Blend Coffee";
}
@Override
public double cost() {
return 0.89;
}
}
class DarkRoast extends Beverage{
public DarkRoast(){
description="DarkRoast";
}
@Override
public double cost() {
return 0.99;
}
}
class Decaf extends Beverage{
public Decaf(){
description="Decaf";
}
@Override
public double cost() {
return 1.05;
}
}
写调料代码
如果你回头看看装饰者模式的类图,将发现我们已经完成了抽象组件(Beverage),有了具体组件(HouseBlend),也有了抽象装饰者(CondimentDecorator)。现在, 我们就来实现具体装饰者,先从摩卡下手:
//摩卡是一个装饰者,所以让它扩展自CondimentDecorator。别忘了,CondimentDecorator扩展自Beverage
class Mocha extends CondimentDecorator{
Beverage beverage;
public Mocha(Beverage beverage){
this.beverage=beverage;
}
//返回加入调料后的描述
@Override
public String getDescription() {
return beverage.getDescription()+",Mocha";
}
//返回装饰后的价格
@Override
public double cost() {
return 0.20+beverage.cost();
}
}
class Soy extends CondimentDecorator{
Beverage beverage;
public Soy(Beverage beverage){
this.beverage=beverage;
}
@Override
public String getDescription() {
return beverage.getDescription()+",Soy";
}
@Override
public double cost() {
return 0.15+beverage.cost();
}
}
class Whip extends CondimentDecorator{
Beverage beverage;
public Whip(Beverage beverage){
this.beverage=beverage;
}
@Override
public String getDescription() {
return beverage.getDescription()+",Whip";
}
@Override
public double cost() {
return 0.10+beverage.cost();
}
}
这里用来下订单的一些测试代码:
class StarbuzzCoffee{
public static void main(String arg[]){
Beverage beverage=new Espresso();
beverage=new Mocha(beverage);
beverage=new Whip(beverage);
System.out.println(beverage.getDescription()+"$"+beverage.cost());
}
}
真实世界的装饰者:Java I/O
java.io包内的类太多,如果你已经知道装饰者模式,这些I/O相关的类对你来说应该更有意义,因为其中许多类都是装饰者。下面是一个典型的对象集合:
装饰java.io类
编写自己的Java I/O装饰者
编写一个装饰者,把输入流内的所有大写字符转换成小写.
class LowerCaseInputStream extends FilterInputStream{
protected LowerCaseInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int c=super.read();
return (c==-1?c:Character.toLowerCase(c));
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int result=super.read(b,off,len);
for(int i=off;i<off+result;i++){
b[i]= (byte) Character.toLowerCase(b[i]);
}
return result;
}
}
装饰者有能力为设计注入弹性,但是也有不好的地方,又是后会在设计中加入大量的小类,会导致别人不容易了解我的设计方式。拿Java I/O库来说,在人们第一次接触的时候无法轻易地理解,但是认识到这些类都是用来包装InputStream的,一切就简单多了。
采用装饰者在实例化组件时,将增加代码的复杂度,一旦使用装饰者模式,不只需要实例化组件,还要把此组件包装进装饰者中,天知道有多少个。
但是后面介绍的工厂模式和生成器模式,他们对这个问题有很大的帮助。
热门评论
为什么不把Beverage对象定义在CondimentDecorator 类中呢
确实,可以仔细的检查下,有很多的错别字。处理下,就更好了。
把书中的精华和必要信息,全部都留下来了,相当辛苦。。感谢。
不过偶尔有一些错别字,瑕不掩瑜~