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