三、装饰者模式 (Decorator Pattern)《HeadFirst设计模式》读书笔记
装饰者模式可以实现在不修改任何代码的情况下,给对象赋予新的功能。
1.使用继承的缺陷:
假设一家咖啡店的类设计是有一个抽象父类Beverage(饮料):
public abstract class Beverage { //由子类设置,用来描述具体饮料 protected String description; //计算总价的抽象方法 public abstract double cost(); public String getDescription() { return description; } }
具体的cost()方法由子类实现,比如DarkRost(深烘培咖啡)的价格是0.1:
public class DarkRoast extends Beverage { public DarkRoast(String description) { super.description = description; } @Override public double cost() { return 0.1; } }
现在进行功能扩展,要求可以加入各种配料,如milk、soy(豆浆)、mocha(摩卡)、whip(奶泡),如果我们把每一种配料组合都写成一个子类(如DarkRostWithMilk)的话,会造成类数量的猛增,而且在增加配料或者修改价格的时候会造成维护上的困难。
因此进一步的,我们可以在父类Beverage中预先定义好每种配料,对应一个布尔值表示是否添加,父类的cost()也本身提供,直接将用到的配料价格加起来返回。
public abstract class NewBeverage { //由子类设置,用来描述具体饮料 protected String description; //配料变量 protected int milk; protected int soy; protected int mocha; protected int whip; //构造方法 public NewBeverage(String description, int milk, int soy, int mocha, int whip) { this.description = description; this.milk = milk; this.soy = soy; this.mocha = mocha; this.whip = whip; } //计算配料的总价格 public double cost(){ return milk*CondimentCost.MILK_COST + soy*CondimentCost.SOY_COST + mocha*CondimentCost.MOCHA_COST + whip*CondimentCost.WHIP_COST; } //省略getter和setter方法 }
子类重写cost()方法,在其中加入自己的价格。
public class NewDarkRoast extends NewBeverage { public NewDarkRoast(String description, int milk, int soy, int mocha, int whip) { super(description, milk, soy, mocha, whip); } @Override public double cost() { return super.cost() + CondimentCost.NEW_DARK_ROAST; } }
这里将价格统一放到了一个常量类中:
public class CondimentCost { //不同咖啡种类价格 public static double NEW_DARK_ROAST = 0.1; //配料价格 public static double MILK_COST = 0.01; public static double SOY_COST = 0.01; public static double MOCHA_COST = 0.01; public static double WHIP_COST = 0.01; }
不过这样计算得到的结果不太对,小数点后面有很多位数,后来换成了BigDecimal类型通过String传参得到的结果是准确的,这里就不展开了。
上面这种方法虽然不用写很多具体的子类,修改价格也可以直接在常量类中改,但是如果要新添加配料的话还是要到父类中去修改代码。除此之外,父类中包含了所有的配料和他们的getter/setter方法,子类可能只需要其中的几个但是却全部继承下来了。装饰者模式该出场了。
2.引出装饰者模式
如上图所示,先用装饰者类Mocha和Whip都继承它想要装饰的父类Beverage,首先Mocha包裹住DarkRoast对象,重写cost()方法,在方法内部调用DarkRoast的cost()方法,并将自己的价钱加进去;然后在外层同样的,Whip也包裹住Mocha对象,并在自己的cost()方法中调用Mocha的cost()方法并将自己的价钱加进去。通过这样一层层的包裹实现了cost()方法的增强。
这里你可能有两个疑问:
1.”包裹“具体指什么,怎么实现
2.为什么装饰者类Mocha和Whip都要继承想要装饰的类的抽象父类Beverage呢
首先第一个问题,”包裹“实际上指的就是组合,在Mocha内部声明类型为Beverage的成员变量,可以通过构造方法将具体的子类传进来,再调用它的cost()方法。通过具体的代码实现来看一下:
public class Mocha extends Beverage { private Beverage beverage; public Mocha(Beverage beverage) { this.beverage = beverage; } @Override public double cost() { return beverage.cost() + CondimentCost.MOCHA_COST; } }
public class Whip extends Beverage { private Beverage beverage; public Whip(Beverage beverage) { this.beverage = beverage; } @Override public double cost() { return beverage.cost() + CondimentCost1.WHIP_COST; } }
测试一下:
public class test03 { public static void main(String[] args) { Beverage darkRoast = new DarkRoast("i am darkroast"); Beverage mocha = new Mocha(darkRoast); Beverage whip = new Whip(mocha); System.out.println("darkroast + mocha + whip total cost: " + whip.cost()); } }
这就是装饰者模式的使用了,通过这个例子也可以看出,之所以让装饰者类继承目标类的抽象父类,是因为想让装饰者也可以被包裹,即装饰者同时也是个被装饰者。
装饰者模式的定义:动态的将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
而书中和上面代码示例有些不同的是抽象了一个装饰者的父类CondimentDecorator,由这个父类继承Beverage,再让Mocha、Whip等继承这个父类。类的关系如下:
那么为什么中间加一层呢?我的理解是这样的:
首先这样层级关系更清晰,明确抽象者类有哪些,不会和原本Beverage真正的子类混在一起;
还有虽然我们在例子中是想要增强cost()方法的功能,但是我们还是要把其它需要用到的原本Beverage中的方法重写一下,并通过装饰的方式调用具体的Beverage的子类去执行。举个例子:
public abstract class Beverage { //由子类设置,用来描述具体饮料 protected String description; //计算总价的抽象方法 public abstract double cost(); public String getDescription() { return description; } }
上面的getDescription()方法,如果我们在装饰者类中不重写的话,那么实际调用这个方法的时候,就不会委托给Beverage的具体子类去做,而是调用Beverage中的这个默认的方法,得到的description自然就是null,因此不管是不是需要增强的方法,都要去重写,如下所示:
public class Mocha extends Beverage { private Beverage beverage; public Mocha(Beverage beverage) { this.beverage = beverage; } @Override public double cost() { return beverage.cost() + CondimentCost1.MOCHA_COST; } @Override public String getDescription() { return beverage.getDescription(); } }
这样如果我们想要装饰DarkRoast对象,才会在真正执行的时候调用到DarkRoast的getDescription()方法,同样的在Whip中我们也要这样实现,那么如果这样的方法有很多,装饰者类也有很多的话,写起来就会很麻烦,所以可以抽象出一个父类,在父类中将一些方法预先都准备好,就不用再在具体的装饰者类中去重写了。
在JDK的输入输出流中就使用了装饰者模式,如下是用BuffedInputStream读取文件的一个demo(这里使用的JDK8,所以用的try-with-resource异常处理方式,在try代码块结束后自动释放资源):
public class ReadFilesDemo { public static void main(String[] args) throws IOException { try (InputStream fis = new FileInputStream("D:/learnspace/demo.txt");
InputStream bis = new BufferedInputStream(fis)) { int len; byte[] bytes = new byte[1024]; while ((len = bis.read(bytes)) != -1) { //将读取到的内容直接输出 System.out.println(new String(bytes, 0, len)); } } catch (IOException e) { e.printStackTrace(); } } }
这里InputStream就是我们想要扩展功能的类,想通过buffer实现更高效的读取,BufferedInputStream就是一个具体的装饰者类,它首先继承了FilterInputStream,而FilterInputStream继承了InputStream,在FilterInputStream中包含了很多重写的方法,只是简单的在方法内部通过多态的方式委托给实际InputStream的具体子类去做了,因为FilterInputStream也有很多子类,这样通过继承可以减少代码的重复。这里举个例子:
public class FilterInputStream extends InputStream { protected volatile InputStream in; //构造方法 protected FilterInputStream(InputStream in) { this.in = in; } //重写了read(),只是简单调用了in的read()方法 public int read() throws IOException { return in.read(); } ... }
而具体的增强read()方法的代码是在FilterInputStream的子类BufferedInputSream中重写的:
public synchronized int read(byte b[], int off, int len) throws IOException { getBufIfOpen(); // Check for closed stream if ((off | len | (off + len) | (b.length - (off + len))) < 0) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return 0; } int n = 0; for (;;) { int nread = read1(b, off + n, len - n); if (nread <= 0) return (n == 0) ? nread : n; n += nread; if (n >= len) return n; // if not closed but no bytes available, return InputStream input = in; if (input != null && input.available() <= 0) return n; } }
3.总结:
装饰者模式在继承之外提供了一种新的方式拓展被装饰者的行为,但同时也会有一些问题,比如会产生很多小类,会在实例化组件时增加复杂度(要创建很多装饰者类),还有类型问题(书中P104页写的,没太看明白具体指什么),因此要合理的使用装饰者模式。