上一篇:《设计模式02——观察者模式》
1.需求来源
有一家咖啡店由于店面规模的迅速扩张,他们准备更新订单系统,来符合他们的饮料供应要求,原先设计如下:
购买咖啡时,也可以要求在其中加入各种调料,例如:蒸奶(Steamed Milk)、豆浆(Soy) .摩卡(Mocha,也就是巧克力风味)或覆盖奶泡。星巴兹会根据所加入的调料收取不同的费用。所以订单系统必须考虑到这些调料部分。
于是在此背景下我们引入了另外一个设计的原则:类应该对外扩展开放,对修改关闭
这样设计的好处就是设计具有弹性,可以应对改变,可以接受新的功能来应对改变的需求。
下面我们的主角正式登场:
有以下几个角色:
- 饮料:HouseBlend,DarkRoast,Decaf,Espresso
- 调料:蒸奶(Steamed Milk),豆浆(Soy),摩卡(Mocha),奶泡(Whip)
那么我们接下来要做的就是调制一杯DarkRoast,用摩卡装饰,以奶泡装饰,然后调用cost方法并且依赖委托(delegate)将调料价钱加上去
通过上面一段话我们可以看到装饰者模式里有两个角色:装饰者也就是我们的调料,被装饰者即饮料,比如刚刚调制的DarkRoast
下面我们以装饰者构造饮料订单
- 装饰者和被装饰对象有相同的超类型。
- 可以用一个或多个装饰者包装一个对象。
- 既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象代替它。
- 装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,以达到特定的目的。
- 对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象。
2.定义装饰者模式
2.1 装饰者模式的说明
装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
通过上面的类图我们可以看到CondimentDecorator扩展自Beverage类,这么做的重点在于,装饰者和被装饰者必须是一样的类型,也就是有共同的超类,这是相当关键的地方。在这里,我们利用继承达到“类型匹配”,而不是利用继承获得行为”。
装饰者需要和被装饰者(亦即被包装的组件)有相同的“接口”,因为装饰者必须能取代被装饰者。但是行为又是从哪里来的?
当我们将装饰者与组件组合时,就是在加入新的行为。所得到的新行为,并不是继承自超类,而是由组合对象得来的。而且因为使用对象组合,可以把所有饮料和调料更有弹性地加以混和与匹配,非常方便。
接下来我们把这些思路编程代码展示一下,先从Beverage类下手,也就是我们的超类
/**
* 饮料的超类
*/
public abstract class Beverage {
String description = "未知的饮料";
public String getDescription() {
return description;
}
public abstract double cost();
}
Beverage很简单。让我们也来实现Condiment(调料)抽象类,也就是装饰者类吧:
/**
* 调料
* 首先,必须让Condiment Decorator 能够取代Beverage,所以将Condiment
* Decorator扩展Beverage类。
*/
public abstract class CondimentDecorator extends Beverage{
/**
* 所有的调科装饰奢都必须重新实现jetDescription()方法
* @return
*/
public abstract String getDescription();
}
接下来开始编写饮料的代码
为了我们的思路更加清晰我在将饮料和调料分别有哪些在书写一遍
- 饮料:HouseBlend(自酿咖啡),DarkRoast(黑咖啡),Decaf,Espresso(浓缩咖啡)
- 调料:蒸奶(Steamed Milk),豆浆(Soy),摩卡(Mocha),奶泡(Whip)
我们先从浓缩咖啡Espresso开始
/**
* 浓缩咖啡
* 首先.让Espresso扩展自Beveraze类.因为Espresso是
* ━种饮科。
*/
public class Espresso extends Beverage {
/**
* 为了要设置饮料的描述,我们写了一个构造器。记住.descxiption实例变量继承自Beverage
*/
public Espresso() {
this.description = "浓缩咖啡";
}
/**
* 最后,需要计算Espresso的价钱,现在不需墓啻调料的价钱
* @return
*/
@Override
public double cost() {
return 1.99;
}
}
黑咖啡
/**
* 饮料--黑咖啡
*/
public class DarkRoast extends Beverage {
public DarkRoast() {
description = "黑咖啡";
}
@Override
public double cost() {
return 0.99;
}
}
自酿咖啡
/**
* 饮料--自酿咖啡
*/
public class HouseBlend extends Beverage {
public HouseBlend() {
description = "自酿咖啡";
}
@Override
public double cost() {
return 1.0;
}
}
接下来我们就具体的实现装饰者,先从摩卡下手
/**
* 装饰者(调料)--摩卡
*/
public class Mocha extends CondimentDecorator {
/**
* 用一个实例变量记录饮料,也就是被装饰者。
*/
Beverage beverage;
/**
* 想办法让被装饰者(饮料)被记录到实例变量中。这里的做法是:把饮科当作构造器的参数,再由构造器将此饮科记录在实例变量中
*
* @param beverage
*/
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
/**
* 要计算带Mocha饮科的价钱。首先把调用委托给被装饰对象,以计算价钱.然后再加
* Mocha的价钱.得到最后结果。
*
* @return
*/
@Override
public double cost() {
return 0.20 + this.beverage.cost();
}
/**
* 我们希望叙述不只是描述饮料(例“OarkRoast”) ,
* 而是完整比连调科都描述出来(例如“DarkRoast,Mocha”) 。
* 所以首先利用委托的做法,得到一个叙述,然后在其后加上附加的叙连(例“Mocha”)。
*
* @return
*/
@Override
public String getDescription() {
return beverage.getDescription() + ",Mocha";
}
}
接下来依次实现豆浆(Soy),奶泡(Whip)
- 1.Soy
/** * 装饰者(调料)--豆浆 */ public class Soy extends CondimentDecorator { /** * 用一个实例变量记录饮料,也就是被装饰者。 */ Beverage beverage; /** * 想办法让被装饰者(饮料)被记录到实例变量中。这里的做法是:把饮科当作构造器的参数,再由构造器将此饮科记录在实例变量中 * * @param beverage */ public Soy(Beverage beverage) { this.beverage = beverage; } /** * 要计算带Mocha饮科的价钱。首先把调用委托给被装饰对象,以计算价钱.然后再加 * Mocha的价钱.得到最后结果。 * * @return */ @Override public double cost() { return 0.30 + this.beverage.cost(); } /** * 我们希望叙述不只是描述饮料(例“OarkRoast”) , * 而是完整比连调科都描述出来(例如“DarkRoast,Mocha”) 。 * 所以首先利用委托的做法,得到一个叙述,然后在其后加上附加的叙连(例“Mocha”)。 * * @return */ @Override public String getDescription() { return beverage.getDescription() + ",豆浆"; } }
- 2.Whip
/** * 装饰者(调料)--奶泡 */ public class Whip extends CondimentDecorator { /** * 用一个实例变量记录饮料,也就是被装饰者。 */ Beverage beverage; /** * 想办法让被装饰者(饮料)被记录到实例变量中。这里的做法是:把饮科当作构造器的参数,再由构造器将此饮科记录在实例变量中 * * @param beverage */ public Whip(Beverage beverage) { this.beverage = beverage; } /** * 要计算带Mocha饮科的价钱。首先把调用委托给被装饰对象,以计算价钱.然后再加 * Mocha的价钱.得到最后结果。 * * @return */ @Override public double cost() { return 0.40 + this.beverage.cost(); } /** * 我们希望叙述不只是描述饮料(例“OarkRoast”) , * 而是完整比连调科都描述出来(例如“DarkRoast,Mocha”) 。 * 所以首先利用委托的做法,得到一个叙述,然后在其后加上附加的叙连(例“Mocha”)。 * * @return */ @Override public String getDescription() { return beverage.getDescription() + ",奶泡"; } }
接下来我们开始下订单
public class StarBuckCoffee {
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()
+ " $" + String.format("%.2f", beverage2.cost()));
//调料为豆浆,摩卡,奶泡
Beverage beverage3 = new HouseBlend();//原料
beverage3=new Soy(beverage3);
beverage3=new Mocha(beverage3);
beverage3=new Whip(beverage3);
System.out.println(beverage3.getDescription()
+ " $" + String.format("%.2f", beverage3.cost()));
}
}
最终结果如下:
店主决定开始在菜单上加上咖啡的容量大小,供顾客可以选择小杯(tall)、中杯(grande) 、大杯(venti)。所以在Beverage类中加上了getSize()与setSize()。他们也希望调料根据咖啡容量收费,例如:小中大杯的咖啡加上豆浆,分别加收0.10、0.15、0.20美金。如何改变装饰者类应对这样的需求?
首先我们对超类Beverage进行更改
/**
* 饮料的超类
*/
public abstract class Beverage {
//小杯、中杯、大杯
public enum Size {
TALL, GRANDE, VENTI
}
;
Size size = Size.TALL;
String description = "未知的饮料";
public String getDescription() {
return description;
}
public Size getSize() {
return size;
}
public abstract double cost();
}
由于是杯子的大小只针对于豆浆这一个装饰者,所以我们对豆浆Soy类进行修改
/**
* 装饰者(调料)--豆浆
*/
public class Soy extends CondimentDecorator {
/**
* 用一个实例变量记录饮料,也就是被装饰者。
*/
Beverage beverage;
/**
* 想办法让被装饰者(饮料)被记录到实例变量中。这里的做法是:把饮科当作构造器的参数,再由构造器将此饮科记录在实例变量中
*
* @param beverage
*/
public Soy(Beverage beverage) {
this.beverage = beverage;
}
/**
* 要计算带饮科的价钱。首先把调用委托给被装饰对象,以计算价钱.然后再加
* 豆浆的价钱.得到最后结果。
* @return
*/
@Override
public double cost() {
double cost = beverage.cost();
switch (beverage.getSize()) {
case TALL:
cost += 0.10;
break;
case GRANDE:
cost += 0.15;
break;
case VENTI:
cost += 0.20;
break;
}
return cost;
}
/**
* 我们希望叙述不只是描述饮料(例“OarkRoast”) ,
* 而是完整比连调科都描述出来(例如“DarkRoast,Mocha”) 。
* 所以首先利用委托的做法,得到一个叙述,然后在其后加上附加的叙连(例“Mocha”)。
*
* @return
*/
@Override
public String getDescription() {
return beverage.getDescription() + ",豆浆";
}
}
最后我们再次进行测试
public class StarBuckCoffee {
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()
+ " $" + String.format("%.2f", beverage2.cost()));
//调料为豆浆,摩卡,奶泡
Beverage beverage3 = new HouseBlend();//原料
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription()
+ " $" + String.format("%.2f", beverage3.cost()));
//和上面同样的配方,但是我们将杯子的大小改成中杯(默认是小杯)
Beverage beverage3_1 = new HouseBlend();//原料
beverage3_1.size = Beverage.Size.GRANDE;
beverage3_1 = new Soy(beverage3_1);
beverage3_1 = new Mocha(beverage3_1);
beverage3_1 = new Whip(beverage3_1);
System.out.print("将杯子大小改为GRANDE--中杯之后:");
System.out.println(beverage3_1.getDescription()
+ " $" + String.format("%.2f", beverage3_1.cost()));
}
}
运行结果:
3.装饰java.io类
我们完成下面这个功能:
当读取“I know the veeorator Pattern therefore I RuLE!”,装饰者会将它转成“i know the decorator patfern therefore i rule!”
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class InputTest {
public static void main(String[] args) {
int c;
try {
//设置FileInputStream,先用
//BufferedInputStream装饰它,再用我们崭新的LowerCaseInputStream过滤装饰它。
InputStream in = new LowerCaseInputStream(new BufferedInputStream(
new FileInputStream("D:\\program\\java_program\\design-mode\\DesignMode\\src\\main\\java\\com\\zhiyi\\design\\装饰者模式\\test.txt")));
while ((c = in.read()) >= 0) {//只用流来读取字符,一直到文件尾端。每读一个字符,就马上将它显示出来。
System.out.print((char) c);
}
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果如下:
至此对于装饰者模式就结束啦
下一篇:《设计模式04—工厂模式》