运行时扩展,远比编译时期的继承威力更大。本章可以成为“给爱用继承的人一个全新的设计眼界”。
本章将再度讨论典型的继承滥用问题。本章中将讲解如何使用对象组合的方式,做到运行时装饰类。一旦熟悉了装饰的技巧,则能够在不修改任何底层代码的情况下,给对象赋予新的职责。
咖啡店的故事:一家快速扩张的咖啡连锁店准备更新订单系统,以合乎他们的饮料供应要求。
原先的类设计:
购买咖啡时,可以要求在其中加入各种调料,例如:蒸奶(Steamed Milk)、豆浆(Soy)、摩卡(Mocha,也就是巧克力风味)或者覆盖奶泡。咖啡店会根据加入的调料收取不同的费用,因此订单系统必须考虑到这些调料部分。
这是该咖啡店的第一个尝试:
很明显,这制造了一个维护噩梦。因为如果某个调料的价格上涨或者需要新增一种调料,则会导致维护困难。
书上的初步尝试:
现在加入子类,每个类就代表菜单上的一种饮料:
这样改变带来的影响:
当一些需求或者因素改变时将会影响这个设计:
关于组合和委托:
虽然继承威力强大,但是继承不总是能够实现最有弹性和最好维护的设计。
利用组合和委托可以在运行时具有继承行为的效果。
利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态地进行扩展。则可以利用此技巧把多个新职责,甚至是设计超类时还没想到的职责加在对象上,而且不需要修改原来的代码。
利用组合维护代码,能够通过动态地组合对象,写新的代码添加新功能,而无需修改现有代码,既然没有改变现有代码,那么引进Bug或者产生意外副作用的机会将大幅度减少。
设计原则:类应该对扩展开放,对修改关闭。
我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。实现这一目标的好处:具有弹性可以应对改变,可以接受新的功能来应对改变的需求。
相关问答:
Q1:对扩展开放,对修改关闭,听起来很矛盾,在设计的时候该如何兼顾?
A1:有一些聪明的OO技巧,允许系统在不修改代码的情况下,进行功能扩展。比如观察者模式,通过加入新的观察者,我们可以在任何时间扩展Subject(主题),而且不需要向主体中添加代码。以后,还会看到更多的扩展行为的其他OO设计技巧。
Q2:如何将某件东西设计成可以扩展,又禁止修改?
A2:在本章将使用装饰者模式的一个好例子,完全遵循开放-关闭原则。
Q3:如何设计的每个部分都遵循开放-关闭原则?
A3:通常,是办不到的,要让OO设计同时具备开放性和关闭性,又不修改现有的代码,需要花费许多时间和努力。一般来说,不会把设计的每个部分都这么设计,即便做到了,也可能是一种浪费。遵循开放-关闭原则,通常会引入新的抽象层次,增加代码的复杂度。那么需要将注意力集中在设计中最有可能改变的地方,然后应用开放-关闭原则。
通过问题引入已经得知,无法利用继承来完全解决问题,遇到的问题有:类数量爆炸、设计死板,以及基类加入的新功能并不适用于所有的子类。
因此,在这里要采用不一样的做法:要以饮料为主题,然后在运行时以调料来“装饰”(decorate)饮料。比如,如果顾客想要摩卡和奶泡深焙咖啡,那么需要做的是:
以装饰者构建饮料订单
第一步:以DarkRoast对象开始
第二步:顾客下你给要摩卡(Mocha),所以建立一个Mocha对象,并用它将DarkRoast对象包(wrap)起来。
第三步:顾客也想要奶泡(Whip),所以需要建立一个Whip装饰者,并用它将Mocha对象包起来。别忘了,DarkRoast继承自Beverage,且有一个cost()方法,用来计算饮料价钱。
第四步:到为顾客算钱的时候了。通过调用最外圈装饰者(Whip)的cost()就可以办得到。Whip的cost()会先委托它装饰的对象(也就是Mocha)计算出价钱,然后再加上奶泡的价钱。
到此则装饰结束。这就是目前所知道的一切:
接下来就看看装饰者模式的定义,并写一些代码,了解它到底是怎么工作的。
装饰者模式动态地将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
装饰者模式的类图:
需要知道的是:每个装饰者都包装一个组件,也就是说,装饰者有一个实例变量以保存某个Component的引用。装饰者本身也是继承自Component的。
将咖啡店的饮料应用在这个框架上:
组件其实就是被装饰者。
通过类图可知,CondimentDecorator扩展自Beverage类,这用到了继承。这么做的重点在于:装饰者和被装饰者必须是一样的类型,即拥有共同的超类,这是相当关键的,因为我们利用继承达到“类型匹配”,而不是利用继承获得“行为”。
装饰者需要和被装饰者(被包装的组件)有相同的“接口”,因为装饰者必须能取代被装饰者,但是行为又是从哪里来的?当我们将装饰者和组件组合时,就是在加入新的行为。所得到的新行为,并不是继承自超类,而是由组合对象得来的。继承Beverage抽象类,是为了有正确的类型,而不是继承他的行为。行为来自装饰者和基础组件,或与其他装饰者之间的组合关系。因为使用对象组合,可以把所有饮料和调料更加有弹性的加以混合与匹配,如果只是依赖继承,那么类的行为只能在编译时静态决定,即行为不是来自超类,那么就是子类覆盖后的版本。反之,利用组合,可以把装饰者混合着用,并且是在“运行时”。并且这样的话,就能够在任何时候,实现新的装饰者增加新的行为。如果依赖继承,每当需要新行为时,还得修改现有的代码。
如果需要继承的是component类型,为什么不把Beverage类设计成一个接口,而是设计成一个抽象类呢?通常装饰者模式采用抽象类,但是在JAVA中可以使用接口。尽管如此,通常我们都努力避免修改现有的代码,所以,如果抽象类运作得好好地,还是别去修改它。
首先从Beverage类下手,这不需要改变咖啡店原始的设计:
public abstract class Beverage { String description="Unkonwn Beverage"; public String getDescription() { return description; } public abstract double cost(); }
Beverage很简单。同样来实现调料的抽象类Condiment,,也就是装饰者类:
//首先,必须让CondimentDecorator能够取代Beverage,所以CondimentDecorator扩展自Beverage类 public abstract class CondimentDecorator extends Beverage{ //所有的调料装饰者都必须重新实现getDescription()方法。 @Override public abstract String getDescription(); }
基类已经建立完成了,则开始实现一些饮料。
首先创建 浓缩咖啡(Espresso)开始,我们需要为具体的饮料设置描述,而且还必须实现cost()方法。
//首先,让Espresso扩展自Beverage类,因为Espresso是一种饮料 public class Espresso extends Beverage{ //为了要设置饮料的描述,我们写了一个构造器,记住:description实例变量继承自Beverage。 public Espresso() { description="Espresso"; } //最后,需要计算Espresso的价钱,现在不管调料的价钱,直接把Espresso的价格返回即可 @Override public double cost() { return 1.99; } }
同样的,编写HouseBlend的相关代码:
public class HouseBlend extends Beverage{ public HouseBlend() { description="HouseBlend"; } @Override public double cost() { return .89; } }
上面完成了抽象组件,具体组件,也有了抽象装饰者。现在,就来实现具体装饰者。
首先编写摩卡Mohca类:
//Mocha是一个装饰者,所以让他扩展自 CondimentDecorator public class Mocha extends CondimentDecorator{ //要让Mocha能够引用一个Beverage,做法如下 //1.用一个实例变量记录饮料,也就是被装饰者 //2.想办法让被装饰者(饮料)被记录到实例变量中。这里的做法是: //把饮料当做构造器的餐宿,再由构造器将其饮料记录在实例变量中 Beverage beverage; public Mocha(Beverage beverage) { this.beverage = beverage; } @Override public String getDescription() { return beverage.getDescription()+",Mocha"; } //要计算带Mocha饮料的价格。首先把调用委托给被装饰对象,以计算价钱,然后再加上Mocha的价钱,得到最后结果 @Override public double cost() { return .20+beverage.cost(); } }
同理写下 Soy和Whip调料的代码:
public 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 .10+beverage.cost(); } } public 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 .15+beverage.cost(); } }
编写测试代码
public class StarbuzzCoffee { public static void main(String[] args) { Beverage beverage=new Espresso(); beverage=new Mocha(beverage); beverage=new Whip(beverage); System.out.println(beverage.getDescription()+" $ "+beverage.cost()); } }
实验结果: