本系列记录个人在“设计模式”上的学习,文中错误之处,恳请读者指正。
装饰者模式:动态地将责任附加到对象上。如要扩展功能,装饰者提供了比继承更有弹性的替代方案。
设计原则:好的代码设计应该免于修改,又易于扩展。
扩展和修改实际上并不矛盾,扩展的是功能,修改的是代码,而功能的增加不一定要通过修改原来的代码来实现,当然很多时候直接修改是一种最简单,最容易想到的方法,有时候这也是一种不得已而为之的方法。好的设计能够保证在不修改源码的基础之上来扩展功能,当然这是很难的,需要一开始就对整个项目的需求有彻底的把握,同时还能对用户未来的需求进行预测,更要有丰富的经验和洞察力,或许只有神才能做到吧O(∩_∩)O哈哈~,当然我们学习设计模式,也是在往这个方向努力不是么,做不到百分之百,也要尽力的保证代码的质量。这让我想起了一些科幻电影中,主人公可以通过增删、替换身上的组件来改变自己的技能,而不需要重新再从头制作一个新的自己,或许这是本文要讨论的装饰者模式一个比较直观的体现。
装饰者模式比较直观的理解:我们可以拿一杯白开水作比喻。当我们往里面加入一些白糖的时候,它就变成了一杯糖水(糖水比白开水好喝,技能加强啦!),当然我们是不知足的,只有甜味显然不能满足我们的味觉,于是我们可以往里面再加一些奶粉,嘿嘿,之前的白开水现在变成了一杯牛奶了,还有白糖的甜味,要比之前的糖水多了些香味和色泽,口感也更好了,技能再次加强!这里面我们可以把之前那杯白开水看作是被装饰者,而白糖和奶粉看作是装饰者,通过装饰者的装饰,让被装饰者变得比之前更强(当然这里是更好喝~),这就是装饰者模式的直接目的。当然我们也可以通过继承来达到相同的目的,但是之前就提到过,在代码设计中“多用组合,少用继承”,组合相对于继承具有更好的灵活性,利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承相同的行为,如果采用组合去扩展对象的行为,就可以在运行时动态地进行扩展。装饰者模式是组合设计思想的一种很好的诠释。
下面还是以《Head First 设计模式》中的例子来结合代码讲解(这个系列的书,个人感觉设计模式是写的最好的一本,在设计模式书籍中,个人觉得这也是最好的一本,推荐购买),感觉我有在做广告的嫌疑...纯属有感而发哈,扯远了。本章节,书中采用了星巴兹咖啡订单系统(应该就是星巴克吧)来作为例子,一个咖啡厅的里面的咖啡有几十上百种(可能夸张了哈,反正很少去咖啡厅),这么多种类型的咖啡实际上如果按咖啡用料来分,也就几种,其他的都是在同种类型中通过加调料包啥的扩展出来的,按照书中的分类,我们将咖啡分成四种:HouseBlend-混合咖啡;DarkRoast-深培;Decaf-脱因咖啡;Espresso-特浓咖啡。其他各种各样的咖啡都是通过这四种里面添加不同的调料包调出来的。调料包有:蒸奶、豆浆、摩卡等。第一种设计思路是将每一种咖啡单独设计成一个类,显然这是不可行的,“组合爆炸”这个词听过吧,仅仅前面列的这四种咖啡和三种调料包,通过不同比例来混合得到的咖啡就已经趋于无穷啦,所以还是放弃这种思路吧,第二种就是按照面向对象的思想,通过继承来不断扩展咖啡的品种,这个思路相对于第一种是可行多了,但是除非星巴克觉得钱挣够了,不想再在咖啡界混了,否则星巴克就会不断的去创新,推出新的咖啡品种,但是这一创新,问题就来了,之前的设计不易于扩展,要想将新的产品加到订单系统,就必须修改之前的代码,这显然不是一个好的设计。第三种就是采用装饰者模式:(先把代码贴出来)
1 package pattern.decorator; 2 3 /** 4 * 所有咖啡的抽象基类, 5 * 这里不能使用接口,因为对于每一种咖啡都有一个描述,这个描述存放在一个变量之中 6 * @author CS_Xiaochao 7 * 8 */ 9 public abstract class Coffee { 10 11 private String description; //咖啡的描述信息 12 13 public abstract double cost(); //每一种咖啡的价格信息 14 15 public String getDescription() { 16 return description; 17 } 18 19 public void setDescription(String description) { 20 this.description = description; 21 } 22 23 }
这是所有装饰者和被装饰者的基类,description用来描述一杯咖啡,cost()用来计算价格。
1 package pattern.decorator; 2 3 /** 4 * 一杯混合咖啡 5 * @author CS_Xiaochao 6 * 7 */ 8 public class HouseBlend extends Coffee { 9 10 public HouseBlend(){ 11 this.setDescription("一杯HouseBlend"); 12 } 13 14 @Override 15 public double cost() { 16 return 12; //返回单价 17 } 18 19 } 20 21 package pattern.decorator; 22 23 /** 24 * 一杯浓咖啡 25 * @author CS_Xiaochao 26 * 27 */ 28 public class Espresso extends Coffee { 29 30 public Espresso() { 31 this.setDescription("一杯Espresso"); 32 } 33 34 @Override 35 public double cost() { 36 return 15; 37 } 38 39 } 40 41 package pattern.decorator; 42 43 /** 44 * 一杯脱因咖啡 45 * @author CS_Xiaochao 46 * 47 */ 48 public class Decaf extends Coffee { 49 50 public Decaf() { 51 this.setDescription("这是一杯Decaf"); 52 } 53 54 @Override 55 public double cost() { 56 return 20; 57 } 58 59 } 60 61 package pattern.decorator; 62 63 /** 64 * 一杯深培咖啡 65 * @author CS_Xiaochao 66 * 67 */ 68 public class DarkRoast extends Coffee { 69 70 public DarkRoast(){ 71 this.setDescription("一杯DarkRoast"); 72 } 73 74 @Override 75 public double cost() { 76 return 16; //返回单价 77 } 78 79 }
上面是所有的被装饰者,即前面提到的4种最基本的咖啡,等同于我们在前面例子中说到的白开水。每一种咖啡都有一个基本的描述信息和价格,一杯白开水也有价格嘛。
1 package pattern.decorator; 2 3 /** 4 * 调料包统一的基类 5 * @author CS_Xiaochao 6 * 7 */ 8 public abstract class Decorator extends Coffee { 9 10 //覆盖基类的获取描述信息的方法 11 //让所有子类都实现该方法,从而可以记录包装的信息 12 public abstract String getDescription(); 13 14 }
这是装饰者的基类,添加这个类的目的就是覆盖基类Coffee的getDescription()方法为抽象方法,从而强制所有子类去实现这个方法,这个方法的目的就是记录装饰者装饰被装饰者的过程,也就是记录被哪些装饰者给装饰过。
1 package pattern.decorator; 2 3 /** 4 * 牛奶调料包 5 * @author CS_Xiaochao 6 * 7 */ 8 public class Milk extends Decorator { 9 10 private Coffee coffee; //记录被装饰者的信息 11 12 public Milk() { 13 14 } 15 16 public Milk(Coffee coffee) { 17 this.coffee = coffee; 18 } 19 20 @Override 21 public String getDescription() { 22 //修改描述信息 23 //记录被装饰的过程 24 return coffee.getDescription() + ", 加入牛奶"; 25 } 26 27 @Override 28 public double cost() { 29 //修改价格信息 30 //在原来价格的基础上加上调料包的价格 31 return coffee.cost() + 3; //一份牛奶的价格为3元 32 } 33 34 public Coffee getCoffee() { 35 return coffee; 36 } 37 38 public void setCoffee(Coffee coffee) { 39 this.coffee = coffee; 40 } 41 42 } 43 44 45 package pattern.decorator; 46 47 /** 48 * 豆浆调料包 49 * @author CS_Xiaochao 50 * 51 */ 52 public class Soy extends Decorator { 53 54 private Coffee coffee; //记录被装饰者的信息 55 56 public Soy() { 57 58 } 59 60 public Soy(Coffee coffee) { 61 this.coffee = coffee; 62 } 63 64 @Override 65 public String getDescription() { 66 //修改描述信息 67 return coffee.getDescription() + ", 加入豆浆"; 68 } 69 70 @Override 71 public double cost() { 72 //修改价格信息 73 return coffee.cost() + 2; //一杯豆浆单价设为2元 74 } 75 76 public Coffee getCoffee() { 77 return coffee; 78 } 79 80 public void setCoffee(Coffee coffee) { 81 this.coffee = coffee; 82 } 83 84 } 85 86 87 package pattern.decorator; 88 89 /** 90 * 摩卡调料包 91 * @author CS_Xiaochao 92 * 93 */ 94 public class Mocha extends Decorator { 95 96 private Coffee coffee; 97 98 public Mocha() { 99 100 } 101 102 public Mocha(Coffee coffee) { 103 this.coffee = coffee; 104 } 105 106 @Override 107 public String getDescription() { 108 //修改描述信息 109 return coffee.getDescription() + ", 加入摩卡"; 110 } 111 112 @Override 113 public double cost() { 114 //修改价格信息 115 return coffee.cost() + 5; //一杯摩卡价格设为5元 116 } 117 118 public Coffee getCoffee() { 119 return coffee; 120 } 121 122 public void setCoffee(Coffee coffee) { 123 this.coffee = coffee; 124 } 125 126 }
上面是所有的装饰者,也就是前面提到的调料包,用于给原生咖啡调味,从而生成出许多其他口味的咖啡。每一个装饰者都是对传进来的被装饰者进行修饰一番。
1 package pattern.decorator; 2 3 /** 4 * 测试驱动类 5 * @author CS_Xiaochao 6 * 7 */ 8 public class Driver { 9 10 public static void main(String[] args) { 11 //要一杯加了牛奶、豆浆、摩卡的深培 12 Coffee darkRoast = new DarkRoast(); 13 darkRoast = new Milk(darkRoast); 14 darkRoast = new Soy(darkRoast); 15 darkRoast = new Mocha(darkRoast); 16 System.out.println("订单信息:" + darkRoast.getDescription() + " | 价格:" + darkRoast.cost() + "元"); 17 18 //要一杯双份摩卡,加牛奶的脱因咖啡 19 Coffee decaf = new Decaf(); 20 decaf = new Mocha(decaf); 21 decaf = new Mocha(decaf); 22 decaf = new Milk(decaf); 23 System.out.println("订单信息:" + decaf.getDescription() + " | 价格:" + decaf.cost() + "元"); 24 } 25 26 }
订单信息:一杯DarkRoast, 加入牛奶, 加入豆浆, 加入摩卡 | 价格:26.0元
订单信息:这是一杯Decaf, 加入摩卡, 加入摩卡, 加入牛奶 | 价格:33.0元
上面是驱动测试类,和运行结果。
以驱动测试中的第一个测试程序来说明程序的大致运行思路是:当创建一杯深培咖啡的对象的时候,一杯热腾腾的原生深培咖啡就真实存在了,这杯咖啡拥有最基本的描述信息“一杯DarkRoast”和基本的价格“16元”。当然一杯原生的咖啡的口感是不太好的,至少我不喜欢原生的咖啡,所以我们可以用一些调料包去改变它的口味,这里我们选用了牛奶、豆浆、摩卡,先加入牛奶,这时候这杯咖啡的描述信息中就添加了“加入牛奶”的装饰信息。同时给这杯咖啡加了价格,当然不能免费让你加,然后再用豆浆调料包去调味,过程同上,当被三个调料包都调味过之后,之前的一杯原生深培咖啡就变成一杯牛奶、豆浆、摩卡的深培咖啡,不管好不好喝,我们的任务已经完成了,嘿嘿。
Java中的I/O相关的类很好的利用了装饰者模式,用Java写过文件操作的读者都会发现,很多时候,为了创建一个输入输出流,需要写一串很长的代码(一层层的括号),才能满足我们的需要,正式因为最基本的InputStream和OutputStream只提供了最基本的输入输出操作,所以很多时候我们需要用其他的类来包装它,比如BufferedOutputStream,从而扩展输入输出流的功能。我们可以按自己的需求来添加或者删除某一个包装类,这也体现了运用组合设计的灵活性,但是缺点也是显而易见的,对于初学者来说,这样的包装往往会让人云里雾里,所以个人觉得在学习Java I/O时应该先去了解一下装饰者模式。
最后提醒一下:装饰者设计模式虽好,但是不要滥用哦,因为这有时候会让你的代码变得复杂,所以一个原则就是“在代码中需要经常改变的地方使用”。