本文来自www.lanttor.org
“给爱用继承的人一个全新的设计眼界”:学习如何使用对象组合的方式,做到在运行时装饰类。为什么呢?一旦熟悉了装饰的技巧,你将能够在不修改任何底层代码的情况下,给你的对象赋予新的职责。
基础类Beverage设计:
购买咖啡,还要求加入各种调料。例如奶,豆浆,摩卡等。所以订单系统要考虑到这些调料的价格。
使用继承的方式尝试设计:
这种极端的继承设计,导致了类的爆炸。(不可取)
利用实例变量和继承,追踪这些调料。在Beverage基类里,加上实例变量代表是否加上调料:
然后继承Beverage基类,实现具体的子类(每个子类代表菜单上的一种饮料):
这种做法,只需要5个类。而且结构也清晰。但存在的潜在的问题有:
更进一步的想法:
利用组合(composition)和委托(delegation)可以在运行时具有继承行为的效果。利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时进行动态扩展。
设计原则类应该对扩展开放,对修改关闭。 我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。如果能实现这样的目标,有什么好处呢?这样的设计具有弹性,可以应对改变,可以接受新的功能来应对改变的需求。 |
在这里,采用不一样的做法:我们要以饮料为主体,然后在运行时以调料来“装饰”(decorate)饮料。例如,如果顾客想要摩卡和奶泡深焙咖啡,那么要做的是:
1. 以DarkRoast对象开始
2. 顾客想要摩卡,所以建立Mocha对象,并用它将DarkRoast对象包(wrap)起来
3. 顾客想要奶泡,所以需要建立一个Whip装饰者,并用它将Mocha对象包起来。
4. 为顾客算钱
装饰者模式动态地将责任附件到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。 |
咖啡饮料新的设计框架:
Beverage.java:
package headfirst.decorator.starbuzz; public abstract class Beverage { String description = "Unknown Beverage"; public String getDescription() { return description; } public abstract double cost(); }
|
Beverage类是一个抽象类。getDescription()已经实现了,但是cost()必须在子类实现。
CondimentDecorator.java:
package headfirst.decorator.starbuzz; public abstract class CondimentDecorator extends Beverage { public abstract String getDescription(); }
|
所有的调料装饰者都必须重新实现getDescription()方法。
Espresso.java:
package headfirst.decorator.starbuzz; public class Espresso extends Beverage { public Espresso() { description = "Espresso"; } public double cost() { return 1.99; } } |
为了设置饮料的描述,我们写了一个构造器。description实例变量继承自Beverage。
家常咖啡类HouseBlend.java:
package headfirst.decorator.starbuzz; public class HouseBlend extends Beverage { public HouseBlend() { description = "House Blend Coffee"; } public double cost() { return .89; } } |
深焙咖啡类DarkRoast.java:
package headfirst.decorator.starbuzz; public class DarkRoast extends Beverage { public DarkRoast() { description = "Dark Roast Coffee"; } public double cost() { return .99; } } |
低咖啡因类Decaf.java:
package headfirst.decorator.starbuzz; public class Decaf extends Beverage { public Decaf() { description = "Decaf Coffee"; } public double cost() { return 1.05; } } |
Mocha.java:
package headfirst.decorator.starbuzz; 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(); } } |
魔卡是一个装饰者,所以让它扩展自CondimentDecorator。
要让摩卡能够引用一个Beverage,做法如下:
重新实现getDescription():不仅描述饮料,而且要描述装饰的调料。
重新实现cost(),首先把调用委托给装饰对象,以计算价钱,然后再加上Mocha的价钱,得到最后结果。
Milk.java:
package headfirst.decorator.starbuzz; public class Milk extends CondimentDecorator { Beverage beverage; public Milk(Beverage beverage) { this.beverage = beverage; } public String getDescription() { return beverage.getDescription() + ", Milk"; } public double cost() { return .10 + beverage.cost(); } } |
Soy.java:
package headfirst.decorator.starbuzz; public class Soy extends CondimentDecorator { Beverage beverage; public Soy(Beverage beverage) { this.beverage = beverage; } public String getDescription() { return beverage.getDescription() + ", Soy"; } public double cost() { return .15 + beverage.cost(); } } |
Whip.java:
package headfirst.decorator.starbuzz; public class Whip extends CondimentDecorator { Beverage beverage; public Whip(Beverage beverage) { this.beverage = beverage; } public String getDescription() { return beverage.getDescription() + ", Whip"; } public double cost() { return .10 + beverage.cost(); } } |
StarBuzzCoffee.java:
package headfirst.decorator.starbuzz; public class StarbuzzCoffee { public static void main(String args[]) { Beverage beverage = new Espresso(); System.out.println(beverage.getDescription() + " $" + beverage.cost()); Beverage beverage2 = new DarkRoast(); beverage2 = new Mocha(beverage2); beverage2 = new Mocha(beverage2); beverage2 = new Whip(beverage2); System.out.println(beverage2.getDescription() + " $" + beverage2.cost()); Beverage beverage3 = new HouseBlend(); beverage3 = new Soy(beverage3); beverage3 = new Mocha(beverage3); beverage3 = new Whip(beverage3); System.out.println(beverage3.getDescription() + " $" + beverage3.cost()); } } |
第一杯咖啡,浓缩咖啡,不需要调料;
第二杯咖啡,深焙咖啡,两份摩卡,一份奶泡;
第三杯咖啡,家常咖啡,一份豆浆,一份魔卡,一份奶泡。
测试结果:
Espresso $1.99 Dark Roast Coffee, Mocha, Mocha, Whip $1.49 House Blend Coffee, Soy, Mocha, Whip $1.34
|
下面是一个典型的对象集合,用装饰者来将功能结合起来,以读取文件数据:
BufferedInputStream和LineNumberInputStream都扩展自FilterInputStream,而FilterInputStream是一个抽象的装饰类。
编写一个装饰者,把输入流的所有大写字符转成小写。
LowerCaseInputStream.java:
package headfirst.decorator.io; import java.io.*; 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 int read(byte[] b, int offset, int len) throws IOException { int result = super.read(b, offset, len); for (int i = offset; i < offset+result; i++) { b[i] = (byte)Character.toLowerCase((char)b[i]); } return result; } } |
测试代码InputTest.java:
package headfirst.decorator.io; import java.io.*; public class InputTest { public static void main(String[] args) throws IOException { int c; try { InputStream in = new LowerCaseInputStream( new BufferedInputStream( new FileInputStream("test.txt"))); while((c = in.read()) >= 0) { System.out.print((char)c); } in.close(); } catch (IOException e) { e.printStackTrace(); } } } |
设置FileInputStream,先用BufferedInputStream装饰它,再用我们崭新的LowerCaseInputStream过滤器装饰它。
只用流来读取字符,一直到文件尾端。每读一个字符,就马上将它显示出来。
本文档完整的文档请参考“设计模式之装饰者模式”