1.声明
设计模式中的设计思想、图片和部分代码参考自《Head First设计模式》,作者Eric Freeman & Elisabeth Freeman & Kathy Siezza & Bert Bates。
在这里我只是对这本书进行学习阅读,并向大家分享一些心得体会。
2.装饰者模式的定义
装饰者模式指的是尽量不使用继承,且不必改变原类文件,就能够动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
3.需求案例
定义比较抽象,所以还是从一个案例入手。
星巴兹是非常著名的咖啡店,随着规模的壮大,店内的饮品种类也越来越丰富,于是星巴兹想要改进饮料系统。
开始时的设计:
但是事情没有那么简单,因为饮料需要添加各种调料,所以加上调料之后的类图变成了下面一坨:
这种设计简直不见看上去就就令人毛骨悚然,最重要的是,这种设计,根本无法维护,设想如果新添一种调料,那么需要添加多少个新的饮料类?或者,如果一种调料改变了价钱,那么又需要改变多少个类?
考虑到这些问题后,他们又拿出了新的方案。
4.初始的解决方案
父类结构如下:
子类继承图如下:
在父类的结构中,定义了是否添加各个调料的布尔值,这样调料就得以被子类复用了,因为子类只需要去负责设置这些调料是否添加即可,并且在价格的计算中,调料价格的计算也得到了复用,调料的价格交给了父类去计算,而子类只需要将自己的单价与父类计算出的调料做和即可。这样的设计,就避免了仅仅因为调料的不同,就需要设计不同类别的饮料的弊端。
父类代码如下:
//饮料的基类
public class Beverage {
//饮料的名称
String description = "未知饮料";
//饮料可加的调料
boolean milk; //是否加奶 单价1元
boolean soy; //是否加soy 单价2元
boolean mocha; //是否加摩卡 单价3元
boolean whip; //是否加奶泡 单价4元
//返回调料的名称
String getDescription() {
return description;
}
//返回调料的总价
double cost() {
int sum = 0;
if(milk) {
sum = sum + 1;
}
if(soy) {
sum = sum + 2;
}
if(mocha) {
sum = sum + 3;
}
if(whip) {
sum = sum + 4;
}
return sum;
}
//对这些调料进行设置
public boolean isMilk() {
return milk;
}
public void setMilk(boolean milk) {
this.milk = milk;
}
public boolean isSoy() {
return soy;
}
public void setSoy(boolean soy) {
this.soy = soy;
}
public boolean isMocha() {
return mocha;
}
public void setMocha(boolean mocha) {
this.mocha = mocha;
}
public boolean isWhip() {
return whip;
}
public void setWhip(boolean whip) {
this.whip = whip;
}
}
某个子类的代码如下:
//炭烧饮料 单价10元
public class DarkRoast extends Beverage{
//初始化饮料名称
public DarkRoast() {
description = "最优秀的炭烧";
}
//配料的价钱由父类cost方法决定,子类cost方法计算出本饮料的单价并做和。
double cost() {
return super.cost() + 10;
}
}
测试代码如下:
public class Test {
public static void main(String[] args) {
DarkRoast dr = new DarkRoast();
dr.setMilk(false);
dr.setMocha(false);
dr.setSoy(true);
dr.setWhip(true);
System.out.println("DarkRoast的价格是:"+dr.cost());
}
}
测试结果如下:
DarkRoast的价格是:16.0
但是这样的设计仍然有一下几个弊端:
-
调料价格的改变,需要我们去修改现有代码;
-
一旦出现新的调料,需要我们加上新的set/has方法(是否添加此调料的方法),并且需要修改父类中的cost方法;
-
如果以后出一种新的饮料,例如绿茶,但是对于绿茶来说,whip这个调料根本就没有set/has的意义,因为绿茶根本就不能加奶泡(我的意思是,绿茶加奶泡就没法喝了),所以对于绿茶来说,父类空间中,boolean whip这个变量就是无意义的;
-
如果某个顾客需要一份双倍摩卡的咖啡,怎么办(毕竟父类的cost方法中,只能计算出一份摩卡的价格)。
那么问题该如何解决呢?
5.装饰者模式
5.1装饰模式的解决思路
举例:以DarkRoast为例,如果一个顾客需要加Mocha和Whip的DarkRoast,那么这个过程可以这样设计:
5.2装饰者模式设计原则
装饰者和被装饰者对象需要有相同的超类型;
可以由一个或多个装饰者包装一个被装饰者;
既然装饰者和被装饰者有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象代替它;
装饰者可以在其所委托的被装饰者的行为前/后,加上自己的行为,以达到特定的目的;
对象可以在任何时候被装饰,所以可以在运行时动态地,不限量的用装饰者来装饰对象。
设计原则
类应该对扩展开放,对修改关闭。
5.3装饰者模式结构
装饰者模式:
动态地将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
5.4装饰者模式在此饮料框架下的类图
让调料继承Beverage是否合理?
答:刚开始我有这样的疑问,因为调料和饮料所属不同的类别,这样是否乱用了继承,其实调料继承饮料的目的在与想要得到类型匹配,而不是为了复用饮料的行为,如果只是想复用饮料的行为,那么当需要新行为时,还是得需要修改现在的代码。但是由于侧重点在于得到类型匹配,而且行为并不是通过继承得到的,而是通过组合得到(每一次装饰,都会得到新的行为),所以这样的设计还是较为合理的。
5.5代码设计
饮料的超类:(抽象组件)
//饮料的超类
public abstract class Beverage {
//名称
String description = "未知的饮料类型";
//获得饮料类型的方法
public String getDescription() {
return description;
}
//获得花费
public abstract double cost();
}
调料的超类:(抽象装饰者)
//调料的超类,为了让装饰者可以完全替代被装饰者,所以继承Beverage
public abstract class CondimentDecorator extends Beverage{
//获得调料的描述
public abstract String getDescription();
}
饮料实现类-浓缩咖啡(具体组件)
//浓缩咖啡
public class Espresso extends Beverage{
public Espresso() {
description = "浓缩咖啡";
}
//子类无需再管调料的价格,只关注自己的价格就可以
@Override
public double cost() {
return 1.0;
}
}
饮料实现类-HouseBlend(具体组件)
public class HouseBlend extends Beverage{
public HouseBlend() {
description = "House Blend 咖啡";
}
//子类无需再管调料的价格,只关注自己的价格就可以
@Override
public double cost() {
return 2.0;
}
}
饮料实现类-DarkRoast(具体组件)
//饮料实现类:DarkRoast
public class DarkRoast extends Beverage{
public DarkRoast() {
description = "DarkRoast";
}
//子类无需再管调料的价格,只关注自己的价格就可以
@Override
public double cost() {
return 3.0;
}
}
调料的实现类-豆浆(具体装饰者)
//豆浆
public class Soy extends CondimentDecorator{
/**
* 这里将被装饰者的引用保持下来
* 目的:
* getDescription()时可以获取之前所有被装饰者的描述
* cost()时可以获取之前所有被装饰的价格
*/
Beverage beverage;
public Soy(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDescription() {
//利用委托的办法获取之前的叙述,然后加上自己的描述
//这样得到的描述就是 饮料+各种调料
return beverage.getDescription() + ", 豆浆";
}
@Override
public double cost() {
// 同意利用委托获取的被装饰者的价格,然后加上自己的价格
return beverage.cost() + 0.2;
}
}
调料的实现类-摩卡(具体装饰者)
//摩卡
public class Mocha extends CondimentDecorator{
/**
* 这里将被装饰者的引用保持下来
* 目的:
* getDescription()时可以获取之前所有被装饰者的描述
* cost()时可以获取之前所有被装饰的价格
*/
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDescription() {
//利用委托的办法获取之前的叙述,然后加上自己的描述
//这样得到的描述就是 饮料+各种调料
return beverage.getDescription() + ", 摩卡";
}
@Override
public double cost() {
// 同意利用委托获取的被装饰者的价格,然后加上自己的价格
return beverage.cost() + 0.1;
}
}
调料的实现类-奶泡(具体装饰者)
//奶泡
public class Whip extends CondimentDecorator{
/**
* 这里将被装饰者的引用保持下来
* 目的:
* getDescription()时可以获取之前所有被装饰者的描述
* cost()时可以获取之前所有被装饰的价格
*/
Beverage beverage;
public Whip(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDescription() {
//利用委托的办法获取之前的叙述,然后加上自己的描述
//这样得到的描述就是 饮料+各种调料
return beverage.getDescription() + ", 奶泡";
}
@Override
public double cost() {
// 同意利用委托获取的被装饰者的价格,然后加上自己的价格
return beverage.cost() + 0.3;
}
}
测试类:
//星巴兹咖啡系统测试
public class Test {
public static void main(String[] args) {
//压缩咖啡,什么调料也不加
Beverage espresso = new Espresso();
System.out.println(espresso.getDescription());
System.out.println("价格:" + espresso.cost() + "元");
System.out.println();
//HouseBlend,将双倍摩卡和奶泡
Beverage houseBlend = new HouseBlend();
Beverage houseMocha1 = new Mocha(houseBlend);
Beverage houseMocha2 = new Mocha(houseMocha1);
Beverage houseWhip = new Whip(houseMocha2);
System.out.println(houseWhip.getDescription());
System.out.println("价格:" + houseWhip.cost() + "元");
System.out.println();
//DarkRoast,加摩卡、豆浆、奶泡
Beverage darkRoast = new DarkRoast();
Beverage darkMocha = new Mocha(darkRoast);
Beverage darkSoy = new Soy(darkMocha);
Beverage darkWhip = new Whip(darkSoy);
System.out.println(darkWhip.getDescription());
System.out.println("价格:" + darkWhip.cost() + "元");
System.out.println();
}
}
测试结果:
[浓缩咖啡]
价格:1.0元
[HouseBlend咖啡], 摩卡, 摩卡, 奶泡
价格:2.5元
[DarkRoast], 摩卡, 豆浆, 奶泡
价格:3.6元
6.真实世界的装饰者模式
6.1以装饰者模式的视角看待Java的I/O类库
以输入流为例:
举例:
被装饰的组件:FileInputStream
具体的装饰者1:BufferedInputStream
增加的功能:
1.利用缓冲区提高性能
2.readLine()一次读取一行数据
具体的装饰者2:LineNumberInputStream
增加了计算行数的能力
6.2FileInputStream使用案例
被读取的文件:
HELLO WORLD
测试程序:
//BufferedInputStream对FileInputStream进行了装饰
//使得我们现在不仅可以读取文件中的数据,而且还可以利用缓冲区,按行读取。
public class InputTest {
public static void main(String[] args) throws IOException {
int c;
InputStream in = new BufferedInputStream(new FileInputStream("test.txt"));
while((c = in.read()) >= 0) {
System.out.print((char)c);
}
in.close();
}
}
读取结果:
HELLO WORLD
6.3编写装饰BufferedInputStream的程序
现在编写程序对BufferedInputStream进行装饰,装饰后的效果是,新的类不仅可以利用缓冲区按行读取文件数据,而且还具有将文件中的字符从大写转换为小写的能力。
定义装饰类:
//继承FilterInputStream,FilterInputStream是所有InputStream的抽象装饰者
public class LowerCaseInputStream extends FilterInputStream{
protected LowerCaseInputStream(InputStream in) {
super(in);
}
//覆盖父类的read方法,读取字节
@Override
public int read() throws IOException {
//从此输入流中读取下一个数据字节。
//valuebyte作为int返回,范围为0到255.如果没有可用字节,则由于已到达流的末尾,则返回值-1
int c = super.read();
//将字符数据转换为小写,直至文件末尾
return c == -1 ? c : Character.toLowerCase((char)c);
}
}
测试程序:
//对BufferedInputStream进行装饰,在原来的基础上增加将字母转换为小写的能力
public class InputTest {
public static void main(String[] args) throws IOException {
int c;
InputStream in =
new LowerCaseInputStream(
new BufferedInputStream(
new FileInputStream("test.txt")));
while((c = in.read()) >= 0) {
System.out.print((char)c);
}
in.close();
}
}
输出结果:
hello world