装饰对象
java
咱们即将讨论典型的继承滥用问题。并学到如何使用对象组合的方式,作到在运行时装饰类。api
用一个简单的需求来描述问题,星巴兹(Starbuzz)须要准备订单系统,这是他们的第一个尝试,类设计是这样的:框架
Beverage(饮料)是一个抽象类,店内所提供的饮料都必须继承自此类,这个类有一个description(描述)的实例变量,能够由每一个子类设置,用来描述饮料,例如:超优深培咖啡豆(Dark Roast)等。cost()方法是抽象的,子类必须定义本身的实现。每一个子类都实现cost()来返回饮料的价钱。购买咖啡时,也能够要求在其中加入各类调料,例如:蒸奶、豆浆、摩卡或覆盖奶泡。咖啡店会根据加入的调料收取不一样的费用。ide
代码与客户端调用以下:函数
package cn.net.bysoft.decorator; // 饮料的基类。 public abstract class Beverage { // 计算价格的方法。 public abstract double cost(); // 饮料描述的getter and setter。 public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } // 饮料的描述。 private String description = ""; }
package cn.net.bysoft.decorator; // 深焙咖啡对象。 public class DarkRoast extends Beverage { @Override public double cost() { // 牛奶 + 糖 + 深焙咖啡豆 = 1$ return 1.00D; } }
package cn.net.bysoft.decorator; // 浓缩咖啡对象。 public class Espresso extends Beverage { @Override public double cost() { // 浓缩咖啡价格是1.15$。 return 1.15D; } }
package cn.net.bysoft.decorator; public class Client { public static void main(String[] args) { // 来一杯浓缩咖啡。 Beverage espresson = new Espresso(); System.out.println("浓缩咖啡的价格是:" + espresson.cost() + "$"); // 来一杯深焙咖啡。 Beverage darkRoast = new DarkRoast(); System.out.println("深焙咖啡的价格是:" + darkRoast.cost() + "$"); /** * output: * 浓缩咖啡的价格是:1.15 * 深焙咖啡的价格是:1.0 * */ } }
很明显,咖啡店为本身制造了一个维护的噩梦,看类图,假如咖啡店有82种咖啡,若是牛奶的价钱上扬,须要改动全部与牛奶有关的咖啡的价格,若是咖啡豆价格上调的话……若是新增一种全部饮料都试用的焦糖风味,全部类都要改变……测试
这时候,有些人已经想到了解决办法,利用实例变量与继承,就能够追踪这些调料。this
好吧,就来试试看。先从Beverage基类下手,加上实例变量表明是否加上调料,如今,Beverage类中的cost()再也不是一个抽象方法,咱们提供了cost()的实现,让他计算要加入各类饮料的调料价钱。子类仍覆盖cost(),可是会调用超类的cost(),计算出基本饮料加上调料的价钱。看一下代码如何实现:spa
package cn.net.bysoft.decorator; // 饮料的基类。 public abstract class Beverage { // 基类计算全部调料的价钱。 public double cost() { double price = 0.0D; if (hasMilk()) { price += 0.2D; } if (hasSoy()) { price += 0.2D; } if (hasMocha()) { price += 0.3D; } if (hasWhip()) { price += 0.3D; } return price; } // 饮料描述的getter and setter。 public String getDescription() { return description; } public boolean hasMilk() { return milk; } public void setMilk(boolean milk) { this.milk = milk; } // getter and setter... // 饮料的描述。 private String description = ""; private boolean milk = false; private boolean soy = false; private boolean mocha = false; private boolean whip = false; }
package cn.net.bysoft.decorator; // 深焙咖啡对象。 public class DarkRoast extends Beverage { @Override public double cost() { // 深焙咖啡使用牛奶。 double price = 0.0D; super.setMilk(true); // 计算了牛奶的价钱,在加上深焙咖啡的原料。 // 0.2 + 1.05 = 1.25; price = super.cost() + 1.05; return price; } }
package cn.net.bysoft.decorator; public class Client { public static void main(String[] args) { // 来一杯深焙咖啡。 Beverage darkRoast = new DarkRoast(); System.out.println("深焙咖啡的价格是:" + darkRoast.cost() + "$"); /** * output: * 深焙咖啡的价格是:1.25$ * */ } }
这种作法,很是容易的就解决的前一种设计的问题,咖啡店很满意,开始使用修改后的订单系统。.net
过了一阵子,发现这种设计并不能知足平常应用,好比:设计
调料价格的改变仍是要修改现有代码;
一旦出现新的调料,就须要加上新的hasXXX方法,并改变超类中的cost()方法;
之后可能会开发出新饮料,对于这些饮料(例如冰茶等),某些调料可能并不合适,可是在这个设计方式汇总,Tea(茶)子类仍然继承哪些不合适的方法,好比hasWhip(加奶泡);
万一顾客想要双倍的摩卡或者双倍的糖怎么办;
此刻,就面临了最重要的设计原则之一:
设计原则
类应该对扩展开放,对修改关闭。
咱们的目标是容许类容易扩展,在不修改现有代码的状况下,就能够配置新的行为。如能实现这样的目标,有什么好处呢?这样的设计具备弹性能够应对改变,能够接受新的功能来应对需求变动。
认识装饰者模式
好了,咱们已经了解到利用继承没法彻底解决问题,在咖啡馆遇到的问题有:类数量爆炸、设计死板,以及基类加入的新功能并不适用于全部的子类。
因此,在这里要采用不同的作法:咱们要以饮料为主题,而后在运行时以调料来“装饰”(decorate)饮料。比方说,若是顾客想要摩卡和奶泡的深培咖啡,那么,要作的是:
拿一个深培咖啡(DarkRoast)对象;
以摩卡(Mocha)对象装饰它;
以奶泡(WHip)对象装饰它;
调用cost()方法,并依赖委托(delegate)将调料的价钱加上去;
这样就完工了一个深培咖啡对象,可是如何“装饰”一个对象,而“依赖委托”又要如何与此搭配使用呢?
装饰者(调料)和被装饰者(饮料)都有相同的超类;
能够用一个或多个装饰者(调料)包装一个被装饰者(饮料);
既然装饰者和被装饰者都有相同的超类,那么在任何须要原始对象,也就是被装饰者(饮料)的场合,均可以使用装饰过的对象(饮料)代替它;
装饰者能够在所委托的被装饰者的行为以前或以后,加上本身的行为,以达到特定的目的;
对象能够在任什么时候候被装饰,因此能够在运行时动态地、不限量地用你喜欢的装饰者来装饰对象;
如今,就来卡看装饰者模式的定义,并写一些代码,了解它究竟是怎么工做的吧!先来看看装饰者模式的类图:
Component(组件)类:每一个组件均可以单独使用,或者被装饰者包装起来使用。
ConcreteComponent(被装饰组件)类:咱们将要动态地加上新行为的对象,它拓展自Component类。
Decorator(装饰)类:每个装饰者都“有一个”(包装一个)组件,也就是说,装饰者有一个实例变量以保存某个Component的引用。
ConcreteDecoratorXX类:有一个实例变量能够记录所装饰的事物,还能够加上新的方法。
如今,让咖啡店的订单系统也符合此框架:
左侧4个类HouseBlend、DarkRoast、Decaf、Espresso是具体的组件类,而右下角的4个类Milk、Mocha、Soy、Whip是具体的装饰者类,这么一看并无感受到装饰者有多厉害,让咱们用代码来实现之后在看看效果,如今的需求为:“来一杯双倍摩卡豆浆奶泡拿铁咖啡”,使用订单系统获得正确的价钱:
package cn.net.bysoft.decorator; // 饮料的基类。 public abstract class Beverage { // 饮料的说明。 String description = "Unknow Beverage"; public String getDescription() { return description; } public abstract double cost(); }
第一个与咱们见面的类是饮料的基类,有两个方法,分别是getDescription()和cost(),用来返回饮料说明和价格。
package cn.net.bysoft.decorator; // 调料对象。 public abstract class CondimentDecorator extends Beverage { // 饮料描述。 public abstract String getDescription(); }
第二个出现的类是调料的抽象类,首先,必须让该类能取代Beverage,因此继承自Beverage类。
它的方法getDescription()必须让全部的调料类都实现。
如今,基类已经有了,开始实现一些饮料吧!先从浓缩咖啡开始。
package cn.net.bysoft.decorator; // 浓缩咖啡对象。 public class Espresso extends Beverage { public Espresso() { super.description = "Espresso Coffee"; } @Override public double cost() { return 1.99D; } }
首先,让Espresso拓展自Beverage类,由于浓缩咖啡也是一种饮料。而后设置浓缩咖啡的说明属性description,最后使用cost()方法返回价格。
再实现一个黑咖啡(HouseBlend)和深焙咖啡(DarkRoast),代码同上,其他的饮料都同样。
package cn.net.bysoft.decorator; // 黑咖啡对象。 public class HouseBlend extends Beverage { public HouseBlend() { super.description = "House Blend Coffee"; } @Override public double cost() { return .89; } }
package cn.net.bysoft.decorator; // 深焙咖啡对象。 public class DarkRoast extends Beverage { public DarkRoast() { super.description = "DarkRoast Coffee"; } @Override public double cost() { return .89; } }
写好具体的饮料对象后,就能够着手调料类的编写了:
package cn.net.bysoft.decorator; public class Mocha extends CondimentDecorator { // 须要装饰的类。 Beverage beverage; public Mocha(Beverage beverage) { this.beverage = beverage; } @Override public String getDescription() { return beverage.getDescription() + ", Mocha"; } @Override public double cost() { return .20 + beverage.cost(); } }
摩卡(Mocha)是一个装饰者,拓展自CondimentDecorator类,也就是说,摩卡也是一个Beverage类。
摩卡对象中有一个Beverage对象用于存放要封装的具体饮料类,目前有Espresso和HouseBlend两个饮料。
在构造函数中把具体饮料看成参数传递到Mocha中。
在描述时,不仅是调用传递进来的饮料的描述,还能够加入本身想要加入的描述(例如“DarkRoast, Mocha”)。
最后cost()方法,首先调用传递进来的饮料的cost()方法,得到价格,在加上Mocha本身的钱,获得最终结果。
在最后测试以前,实现奶泡条件,完成需求:
package cn.net.bysoft.decorator; // 奶泡调料 public class Whip extends CondimentDecorator { // 须要装饰的类。 Beverage beverage; public Whip(Beverage beverage) { this.beverage = beverage; } @Override public String getDescription() { return beverage.getDescription() + ", Whip"; } @Override public double cost() { return .20 + beverage.cost(); } }
下面就是使用订单的一些测试代码:
package cn.net.bysoft.decorator; public class Client { public static void main(String[] args) { // 订一杯浓缩咖啡,不须要调料,打印价格。 Beverage beverage = new Espresso(); System.out.println(beverage.getDescription() + " $" + beverage.cost()); System.out.println(); // 订一杯深焙咖啡,加入双倍的Mocha,在加入奶泡。 // 由于调料与饮料都是扩展自Beverage,因此可使用下面的等式。 Beverage beverage2 = new DarkRoast(); beverage2 = new Mocha(beverage2); beverage2 = new Mocha(beverage2); beverage2 = new Whip(beverage2); System.out .println(beverage2.getDescription() + " $" + beverage2.cost()); System.out.println(); /** * output: * Espresso Coffee $1.99 * * DarkRoast Coffee, Mocha, Mocha, Whip $1.49 * */ } }
再来拓展一下需求,如今咖啡店决定开始在菜单上为全部饮料加入容量,提供小杯(Tall)、中杯(Grande)、大杯(Venti)饮料,每次加入摩卡和奶泡不是按照固定的0.20美金收费了,而是按照,小中大杯的咖啡加摩卡和奶泡时,判断size,而后分别加收0.一、0.1五、0.2美金。
如何改变装饰者类对应这样的需求呢?
首先,在饮料基类中加入SIZE属性:
public enum BeverageSize { TALL, GRANDE, VENTI }
public abstract class Beverage { BeverageSize size = BeverageSize.TALL; public void setSize(BeverageSize size) { this.size = size; } public String getDescription() { return description; } ... }
而后,修改摩卡和奶泡的cost方法,修改以前将摩卡和奶泡类中的Beverage提取到基类CondimentDecorator中:
// 调料对象。 public abstract class CondimentDecorator extends Beverage { Beverage beverage; public BeverageSize getSize() { return beverage.getSize(); } ... }
package cn.net.bysoft.decorator; public class Mocha extends CondimentDecorator { public Mocha(Beverage beverage) { super.beverage = beverage; } @Override public double cost() { double cost = beverage.cost(); if (getSize() == BeverageSize.TALL) { cost += .10; } else if (getSize() == BeverageSize.GRANDE) { cost += .15; } else if (getSize() == BeverageSize.VENTI) { cost += .20; } return cost; } ... }
奶泡的类,与摩卡类相同,最后进行测试:
public static void main(String[] args) { // 订一杯深焙咖啡,加入双倍的Mocha,在加入奶泡。 Beverage beverage2 = new DarkRoast(); // 大杯饮料,每种调料0.15美金。 beverage2.setSize(BeverageSize.GRANDE); beverage2 = new Mocha(beverage2); beverage2 = new Mocha(beverage2); beverage2 = new Whip(beverage2); // 0.89+0.15+0.15+0.15 = 1.34 System.out.println(beverage2.getDescription() + " $" + String.format("%.2f", beverage2.cost())); System.out.println(); /** * output: DarkRoast Coffee, Mocha, Mocha, Whip $1.34 * */ }
以上就是装饰者模式的所有内容。
另外,java.io包内的类大量的使用了装饰者模式,好比:
FileInputStrame被BufferedInputStream装饰。
而BufferedInputStream又被LineNumberInputStream装饰。
具体的细节能够查看java的api和源码。