装饰器设计模式
什么是装饰器设计模式?
动态的将责任附加到对象身上。若要扩展功能,装饰着提供了比继承更有弹性的替代方案。
为什么要用装饰器设计模式?
试想下面这样一种需求。你供职的咖啡店最近准备设计一套订单系统以满足日益膨胀的业务需要。最初的设计是这样的:
屏幕快照 2016-11-24 下午10.43.48.png-32.4kB
Beverage 是一个抽象的饮料类。每种不同的咖啡,继承这个抽象类并提供不同的 cost 实现。也就是说,每种不同的类都有一个单独的实现,并且提供自己的 cost 方法。
顾客在购买咖啡时往往需要加入一些自己的调料,于是,你不得不为每一种调料的每一种饮料提供一个单独的实现类。哇塞,这简直是类爆炸!
但是别担心,要知道我毕竟是一个面向对象程序员。
我想到了一个点子,通过把各种调料设置到超类中,并提供一个 cost 方法计算所有调料的价格。然后在子类的 cost 中首先调用超类的 cost,然后与子类的 cost 方法的值相加后就可以得到某种饮料的价格,而不用为加入了不同调料的饮料设计单独的类了。
屏幕快照 2016-11-24 下午11.11.09.png-30.9kB
像上面这样,子类依然会覆盖父类的 cost,但是会调用 setMilk() 等方法设置调料信息,然后调动父类的 cost 计算出调料的总价,再与子类的 cost 相加。
遗憾的是,这样的设计依然不够好,它至少存在下面这些问题。
- 如果调料的价格发生变动,我们需要修改超类中的代码。
- 如果将来加入新的调料,我们需要修改超类中的代码。
- 如果顾客需要双倍摩卡的咖啡我们应该怎么办?
- 超类中包含的调料似乎并不能和所有饮料所吻合。
装饰器设计模式怎么用?
正当我一筹莫展的时候,有一个设计原则能够为我们提供一些思路
类应该对扩展开放,对修改关闭。
现在,让我们改变一种思路,尝试以饮料为主题然后使用调料对象来装饰饮料,然后最终计算出价格的做法。
也就是利用装饰器设计模式来帮我们解决问题。
在正式开始之前,你需要知道一些装饰器设计模式的特点。
- 装饰器和被装饰对象具有相同的超类型。
- 你可以用一个或多个装饰器包装一个被装饰对象。
- 装饰器和被装饰对象都用有同样的超类型。对于客户端来说,使用装饰器和被装饰对象,都应该拥有一样的体验。
- 装饰者可以在所委托被装饰者的行为之前、或之后,加上自己的行为。
- 对象可以动态的,任何时候被你的装饰器所装饰。
了解了装饰器设计模式的要点后,尝试在本次的咖啡订单系统上运用装饰器设计模式吧。
整体设计:
饮料的抽象类中拥有抽象的 cost() 方法,与被实现了的 getDescription 方法。
HouseBlend 与 Decaf 对象分别是抽象的饮料的实现,并且覆盖了 cost() 方法,实现了自己的逻辑。
Decorator 是一个装饰器抽象类(或接口)。按照上述装饰器的定义,为了让客户端的使用体验一致,Decorator 必须具有与被装饰对象同样的类型。装饰器可以选择实现被装饰对象的方法,也可以选择不实现。并且,每个装饰器都应该包含一个被装饰对象的实例,以便于在装饰器中调用被装饰对象的方法。
最后,每种不同的调料都是一个具体的装饰器实现。除了继承了装饰器对象以外还需要覆盖你想要装饰的被装饰方法。
被装饰对象
/**
* 装饰器设计模式
* 被装饰饮料抽象类
*/
public abstract class Beverage {
private String description;
public abstract int cost();
protected String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
/**
* 装饰器设计模式
* 被装饰对象
*/
public class Decaf extends Beverage {
public Decaf(String description) {
this.setDescription(description);
}
/**
* 返回decaf饮品的价格
*
* @return
*/
@Override
public int cost() {
return 30;
}
}
package designpattern.decorator;
/**
* 装饰器设计模式
* HouseBlend
* 饮品实现类
*/
public class HouseBlend extends Beverage {
@Override
public int cost() {
// 30元
return 30;
}
public HouseBlend(String description) {
setDescription(description);
}
}
装饰器类
/**
* 装饰器设计模式
* 装饰器抽象类
*/
public abstract class Decorator extends Beverage {
protected Beverage beverage;
protected Beverage getBeverage() {
return beverage;
}
protected void setBeverage(Beverage beverage) {
this.beverage = beverage;
}
/**
* 装饰器必须重写此方法
*
* @return
*/
@Override
protected abstract String getDescription();
}
package designpattern.decorator;
/**
* 装饰器设计模式
* 牛奶调料装饰器
*/
public class Milk extends Decorator {
public Milk(Beverage beverage) {
this.beverage = beverage;
}
/**
* 重写cost方法
* 调用被装饰者的方法再加上本身调料的价格
*
* @return
*/
@Override
public int cost() {
return 5 + beverage.cost();
}
@Override
protected String getDescription() {
return "牛奶" + beverage.getDescription();
}
}
package designpattern.decorator;
/**
* 装饰器设计模式
* 调料装饰器类
*/
public class Mocha extends Decorator {
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
/**
* 重写cost方法。返回被装饰后的价格
*
* @return
*/
@Override
public int cost() {
return beverage.cost() + 6;
}
@Override
protected String getDescription() {
return "抹茶" + beverage.getDescription();
}
}
测试
/**
* 装饰器设计模式
* 测试类
*/
public class DecoratorTester {
public static void main(String[] args) {
// 创建被装饰对象
Beverage beverage = new Decaf("decaf");
System.out.println(beverage.cost());
System.out.println(beverage.getDescription());
// 客户要求添加牛奶
Decorator milkDecaf = new Milk(beverage);
System.out.println(milkDecaf.cost());
System.out.println(milkDecaf.getDescription());
// 客户还要求添加抹茶
Decorator mochaMilkDecaf = new Mocha(milkDecaf);
System.out.println(mochaMilkDecaf.cost());
System.out.println(mochaMilkDecaf.getDescription());
}
}
30
decaf
35
牛奶decaf
41
抹茶牛奶decaf
通过装饰器模式,动态的给被装饰对象加上了不同的行为。
现在,让我们来回想一下将来可能出现的业务变动
- 如果调料的价格发生变动。
-> 不需要修改超类的代码。我们唯一需要做的就是重新创建一个新的调料类,然后在装饰对象时使用它就可以了。 - 如果将来加入新的调料。
-> 同上。你只需要创建新的调料对象。 - 如果顾客需要双倍摩卡的咖啡我们应该怎么办?
-> 太容易了,包装两次! - 超类中包含的调料似乎并不能和所有饮料所吻合。
-> 不吻合的情况下则不包装即可!例如你不可能拥有奶泡碧螺春。
总结
本篇文章的总结很简单,你需要做的就是回顾一下上面所提到过的装饰器设计模式的特性,并尝试理解它。
- 装饰器和被装饰对象具有相同的超类型。装饰器并非一定要是抽象类,也可以是一个接口。
- 你可以用一个或多个装饰器包装一个被装饰对象。
- 装饰器和被装饰对象都用有同样的超类型。对于客户端来说,使用装饰器和被装饰对象,都应该拥有一样的体验。
- 装饰者可以在所委托被装饰者的行为之前、或之后,加上自己的行为。
- 对象可以动态的,任何时候被你的装饰器所装饰。