设计模式目录:
概要
第1部分 引言
Central Perk的名字因为《老友记》而享誉全球,他们的分店几乎开遍世界各地。他们发展的实在是太快了,所以他们此时正在急于实现一套由计算机管理的自动化记账系统。在第一次研究了他们的需求以后,开发者设计了如下图的类结构:
Beverage是所有饮料的基类;cost()是抽象方法,所有子类都需要定义它们自己的cost()实现来返回特定饮料的价钱;description变量也是在子类里赋值的,表示特定饮料的描述信息,getDescription()方法可以返回这个描述;
除了咖啡以为,Central Perk还提供丰富的调味品,比如:炼乳、巧克力、砂糖、牛奶等,而且这些调味品也是要单独按份收费的,所以调味品也是订单系统中重要的一部分。
于是,考虑到调味品的管理,开发者又有了下面这样的类结构:
看了上面的类图,你一定有话要说!是的!这简直是太恐怖了,好像是整个类图都要爆炸了一样,而且以后随便增加一种调味品,继承于Beverage的子类还会翻倍!(因为理论上可能的咖啡种类数 = 咖啡类别数×调味品类别数) 我的神啊!
上面的情况绝对是不能容忍的,于是开发者们经过讨论,又提出了下面的设计方案件:
如图所示,这是改进后的Beverage基类。首先在基类里增加了表示是否包含特定调味品的布尔变量,如milk, soy等,然后提供了一些has(get)和set方法来设置这些布尔值;其次在Beverage类里实现cost()方法来计算调味品的价钱。所有咖啡子类将仍然覆盖cost()方法,只是这次它们需要同时调用基类的cost()方法,以便获得咖啡加上调味品后的总价。
看上去似乎这是一个不错的设计,那么下面我们再来给Beverage增加子类,如下图所示:
如图所示,这是改进后的Beverage基类。首先在基类里增加了表示是否包含特定调味品的布尔变量,如milk, soy等,然后提供了一些has(get)和set方法来设置这些布尔值;其次在Beverage类里实现cost()方法来计算调味品的价钱。所有咖啡子类将仍然覆盖cost()方法,只是这次它们需要同时调用基类的cost()方法,以便获得咖啡加上调味品后的总价。
看上去似乎这是一个不错的设计,那么下面我们再来给Beverage增加子类,如下图所示:
基类的cost()方法将计算所有调味品的价钱(当然是只包括布尔值为true的调味品),子类里的cost()方法将扩展其功能,以包含特定类型饮料的价钱。
OK! 现在我们似乎已经有了一个看上去还不错的设计,那么Central Perk的这个记账系统就按这个设计来实现就万事大吉了吗?等一下,还是让我们先从以前学习过的“找到系统中变化的部分,将变化的部分同其它稳定的部分隔开。”这个设计原则出发,重新推敲一下这个设计。
那么对于一家咖啡店来说,都有那些变化点呢?调味品的品种和价格会变吗?咖啡的品种和价格会变吗?咖啡和调味品的组合方式会变吗?YES! 对于一家咖啡店来说,这些方面肯定会经常发生改变的!那么,当这些改变发生的时候,我们的记账系统要如何应对呢? 如果调味品发生改变,那么我们只能从代码的层次重新调整Beverage基类,这太糟糕了;如果咖啡发生改变,我们可以增加或删除一个子类即可,这个似乎还可以忍受;那么咖啡和调味品的组合方式发生改变呢?如果顾客点了一杯纯黑咖啡外加两份砂糖和一份巧克力,或者顾客点了一杯脱咖啡因咖啡(Decaf)外加三份炼乳和一份砂糖呢?我倒!突然意识到,上面的设计根本不支持组合一份以上同种调味品的情况,因为基类里的布尔值只能记录是否包含某种调味品,而并不能表示包含几份,连基本的功能需求都没有满足。
好吧!我们已经分析了前面设计的失败之处,我们应该实现支持调味品的品种和价格任意改变而不需要修改已有代码的设计;我们还要实现支持咖啡品种和价格任意改变而不需要修改已有代码的设计(这点上面的设计通过继承算是实现了);还有就是支持咖啡和调味品的品种和份数任意组合而不需要修改已有代码的设计;还有就是代码重用越多越好了,内聚越高越好了,耦合越低越好了。
看来我们要实现的目标还真不少,那么我们到底该怎么做呢?说实话,我现在也不知道!我们需要先去拜访一下今天的主角—装饰者模式,看看她能给我们带来什么惊喜吧!
第2部分 装饰者模式介绍
先看一下官方的定义:
The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. (装饰者模式可以动态地给一个对象增加其他职责。就扩展对象功能来说,装饰者模式比生成子类更为灵活。)
这里我们要重点注意那个dynamically(动态的),什么是动态?静态又是什么?这是我们要重点区分的地方,后面我们还会专门讨论这个问题。下面先看看装饰者模式的类图:
Component(被装饰对象基类)
定义对象的接口,可以给这些对象动态增加职责;
ConcreteComponent(具体被装饰对象)
定义具体的对象,Decorator可以给它增加额外的职责;
Decorator(装饰者抽象类)
维护一个指向Component实例的引用,并且定义了与Component一致的接口;
ConcreteDecorator(具体装饰者)
具体的装饰对象,给内部持有的具体被装饰对象增加具体的职责;
先来说说上面提到的动态和静态的问题,所谓动态是说可以在系统运行时(RunTime)动态给对象增加其它职责而不需要修改代码或重新编译;所谓静态是说必须通过调整代码(DesignTime)才能给对象增加职责,而且系统还需要重新编译;从具体技术层面来说,对象的组合和继承正好对应于前面的动态和静态,因为通过对象组合建立的交互关系不是在代码中(DesignTime)固定死的,而是在运行时(RunTime)动态组合的;而通过继承建立的关系是僵硬的难以改变的,因为它是在代码中(DesignTime)固定死了的,根本不存在运行时(RunTime)改变的可能。换个角度说:我们应该多使用对象组合来保持系统的运行时扩展性,尽量少使继承,因为继承让程序变得僵硬!这句话听着是不是很熟悉啊?恩!这就是我们前面文章里提过多次的一个设计原则:Favor composition over inheritance.(优先使用对象组合,而非类继承),更多的就不需要再解释了吧?
设计原则:
1.多用组合,少用继承。
利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态地进行扩展。
2.类应设计的对扩展开放,对修改关闭。
那么回到装饰者模式,跟前面介绍过的模式一样,装饰者同样是一个很简单的模式,特别是画出类图之后,一切都很清楚明了。这里只有一个地方需要特殊强调一下:Decorator是装饰者模式里非常特殊的一个类,它既继承于Component【IS A关系】,又维护一个指向Component实例的引用【HAS A关系】,换个角度来说,Decorator跟Component之间,既有动态组合关系又有静态继承关系,WHY? 这里为什么要这么来设计?组合的好处是可以在运行时给对象增加职责Decorator【HAS A】 Component的目的是让ConcreteDecorator可以在运行时动态给ConcreteComponent增加职责,这一点相对来说还比较好理解;那么Decorator继承于Component的目的是什么?在这里,继承的目的只有一个,那就是可以统一装饰者和被装饰者的接口,换个角度来说,不管是ConcretComponent还是ConcreteDecorator,它们都是 Component,用户代码可以把它们统一看作Component来处理,这样带来的更深一层的好处就是,装饰者对象对被装饰者对象的功能职责扩展对用户代码来说是完全透明的,因为用户代码引用的都是Component,所以就不会因为被装饰者对象在被装饰后,引用它的用户代码发生错误,实际上不会有任何影响,因为装饰前后,用户代码引用的都是Component类型的对象,这真是太完美了!装饰者模式通过继承实现统一了装饰者和被装饰者的接口,通过组合获得了在运行时动态扩展被装饰者对象的能力。
我们再举个生活中的例子,俗话说“人在衣着马在鞍”,把这就话用装饰者模式的语境翻译一下,“人通过漂亮的衣服装饰后,男人变帅了,女人变漂亮了;”。对应上面的类图,这里人对应于ConcreteComponent,而漂亮衣服则对应于ConcreteDecorator;换个角度来说,人和漂亮衣服组合在一起【HAS A】,有了帅哥或美女,但是他们还是人【IS A】,还要做人该做的事情,但是可能会对异性更有吸引力了(扩展功能)!
现在我们已经认识了装饰者模式,知道了动态关系和静态关系是怎么回事,是时候该解决咖啡店的问题了,从装饰者模式的角度来考虑问题,咖啡和调味品的关系应该是:咖啡是被装饰对象而调味品是装饰者,咖啡和调味品可以任意组合,但是不管怎么组合,咖啡还是咖啡!原来这么简单啊!具体看下面的类图:
如图所示,Beverage还是所有饮料的基类,它对应于装饰者模式类图里的Component,是所有被装饰对象的基类;HouseBlend, DarkRoast, Espresso, Decaf是具体的饮料(咖啡)种类,对应于前面的ConcreteComponent,即是具体的被装饰对象;CondimentDecorator对应于前面的Decorator,是装饰者的抽象类;而Milk,Mocha,Soy,Whip则都是具体的调味品,对于前面的ConcreteDecorator,也就是具体的装饰者。下面我们通过具体的代码再进一步理解一下基于装饰者模式的记账系统的实现。
从饮料下手,将饮料作为一个抽象类:
1 package com.xingle_test.designpattern; 2 3 /** 4 * 饮料类 5 * @ClassName: Beverage 6 * 7 * @author Xingle 8 * @date 2014-7-29 上午9:59:47 9 */10 public abstract class Beverage {11 String description = "Unknow Beverage";12 13 public String getDescription(){14 return description;15 }16 17 public abstract double cost();18 19 }
调料抽象类,也就是装饰者类:
1 package com.xingle_test.designpattern; 2 3 /** 4 * 装饰者的抽象类 5 * @ClassName: CondimentDecorator 6 * TODO 7 * @author Xingle 8 * @date 2014-7-29 上午10:04:25 9 */10 public abstract class CondimentDecorator extends Beverage{11 12 public abstract String getDescription();13 }
实现具体的饮料(浓缩咖啡和综合咖啡):
1 package com.xingle_test.designpattern; 2 3 /** 4 * 具体组件——浓缩咖啡 5 * @ClassName: Espresso 6 * 7 * @author Xingle 8 * @date 2014-7-29 上午10:10:10 9 */10 public class Espresso extends Beverage{11 12 public Espresso(){13 description = "Espresso";14 }15 16 /**17 * 18 * @Description: 19 * @return20 * @author xingle21 * @data 2014-7-29 上午10:11:0522 */23 @Override24 public double cost() {25 return 1.99;26 }27 28 }
1 package com.xingle_test.designpattern; 2 3 /** 4 * 具体组件——综合咖啡 5 * @ClassName: HouseBlend 6 * 7 * @author Xingle 8 * @date 2014-7-29 上午10:12:13 9 */10 public class HouseBlend extends Beverage{11 12 public HouseBlend(){13 description = "HouseBlend";14 }15 16 /**17 * 18 * @Description: 19 * @return20 * @author xingle21 * @data 2014-7-29 上午10:12:3522 */23 @Override24 public double cost() {25 return .89;26 }27 28 }
实现具体装饰者类:
1 package com.xingle_test.designpattern; 2 3 /** 4 * 具体组件——浓缩咖啡 5 * @ClassName: Espresso 6 * 7 * @author Xingle 8 * @date 2014-7-29 上午10:10:10 9 */10 public class Espresso extends Beverage{11 12 public Espresso(int cupSize){13 description = "Espresso"; 14 size = cupSize;15 }16 17 @Override18 public double cost() {19 //return 1.99;20 switch (size) {21 case Beverage.VENTI:22 return 1.99;23 case Beverage.GRANDE:24 return 1.89;25 case Beverage.TALL:26 return 1.79;27 default:28 return 0.0;29 }30 }31 32 @Override33 public int getSize() {34 return size;35 }36 37 }
其他装饰者类的实现方式与上述摩卡类似。
1 package com.xingle_test.designpattern; 2 3 /** 4 * 调料装饰者——豆浆 5 * @ClassName: Soy 6 * TODO 7 * @author Xingle 8 * @date 2014-7-29 上午10:23:04 9 */10 public class Soy extends CondimentDecorator{11 12 Beverage beverage;13 14 public Soy(Beverage beverage){15 this.beverage = beverage;16 }17 18 19 @Override20 public String getDescription() {21 return beverage.getDescription()+",Soy";22 }23 24 25 @Override26 public double cost() {27 return .15+beverage.cost();28 }29 30 }
1 package com.xingle_test.designpattern; 2 3 /** 4 * 调料装饰者——牛奶 5 * @ClassName: Milk 6 * TODO 7 * @author Xingle 8 * @date 2014-7-29 上午10:20:33 9 */10 public class Milk extends CondimentDecorator{11 12 Beverage beverage;13 14 public Milk(Beverage beverage){15 this.beverage = beverage;16 }17 18 @Override19 public String getDescription() {20 return beverage.getDescription()+",Milk";21 }22 23 @Override24 public double cost() {25 return .1+beverage.cost();26 }27 28 }
1 package com.xingle_test.designpattern; 2 3 /** 4 * 调料装饰者——奶泡 5 * @ClassName: Whip 6 * TODO 7 * @author Xingle 8 * @date 2014-7-29 上午10:27:27 9 */10 public class Whip extends CondimentDecorator{11 12 Beverage beverage;13 14 public Whip(Beverage beverage){15 this.beverage = beverage;16 }17 18 @Override19 public String getDescription() {20 return beverage.getDescription()+",Whip";21 }22 23 @Override24 public double cost() {25 return .10+beverage.cost();26 }27 28 }
来一杯咖啡测试代码:
1 package com.xingle_test.designpattern; 2 3 /** 4 * 供应咖啡 5 * @ClassName: StartbuzzCoffee 6 * 7 * @author Xingle 8 * @date 2014-7-29 上午10:16:56 9 */10 public class StartbuzzCoffee {11 12 public static void main(String[] args){13 //来一杯Espresso咖啡,不需要加调料,打印价格14 Beverage beverage1 = new Espresso();15 System.out.println(beverage1.getDescription()+", $:"+beverage1.cost());16 17 //来一杯 调料味Soy和Mocha 的 HouseBlend咖啡18 Beverage beverage2 = new HouseBlend();19 beverage2 = new Soy(beverage2);20 beverage2 = new Mocha(beverage2);21 System.out.println(beverage2.getDescription()+",$:"+beverage2.cost());22 }23 }
执行结果:
Espresso, $:1.99
HouseBlend,Soy,Mocha,$:1.24
第3部分 需求扩展
那如果我们这时产生了新的需求,要求在菜单上加上咖啡的容量的大小,供顾客选择大杯,小杯,中杯,那该怎么办?要注意,大杯的饮料比较贵,同时它加的调料也要比较多,所以调料的价格也不一样。
这时我们应该在Beverage中定义size和getSize()的函数,并且在四种饮料中要根据size的大小,cost()函数要返回不同的价格。
在调料中,我们也需要获取被装饰者的size,然后cost函数加上对应调料的价格。
重新调整代码如下:
饮料类:
1 package com.xingle_test.designpattern; 2 3 /** 4 * 饮料类 5 * @ClassName: Beverage 6 * 7 * @author Xingle 8 * @date 2014-7-29 上午9:59:47 9 */10 public abstract class Beverage {11 String description = "Unknow Beverage";12 13 public final static int TALL = 0; //小杯 14 public final static int GRANDE = 1; //中杯 15 public final static int VENTI = 2; //大杯 16 17 protected int size = TALL; //咖啡的大小(大/中/小杯) 18 19 public int getSize(){20 return size;21 }22 23 public void setSize(int size){24 this.size = size;25 }26 27 /**28 * 返回咖啡的种类和杯子大小 29 * @return30 * @author xingle31 * @data 2014-7-29 下午12:18:5532 */33 public String getDescription() {34 switch (size) {35 case Beverage.VENTI:36 return "venti " + description;37 case Beverage.GRANDE:38 return "grande " + description;39 case Beverage.TALL:40 return "tall " + description;41 default:42 return description;43 }44 }45 46 public abstract double cost();47 48 }
具体组件,这里只列出浓缩咖啡和综合咖啡,另两种类似:
1 package com.xingle_test.designpattern; 2 3 /** 4 * 具体组件——浓缩咖啡 5 * @ClassName: Espresso 6 * 7 * @author Xingle 8 * @date 2014-7-29 上午10:10:10 9 */10 public class Espresso extends Beverage{11 12 public Espresso(int cupSize){13 description = "Espresso"; 14 size = cupSize;15 }16 17 /**18 * 19 * @Description: 20 * @return21 * @author xingle22 * @data 2014-7-29 上午10:11:0523 */24 @Override25 public double cost() {26 //return 1.99;27 switch (size) {28 case Beverage.VENTI:29 return 1.99;30 case Beverage.GRANDE:31 return 1.89;32 case Beverage.TALL:33 return 1.79;34 default:35 return 0.0;36 }37 }38 39 /**40 * 41 * @Description: TODO42 * @return43 * @author xingle44 * @data 2014-7-29 上午11:53:3745 */46 @Override47 public int getSize() {48 return size;49 }50 51 }
1 package com.xingle_test.designpattern; 2 3 /** 4 * 具体组件——综合咖啡 5 * @ClassName: HouseBlend 6 * 7 * @author Xingle 8 * @date 2014-7-29 上午10:12:13 9 */10 public class HouseBlend extends Beverage{11 12 public HouseBlend(int cupSize){13 description = "HouseBlend";14 size = cupSize;15 }16 17 18 @Override19 public double cost() {20 // return .89;21 switch (size) {22 case Beverage.VENTI:23 return 0.99;24 case Beverage.GRANDE:25 return 0.89;26 case Beverage.TALL:27 return 0.79;28 default:29 return 0.0;30 }31 }32 33 34 @Override35 public int getSize() {36 return size;37 }38 39 }
调料装饰者,这里列出牛奶和摩卡,另外两种豆浆喝奶泡也类似:
1 package com.xingle_test.designpattern; 2 3 /** 4 * 调料装饰者——牛奶 5 * @ClassName: Milk 6 * TODO 7 * @author Xingle 8 * @date 2014-7-29 上午10:20:33 9 */10 public class Milk extends CondimentDecorator{11 12 Beverage beverage;13 14 public Milk(Beverage beverage){15 this.beverage = beverage;16 }17 18 /**19 * 20 * @Description: 21 * @return22 * @author xingle23 * @data 2014-7-29 上午10:20:5824 */25 @Override26 public String getDescription() {27 return beverage.getDescription()+",Milk";28 }29 30 @Override31 public double cost() {32 //return .1+beverage.cost();33 double cost = beverage.cost();34 if(getSize()==Beverage.TALL){35 cost+=.3;36 }37 else if(getSize()==Beverage.GRANDE){38 cost+=.35;39 }40 else if(getSize()==Beverage.VENTI){41 cost+=.4;42 }43 return cost;44 }45 46 @Override47 public int getSize() {48 return beverage.getSize();49 }50 51 }
1 package com.xingle_test.designpattern; 2 3 /** 4 * 调料装饰者——摩卡 5 * @ClassName: Mocha 6 * 7 * @author Xingle 8 * @date 2014-7-29 上午10:13:48 9 */10 public class Mocha extends CondimentDecorator{11 12 Beverage beverage;13 14 public Mocha(Beverage beverage){15 this.beverage = beverage;16 }17 18 /**19 * 20 * @Description: 21 * @return22 * @author xingle23 * @data 2014-7-29 上午10:14:0324 */25 @Override26 public String getDescription() {27 return beverage.getDescription()+",Mocha";28 }29 30 @Override31 public double cost() {32 //return 0.20+beverage.cost(); 33 double cost = beverage.cost();34 if(getSize()==Beverage.TALL){35 cost+=.3;36 }37 else if(getSize()==Beverage.GRANDE){38 cost+=.25;39 }40 else if(getSize()==Beverage.VENTI){41 cost+=.2;42 }43 return cost;44 }45 46 @Override47 public int getSize() {48 return beverage.getSize();49 }50 51 }
供应咖啡测试代码:
1 package com.xingle_test.designpattern; 2 3 /** 4 * 供应咖啡 5 * @ClassName: StartbuzzCoffee 6 * 7 * @author Xingle 8 * @date 2014-7-29 上午10:16:56 9 */10 public class StartbuzzCoffee {11 12 public static void main(String[] args){13 //来一杯Espresso咖啡,不需要加调料,打印价格14 Beverage beverage1 = new Espresso(Beverage.TALL);15 System.out.println(beverage1.getDescription()+", $:"+beverage1.cost());16 17 //来一杯 调料味Milk和Mocha 的 HouseBlend咖啡18 Beverage beverage2 = new HouseBlend(Beverage.GRANDE);19 beverage2 = new Milk(beverage2);20 beverage2 = new Mocha(beverage2);21 System.out.println(beverage2.getDescription()+",$:"+beverage2.cost());22 }23 }
执行结果:
tall Espresso, $:1.79
grande HouseBlend,Milk,Mocha,$:1.49
第4部分 装饰模式在Java I/O库中的应用
java.io包内的类太多了,简直是……“排山倒海”。你第一次(还有第二次和第三次)看到这些API发出“哇”的惊叹时,放心,你不是唯一受到惊吓的人。现在,你已经知道装饰者模式,这些I/O的相关类对你来说应该更有意义了,因为其中许多类都是装饰者。下面是一个典型的对象集合,用装饰者来将功能结合起来,以读取文件数据:
BufferedInputStream
及 LineNumberInputStream
都扩展自 FilterInputStream
,而FilterInputStream
是一个抽象的装饰类。
你会发现“输出”流的设计方式也是一样的。你可能还会发现Reader/Writer流(作为基于字符数据的输入输出)和输入流/输出流的类相当类似(虽然有一些小差异和不一致之处,但是相当雷同,所以你应该可以了解这些类)。
但是JavaAI/O也引出装饰者模式的一个“缺点”:利用装饰者模式,常常造成设计中有大量的小类,数量实在太多,可能会造成使用此API程序员的困扰。但是,现在你已经了解了装饰者的工作原理,以后当使用别人的大量装饰的API时,就可以很容易地辨别出他们的装饰者类是如何组织的,以方便用包装方式取得想要的行为。
编写自己的Java I/O装饰者,将大写字符转换为小写字符:
1 package com.xingle_test.designpattern; 2 3 import java.io.BufferedInputStream; 4 import java.io.FileInputStream; 5 import java.io.FilterInputStream; 6 import java.io.IOException; 7 import java.io.InputStream; 8 9 /**10 * 编写一个装饰者把所有的输入流内的大写字符转化成小写字符:11 * @ClassName: LowerCaseInputStream12 * 13 * @author Xingle14 * @date 2014-7-29 下午3:19:0415 */16 public class LowerCaseInputStream extends FilterInputStream{17 18 /**19 * @param in20 */21 protected LowerCaseInputStream(InputStream in) {22 super(in);23 // TODO Auto-generated constructor stub24 }25 26 /**27 * 针对字节28 */29 public int read() throws IOException{30 int c = super.read();31 return (c == -1 ? c : Character.toLowerCase((char) c));32 }33 34 /**35 * 针对字节数组36 */37 public int read(byte[] b,int offset,int len)throws IOException{38 int result = super.read(b, offset, len);39 for(int i=0;i= 0)52 {53 System.out.print((char) c);54 } 55 in.close();56 }57 catch (IOException e)58 {59 e.printStackTrace();60 }61 }62 63 64 }
D:\test\1.txt 原本的文件内容为:Test LowerCaseInputStream HELLO
执行结果:
test lowercaseinputstream hello