一、什么情况下考虑使用“装饰者模式”?
当我们需要将责任动态的附加到对象上的时候;也就是说,我们开发设计的对象中有某一部分的功能现在还不能确定,以后需要动态的添加或者去掉。或者是在使用继承比较困难的时候,可以采用组合的实现方式(继承与复用)。
结合下面一个实际可能应用的场景来进行分析:
此为某一咖啡店的简单的菜单系统,其中所有的咖啡必须继承自Beverage类,咖啡中需要根据顾客的要求,加入巧克力、豆浆等调料,然后依据加入的调料的不同收取相应的费用。
eg:菜单上的StarbuzzCoffee实际为:HouseBlend + 2 * Mocha + Soy 调制而成,该如何实现呢?
如果采用装饰着模式的话可以让Mocha来装饰StarBuzzCoffee,然后Soy来装饰Mocha,在计算价格时只需要由外到内依次调用相应的价格即可。
装饰者模式的一些概念:
①装饰者和被装饰者有相同的超类型
②你可以用一个或者多个装饰者包装一个对象。
③由于装饰者和被装饰对象具有相同的对象,所有在任何需要原始对象的时候,可以用装饰后的对象来替换它
④装饰者可以在所委托被装饰者的行为之前或者之后,加上自己的行为,以实现特定的功能。
⑤对象可以在任何时候被装饰,你可以在运行时选择你喜欢的装饰者来装饰对象。
二、如何实现装饰者模式?
①查看已有具体组件:如HouseBlend和Espresso,代码如下
public abstract class Beverage { protected String description = "Unknown Beverage"; public String getDescription() { return description; } public abstract double cost(); }
public class HouseBlend extends Beverage { public HouseBlend(){ description="HouseBlend"; } @Override public double cost() { return .89; } }
②定义抽象的装饰者(也可以不用),同时定义具体的装饰者,装饰者的目的就是增加行为到被包装的对象上,关键这在于“装饰”,所以我们采用组合让Beverage作为具体装饰者的field,同时装饰者也必须继承自Beverage,这样才可以对其进行进一步的装饰。
public class Mocha extends CondimentDecorator { protected 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(); } }
③装饰出需要的咖啡
public class StarbuzzCoffee { public static void main(String[] args) { Beverage beverage = new HouseBlend(); beverage = new Soy(beverage); beverage = new Mocha(beverage); beverage = new Mocha(beverage); System.out.println(beverage.getDescription() + " $" + beverage.cost()); } }
java中典型使用装饰者模式的地方就是Java IO,感兴趣的话可以深入学一下。
ps:在编写上面的代码时发生了一个小插曲:发现最后输入的结果为:
HouseBlend ,Soy, Mocha, Mocha $1.5899999999999999
这一定不是我们想看到的结果,
这是由于float和double视为科学计算和工程计算而设计的,它们执行的为较为精确的快速近似计算,所以不能提供完全精确的结果。
所以为实现精确的计算可以使用BigDecimal
我们将其中的cost()修改如下:
@Override public double cost() { BigDecimal cost = new BigDecimal("0.20"); return (cost.add(new BigDecimal(Double.toString(beverage.cost()))).doubleValue()); }
便可以得出想要的结果了。
如果不使用BigDecimal可以将小数先转换为int或者long类型,最后在除以相应的值即可。
另外注意到了没,BigDecimal新建时使用的构造方法为为String,而没有使用double,这是为什么??
Notes:
- The results of this constructor can be somewhat unpredictable. One might assume that writing
new BigDecimal(0.1)
in Java creates aBigDecimal
which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as adouble
(or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding. - The
String
constructor, on the other hand, is perfectly predictable: writingnew BigDecimal("0.1")
creates aBigDecimal
which is exactly equal to 0.1, as one would expect. Therefore, it is generally recommended that the String constructor be used in preference to this one. - When a
double
must be used as a source for aBigDecimal
, note that this constructor provides an exact conversion; it does not give the same result as converting thedouble
to aString
using theDouble.toString(double)
method and then using theBigDecimal(String)
constructor. To get that result, use thestatic
valueOf(double)
method.