关于装饰者模式的定义,我就直接引用Head First了:装饰者模式动态的将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案 。 其实装饰者模式的重点在于给对象动态的附加职责,通过对象组合的方式,运行时装饰对象,在不改变任何底层代码的情况下,给现有对象赋予新的职责。
现在我们要为卖煎饼的大妈设计一套系统,让大妈能更好的算账收钱。大妈主要经营煎饼(7元),并可以往其中添加配料烤肠(1元),鸡蛋(1元),如果生意红火,可能会考虑扩大经营其他小吃或添加配料种类。现需要设计出一套系统,以便快速计算出每位顾客所购小吃的价格。
这是一个食品类接口。同样,要符合针对接口编程,不针对实现编程的OO原则。
public interface ISnack {
// 食物的描述
String getDescription();
// 食物的价格
double cost();
}
这是一个配料类接口。配料类接口IDecoratorSnack继承自ISnack接口,因为需要两接口的实现类(装饰者与被装饰对象)具有相同的超类型,只有这样IDecoratorSnack接口才能取代ISnack接口。
/**
* 这是配料类的接口。
*
* 装饰者与被装饰者需要具有相同的接口,
* 这样用户使用装饰者对象才会和使用原始对象一样。
*/
public interface IDecoratorSnack extends ISnack {
// 可根据需要扩展属性,如配料大份,小份等
}
这是具体的食品-煎饼类。
public class PancakeSnack implements ISnack {
@Override
public String getDescription() {
return "煎饼";
}
@Override
public double cost() {
return Price.PancakeSnack.price;
}
}
食品类(被装饰者)既可以单独使用,也可以被配料类(装饰者)包着使用,因为装饰者和被装饰者对象具有相同的超类型,所以在任何需要原始对象(被包装的)的场合,都可以用装饰过的对象替代它。
如果,我想要一份加鸡蛋和烤肠的煎饼价格是多少?单价类见下:
public enum Price {
//鸡蛋、烤肠、煎饼
Egg(1.0), Sausage(1.0), PancakeSnack(7.0);
double price;
private Price(double price) {
this.price = price;
}
}
这是我们的具体配料鸡蛋类。
public class Egg implements IDecoratorSnack {
// 持有被修饰的对象
ISnack iSnack;
public Egg(ISnack iSnack) {
this.iSnack = iSnack;
}
@Override
public String getDescription() {
return iSnack.getDescription() + ",加鸡蛋";
}
@Override
public double cost() {
// 被修饰对象的价格 + 鸡蛋价格
return iSnack.cost() + Price.Egg.price;
}
}
这是具体的烤肠配料类。
public class Sausage implements IDecoratorSnack{
//持有被修饰对象的引用
ISnack iSnack;
public Sausage(ISnack iSnack) {
this.iSnack = iSnack;
}
@Override
public String getDescription() {
return iSnack.getDescription() + ",加香肠";
}
@Override
public double cost() {
// 被修饰对象的价格 + 烤肠价格
return iSnack.cost() + Price.Sausage.price;
}
}
每个装饰者都持有一个被装饰者的对象,这个被装饰者对象不一定是原始对象,也可能是被包装了多层的对象。通过这种组合,加入了新的行为。符合对扩展开放,对修改关闭的OO原则。
我们来收钱吧。我们能够发现,我们能够使用被装饰的对象就像使用原始对象一样,这归功于装饰者与被装饰者具备同样的接口。同样一个原始对象是能够被包装多层的,而在使用者的眼里,它只是一个被赋予了新功能的原始对象而已。
public class DecoratorPatternTest {
public static void main(String[] ags) {
// 煎饼 + 鸡蛋 + 烤肠
ISnack snackOneISnack = new PancakeSnack();
snackOneISnack = new Egg(snackOneISnack);
snackOneISnack = new Sausage(snackOneISnack);
// 这样就能够打印出,我们的(煎饼 + 鸡蛋 + 烤肠)价格和描述了
System.out.println(snackOneISnack.getDescription() + "价格:" + snackOneISnack.cost() + "元");
}
}
而且扩展起来方便,我们随时加入不同种类小吃和配料类。如我们现在要加一种烤冷面类。
public class ColdRoastSnake implements ISnack{
@Override
public String getDescription() {
return "烤冷面";
}
@Override
public double cost() {
return Price.ColdRoastSnake.price;
}
}
我们再添加一个鸡里脊肉类。
public class Chicken implements IDecoratorSnack{
ISnack iSnack;
public Chicken(ISnack iSnack) {
this.iSnack = iSnack;
}
@Override
public String getDescription() {
return iSnack.getDescription() + ",加里脊肉";
}
@Override
public double cost() {
return iSnack.cost() + Price.Chicken.price;
}
}
好了,如果用户购买烤冷面+鸡蛋+里脊肉,价格该多少呢?
public class DecoratorPatternTest {
public static void main(String[] ags) {
// 烤冷面 + 鸡蛋 + 里脊肉
ISnack snackOneISnack = new ColdRoastSnake();
snackOneISnack = new Egg(snackOneISnack);
snackOneISnack = new Chicken(snackOneISnack);
// 这样就能够打印出,我们的(烤冷面 + 鸡蛋 + 里脊肉)价格和描述了
System.out.println(snackOneISnack.getDescription() + "价格:" + snackOneISnack.cost() + "元");
}
}
需要注意的是,通过利用装饰者模式,会造成设计中产生大量的小类,如果过度使用,会使程序变得很复杂。另外可能还会出现类型问题,如果把代码写成依赖于具体的被装饰者类型,不针对抽象接口进行编程,那么就会出现问题。