概念什么的都是正确的废话!
所以不说废话,直接上栗子:
Starbuzz是一家咖啡连锁店,他们准备更新订单系统,以适应新品类的咖啡和调料的不断加入,达到供应需求。
Starbuzz改进了他们原先的系统,由饮料基类Beverage
开始,在Beverage
里有各种调料,cost()
方法计算饮料所用调料的总价钱,然后由子类(比如:HouseBlend
(综合)、DarkRoast
(超优深焙咖啡豆)、Decaf
(低咖啡因)、Espresso
(浓缩咖啡))在覆盖基类的cost()
,计算调料价钱和咖啡价钱的总和。
饮料基类Beverage
的Java代码为:
public class Beverage {
String description = "Unkown Beverage";
private boolean milk;
private boolean soy;
private double milkCost;
private double soyCost;
public Beverage() {
}
public String getDescription(){
return this.description;
}
public double cost() {
double sum = 0;
if (hasMilk())
sum += milkCost;
if (hasSoy())
sum += soyCost;
return sum;
}
public boolean hasMilk() {
return milk;
}
public boolean hasSoy() {
return soy;
}
// milk、soy等调料的getter和setter方法
}
咖啡品种以DarkRoast
为例:
public class DarkRoast extends Beverage {
public DarkRoast() {
description = "Most Excellent Dard Roast";
}
public double cost() {
return 1.99 + super.cost();
}
}
看起来好像可以应付所需,但是存在一些潜在问题:变化的部分(调料)一改变就涉及修改,不能动态地添加新功能!
有没有更好地办法改进呢?
从上面的改造可以看到,当新功能出现时(添加新的调料)时,就要修改Beverage
的代码,不能动态地组合对象以避免修改。这是不符合“开闭原则”的!
此处引出第5个设计原则:开放-关闭原则
类应该对扩展开放,对修改关闭
哎,前面的设计太死板,基类不能加入新功能以适应所有子类,太失败了!
经过思考,我们决定重新改造:以饮料为主体,在运行时以调料来“装饰”(decorate)饮料。比方说,如果顾客想要一杯深焙咖啡,加上摩卡和奶泡调料,那么做法是:
PS:此处的“委托”并不是类似C#语法中的delegate,而是指传递性的调用上一级的方法。
值得注意的是,调料Mocha
和Whip
也是Beverage
类型,因为他们要装饰的对象是Beverage
的子类,而它们本身亦可以被其他调料装饰。
由此构造我们可以知道:
看来这个想法可行,我们尝试设计一下装饰者和被装饰者之间的关系类图:
此处,Decorator
(装饰者)里有一个Component
(被装饰者)对象,用来保存对某个Component
的引用。这样其子类(各种具体装饰者)就可以装饰任何Component
对象了~
好了,我们将上面新的设计引进到Starbuzz的订单系统中,重新设计饮料和调料的类结构:
可能有人会有疑问:在策略模式和观察者模式里讲到,不是要多用组合少用继承吗,为什么这里是通过继承来动态地改变功能呢?
其实,装饰者和被装饰者必须是一样的类型,因为装饰者在装饰对象时它本身亦可以被其它装饰者装饰。所以,我们利用继承是想达到“类型匹配”,而不是通过继承获得“行为”。这点很重要!
如此一来,当装饰者和被装饰者组合时,就相当于动态地添加行为。这样得到的新行为,不是继承超类而是组合对象得到的。
下来通过代码来体验一下订单系统的类新结构:
首先建立基类Beverage
:
// 饮料基类
public abstract class Beverage {
protected String descrption;
public String getDescription(){
return descrption;
}
public abstract double cost();
}
再创建调料的抽象类CondimentDecorator
:
// 调料基类
public abstract class CondimentDecorator extends Beverage {
public abstract String getDescription();
}
饮料和调料基础类都有了,下面开始实现一些饮料:
// 超优深焙咖啡豆
public class DarkRoast extends Beverage {
DarkRoast() {
descrption = "DarkRoast";
}
public double cost() {
return 1.99;
}
}
// 浓缩咖啡
public class Espresso extends Beverage {
Espresso() {
descrption = "Espresso";
}
public double cost() {
return .89;
}
}
// 低咖啡因咖啡
public class Decaf extends Beverage {
Decaf() {
descrption = "Decaf";
}
public double cost() {
return 1.05;
}
}
再实现一些调料:
// 摩卡调料
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 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();
}
}
// 奶泡调料
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 .15 + beverage.cost();
}
}
// 牛奶调料
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();
}
}
最后来测试一下新的订单系统是否计算正确:
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 Decaf();
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Milk(beverage3);
System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
}
}
控制台的打印结果是:
Espresso $0.89
DarkRoast, Mocha, Mocha, Whip $2.54
Decaf, Soy, Mocha, Milk $1.5
不仅计算正确,而且这全新设计的订单系统设计也符合Starbuzz的需求!
当然,这里创建装饰者还不够简洁,待到后面接触“工厂模式”时会介绍更好的方式创建装饰者对象~
吃完栗子可以讲正确的废话了!
装饰者模式的定义:
动态地将责任附加到对象上,若要扩展功能,装饰者提供了比继承者更有弹性的替代方案。
我们知道,java.io包里有很多类,其实很多都是装饰者。
比如:用BuffereadInputStream
装饰FileInputStream
,用LineNumberInputStream
装饰BuffereadInputStream
。。。
为了更熟悉装饰者模式,我们自己也来写一个装饰者——把输入流内的所有大写字符转换成小写字符。
import java.io.*;
// 自己的java I/O 装饰者:将大写字符转小写
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;
}
}
测试一把:
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();
}
}
}