《Head

装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

例子:

下面我们以星巴兹(Starbuzz)的订单系统为例加以说明。

需求分析:

  1. 星巴兹的饮料(Beverage)种类繁多,主要有 HouseBlend、DarkRoast、Decaf、Espresso。
  2. 星巴兹的调料很多,主要有 Steamed Milk、Soy、Mocha、Whip。
  3. 星巴兹的饮料价格是根据饮料的基础价和所加入的调料的价格相加得到。

原先的设计:

错误设计

简直是“类爆炸”

另一种错误设计

新方案必须避免“类爆炸”。此时我们想到了实例变量和继承。

先从 Beverage 基类下手,加上实例变量代表是否加上调料(Steamed Milk、Soy、Mocha、Whip等),Beverage 基类的 cost() 计算调料的价钱,而各种具体的饮料(HouseBlend、DarkRoast、Decaf、Espresso等)的 cost() 将把基础饮料的价钱和调料的价钱相加得到饮料的价钱。由此可以设计出第二种类图。

对这个类图设计的评价:如果需求不再变化,那么这个类图设计没有错;但是需求发生了变化,这个设计就会难以招架。经过进一步的分析,我们发现部分需求被我们遗漏了。

新增加的需求:

  1. 调料的价格可能发生变化。
  2. 调料的种类可能发生变化。
  3. 饮料的种类可能增加,不只 HouseBlend、DarkRoast、Decaf、Espresso 四种。
  4. 顾客可能在一种饮料里加双份的同种饮料。

开放-关闭原则

类应该对扩展开放,对修改关闭。

每个地方都采用开放-关闭原则,是一种浪费,还会导致代码变得复杂且难以理解。

装饰者模式

类图

特点

  1. 装饰者和被装饰对象有相同的超类型。
  2. 你可以用一个或多个装饰者包装一个对象。
  3. 既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象代替它。
  4. 装饰者可以在所委托被装饰者的行为之前与 / 或之后,加上自己的行为,以达到特定的目的。
  5. 对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象。

咖啡例子

这里为什么没把 Beverage 类设计成一个接口,而是设计成了抽象类?

因为一开始 Beverage 就是一个抽象类,通常装饰者模式是采用抽象类,但是在 Java 中可以使用接口

代码

Beverage 类,不需要需改原始设计。

public abstract class Beverage {
    String description = "Unknown Beverage";

    public String getDescription() {
        return description;
    }
    public abstract double cost();
}

CondimentDecorator 装饰者类

public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription(); // 必须重新实现该方法,可以打印出是被哪个装饰者装饰了。
}

饮料的代码

public class Espresso extends Beverage {
    public Espresso(){
        description = "Espresso";
    }

    @Override
    public double cost() {
        return 1.99;
    }
}

调料代码

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();
    }
}

测试代码,供应咖啡

public class StarBuzzCoffee {
    public static void main(String[] args) {
        Beverage beverage = new Espresso(); //普通咖啡
        System.out.println(beverage.getDescription() + " $" + beverage.cost());

        Beverage beverage2 = new Mocha(beverage); // 加了两份 Mocha 的咖啡
        beverage2 = new Mocha(beverage2);
        System.out.println(beverage2.getDescription() + " $" + beverage2.cost());
    }
}/* output:
Espresso $1.99
Espresso, Mocha, Mocha $2.39
*/

需求扩展

那如果我们这时产生了新的需求,要求在菜单上加上咖啡的容量的大小,供顾客选择大杯,小杯,中杯,那该怎么办?要注意,大杯的饮料比较贵,同时它加的调料也要比较多,所以调料的价格也不一样。

这时我们应该在 Beverage 中定义 size 和g etSize() 的函数,并且在四种饮料中要根据 size 的大小,cost() 函数要返回不同的价格。

在调料中,我们也需要获取被装饰者的 size,然后 cost 函数加上对应调料的价格。

Beverage 类

public abstract class Beverage {
    String description = "Unknown Beverage";
    public final static int TALL = 0; //小杯
    public final static int GRANDE = 1; //中杯
    public final static int VENTI = 2; //大杯
    protected int size = TALL; //咖啡的大小(大/中/小杯)

    public int getSize() {
        return size;
    }
    public void setSize(int size){
        this.size = size;
    }

    /**
     * 返回咖啡的种类和杯子大小
     */
    public String getDescription() {
        switch (size) {
            case Beverage.VENTI:
                return "venti " + description;
            case Beverage.GRANDE:
                return "grande " + description;
            case Beverage.TALL:
                return "tall " + description;
            default:
                return description;
        }
    }
    public abstract double cost();
}

装饰类

public class Soy extends CondimentDecorator {
    Beverage beverage;
    public Soy(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public int getSize() {
        return beverage.getSize();
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Soy";
    }

    @Override
    public double cost() {
        double cost = beverage.cost();
        if (getSize() == Beverage.TALL) {
            cost += .1;
        } else if (getSize() == Beverage.GRANDE) {
            cost += .15;
        } else if (getSize() == Beverage.VENTI) {
            cost += .2;
        }
        return cost;
    }
}

优缺点

http://design-patterns.readthedocs.io/zh_CN/latest/structural_patterns/decorator.html#id10

装饰模式的优点:

  • 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
  • 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
  • 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
  • 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”

装饰模式的缺点:

  • 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
  • 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 [email protected]