设计模式之装饰者模式(Decorator)

《Head First 设计模式》中描述了一个场景:星巴兹(Starbuzz)咖啡店的咖啡种类(HouseBlend、DarkRoast、Decaf、Espresso 等)增长迅速,同时客户购买咖啡时,要求在其中加入各种调料,例如:蒸奶(Milk)、豆浆(Soy)、摩卡(Mocha)或覆盖奶泡(Whip),订单系统需要根据咖啡种类和所加入的调料收取不同的费用,需要设计一个支持这些功能的订单系统。

糟糕的设计

在支持调料之前,星巴兹订单系统的设计如上图所示,为支持新功能,最简单粗暴的方式就是:为每种咖啡和调料的组合生成一个新类,例如:HouseBlendWithMilkAndMocha,每个类覆盖 cost() 方法返回对应的价格就可以了。但是,随着咖啡和调料种类的增多,必然会造成类的极具膨胀,而且每新增一种咖啡或调料,都要对现有代码逻辑做很多侵入性的修改,最终导致无法维护。

另外一种可行的方案是:在 Berverage 类中设置各种调料是否添加的布尔值,其 cost() 方法计算要加入的各种调料的价格;子类仍将覆盖 cost() 方法,但是会调用父类的 cost(),计算出咖啡加上调料的价格:

public class Beverage {

// 为 milkCost、soyCost、mochaCost 和 whipCost 声明实例变量
// 为 milk、soy、mocha 和 whip 声明 getter 和 setter 方法

public double cost() {
float condimentCost = 0.0;
if (hasMilk()) {
condimentCost += milkCost;
}
if (hasSoy()) {
condimentCost += soyCost;
}
if (hasMocha()) {
condimentCost += mochaCost;
}
if (hasWhip()) {
condimentCost += whipCost;
}
return condimentCost;
}
}

public class DarkRoast extends Beverage {

public DarkRoast() {
description = "Most Excellent Dark Roast";
}

public double cost() {
return 1.99 + super.cost();
}
}

但是这个方案仍然有很多问题:

  • 调料价格的改变会使我们更改现有代码
  • 一旦出现新的调料,需要在超类添加新的布尔值,并改变超类中的 cost() 方法
  • 以后可能会开发出新咖啡,某些调料可能并不适合加入,但是这个设计中每种咖啡都继承了所有调料的方法
  • 万一顾客想要双倍摩卡咖啡怎么办

装饰者模式

到目前为止,星巴兹订单系统遇到的问题有:类数量爆炸、设计死板,以及基类中加入的新功能并不适用于所有的子类。我们需要转换一下思路:以咖啡为主体,然后用调料来「装饰」咖啡,例如顾客想要摩卡深焙咖啡,要做的是:

  • 拿一个深焙咖啡(DarkRoast)对象
  • 以摩卡(Mocha)对象装饰它
  • 调用 cost() 方法,并依赖委托(delegate)将调料的价钱加上去

public abstract class Beverage {
String description = "Unknown Beverage";

public String getDescription() {
return description;
}

public abstract double cost();
}

public class DarkRoast extends Beverage {
public DarkRoast() {
description = "Dark Roast Coffee";
}

public double cost() {
return 1.09;
}
}

public abstract class CondimentDecorator extends Beverage {
public abstract String getDescription();
}

public class Mocha extends CondimentDecorator {
Beverage beverage;

public Mocha(Beverage beverage) {
this.beverage = beverage;
}

public String getDescription() {
return beverage.getDescription() + ", Mocha";
}

public double cost() {
return .20 + beverage.cost();
}
}

public class StarbuzzCoffeeTest {

public static void main(String[] args) {
Beverage beverage = new DarkRoast();
beverage = new Mocha(beverage);
System.out.println(beverage.getDescription() + " $" + beverage.cost());
}
}

// Dark Roast Coffee, Mocha $1.29

IO 库中的样例

Java IO 库也是用了装饰者模式,InputStream 是抽象基类,FileInputStream 等是具体的实现类,FilterInputStream 是一个抽象装饰类,BufferedInputStream 等是具体的装饰者,上面的类图做了简化,只展示了读取字符的方法 read(),我们也可以实现一个自己的装饰者类,只需要继承 FilterInputStream 即可:

public class LowerCaseInputStream extends FilterInputStream {
public LowerCaseInputStream(InputStream in) {
super(in);
}

public int read() throws IOException {
int c = super.read();
return (c == -1 ? c: Character.toLowerCase((char)c));
}
}

public class InputStreamTest() {

// 先用 BufferedInputStream 再用 LowerCaseInputStream 装饰
public static void main(String[] args) throws IOException {
try (
InputStream in =
new LowerCaseInputStream(
new BufferedInputStream(
new FileInputStream("test.txt")
)
)
) {
int c;
while ((c = in.read()) >= 0) {
System.out.println((char)c);
}
}
}
}

从上面两个使用装饰者模式的场景可以看出:

  • 装饰者和被装饰对象有相同的超类型
  • 可以用一个或多个装饰者包装一个对象
  • 装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,已达到特定的目的
0%