【设计模式】3. 装饰者模式

设计模式学习笔记


版权声明:本文由 Hov 所有,发布于 http://chenhy.com ,转载请注明出处。



1 装饰者模式

1.1 简介

装饰者模式可动态地向对象添加额外的属性或行为。装饰者模式是类继承的另外一种选择,类继承在编译时候增加行为,而装饰模式是在运行时增加行为。在对象功能扩展方面,装饰者模式比继承更有弹性。

1.2 思路

通过增加一个装饰类包裹原来的类,包裹的方式一般是通过将原来的对象作为装饰类的构造函数的参数,从而在原有对象上添加新的功能。

1.3 使用场景

在不影响其它对象的情况下,想要动态地给单个对象添加新的属性或功能。

1.4 优缺点

优点:

  1. 可用一个或多个装饰者包装一个对象,扩展性强;
  2. 对象可以在任何时候被修饰,即动态修饰对象;
  3. 非继承,避免子类爆炸。

缺点:

  1. 出现额外的装饰类,使程序变得复杂。


2 实例

模拟咖啡店:

假设,咖啡店有两种咖啡可选: Decaf 和 Espresso ,两种配料可选: Chocalate 和 Milk 。

不同的咖啡和配料可以搭配成不同的饮品,比如 Decaf 、 Espresso 、 Decaf + Chocalate 、 Decaf + Milk 、 Espresso + Chocalate 、 Espresso + Milk 六种不同的饮品。

我们要写个程序来计算不同饮品的价格,比如客户点了一杯 Espresso + Chocalate ,输出饮品的价格和描述信息。

2.1 糟糕的解决方案

设计超类 Drink ,继承自超类 Drink 的子类 Decaf 、 Espresso 、 Decaf + Chocalate 、 Decaf + Milk 、 Espresso + Chocalate 、 Espresso + Milk 。

显然,每种组合的饮品都要写成一个子类。这样将导致子类爆炸,设想种类多的时候将有几百上千个不同的饮品子类。

这样的方案虽然设计简单,只需写个超类,然后将所有不同的饮品通通继承自超类即可。但是缺点是显而易见的,几百个饮品将对应几百个子类,不合理。



2.2 好一些的解决方案

  1. 在超类 Drink 中定义 hasChocalate 、 setChocalate 、 hasMilk 、 setMilk 方法;
  2. Decaf 、 Espresso 作为超类 Drink 的子类;
  3. 当需要 Decaf + Chocalate 就调用超类的 setChocalate 方法添加 Chocolate ,再通过 hasChocalate 方法计算总价。

这种方法比上一种方法好一些,显然它不会引起类的爆炸。但是,因为配料相关的代码都是写在超类 Drink 里面的,如果我们要增加一种新的配料,就必须要修改超类 Drink ,这样维护性不好。



2.3 基于装饰者模式的解决方案

基于装饰者模式,咖啡作为主体(被装饰者),配料作为装饰者。通过装饰类包裹主体,从而实现主体的动态扩展。

(1)超类 Drink

创建超类 Drink ,其包含饮品的基本属性,如描述、价格、子类应该实现的方法。

/* Drink.java */

public abstract class Drink {
    
    public String description = "";     //描述
    private float price = 0f;           //价格
    
    public abstract float cost();
    
    public String getDescription() {
        return description+"-"+this.getPrice();
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public float getPrice() {
        return price;
    }
    public void setPrice(float price) {
        this.price = price;
    }
    
}

(2)被装饰者(ConcreteComponent)

前面我们提到咖啡店有 Decaf 、 Espresso 两种咖啡,其实咖啡就是主体(被装饰者),我们可以往咖啡加入不同的配料“装饰”成不同的饮品。

Decaf 、 Espresso 继承自 Coffee , Coffee 继承自 Drink 。也就是说,主体下有两种类型的咖啡,以后我们新增一种类型的咖啡 LongBlack ,就只需让 LongBlack 像 Decaf 那样继承自 Coffee 即可,而无需修改超类 Drink 。

/* Coffee.java */

public class Coffee extends Drink {     //主体中间层

    @Override
    public float cost() {               //返回价格
        return super.getPrice();    
    }

}
/* Decaf.java */

public class Decaf extends Coffee {     // Decaf 咖啡(被装饰者)
    
    public Decaf() {
        super.setDescription("Decaf");  //描述
        super.setPrice(3.0f);           //价格
    }
    
}
/* Espresso.java */

public class Espresso extends Coffee {      // Espresso 咖啡(被装饰者)
    
    public Espresso() {
        super.setDescription("Espresso");   //描述
        super.setPrice(4.0f);               //价格
    }
    
}

(3)装饰者(Decorator)

咖啡可以加不同的配料,如牛奶、巧克力等等。牛奶、巧克力就是装饰者,通过它们包装被装饰者,从而使被装饰者实现动态扩展。

/* Decorator.java */

public class Decorator extends Drink {      //中间层

    private Drink Obj;                      //被装饰的对象
    
    public Decorator(Drink Obj) {
        this.Obj = Obj;
    };
    
    @Override
    public float cost() {
        return super.getPrice()+Obj.cost();     //计算价格,要加上被装饰对象原有的价格
    }
    
    public String getDescription() {
        return super.description + "-" + super.getPrice() + " && " +Obj.getDescription();   //递归增加原有属性
    }

}
/* Chocolate.java */

public class Chocolate extends Decorator {  //巧克力(装饰者)
    
    public Chocolate(Drink Obj) {           
        super(Obj);
        super.setDescription("Chocolate");  //描述
        super.setPrice(3.0f);               //价格
    }
    
}
/* Milk.java */

public class Milk extends Decorator {   //牛奶(装饰者)
    
    public Milk(Drink Obj) {
        super(Obj);
        super.setDescription("Milk");   //描述
        super.setPrice(2.0f);           //价格
    }
    
}

至此,我们已经完成了基础代码的编写。

我们设计了一个超类 Drink ,两个中间层 Coffee 、 Decorator ,两个继承自中间层 Coffee 的具体实现类 Decaf 、 Espresso ,两个继承自中间层 Decorator 的具体实现类 Chocolate 、 Milk 。

代码结构如下:



(4)测试

咖啡店新增了两个订单。

订单 1 :客户单点了一杯 Decaf ;

订单 2 :客户点了一杯 Espresso + Milk + Chocolate + Chocolate 。

最终,分别输出两个订单的饮品价格、饮品描述。

订单 2 结构如下:

/* CoffeeBar.java */

public class CoffeeBar {

    public static void main(String[] args) {

        Drink order;    //订单

        //订单 1 :只点Decaf,不加任何配料
        order = new Decaf();    //点一杯Decaf
        System.out.println("Order 1 Price:\n"+order.cost());            //输出饮品价格
        System.out.println("Order 1 Desc:\n"+order.getDescription());   //输出饮品描述
        
        System.out.println("******************");
        
        //订单 2 :点一杯 Espress o,加一份 Milk 、两份 Chocolate
        order = new Espresso(); //点一杯Espresso
        order = new Milk(order);        // Milk 装饰 Espresso(加一份 Milk )
        order = new Chocolate(order);   // Chocolate 装饰 Espresso + Milk(再加一份 Chocolate )
        order = new Chocolate(order);   // Chocolate 装饰 Espresso + Milk + Chocolate(再加一份 Chocolate )
        System.out.println("Order 2 Price:\n"+order.cost());            //输出饮品价格
        System.out.println("Order 2 Desc:\n"+order.getDescription());   //输出饮品描述
        
    }

}

CoffeeBar.java 运行结果:

Order 1 Price:
3.0
Order 1 Desc:
Decaf-3.0
******************
Order 2 Price:
12.0
Order 2 Desc:
Chocolate-3.0 && Chocolate-3.0 && Milk-2.0 && Espresso-4.0


3 结语

前面我们通过了一个实例来讲解装饰者模式。不同于直接使用继承,装饰者通过装饰类包裹被装饰对象实现扩展。

具体到例子中,两种咖啡(Decaf、Espresso),两种配料(Chocolate、Milk)。现在我们要实现 Decaf + Milk 的组合,常规的做法是 Decaf + Milk 直接继承自 Drink 这个超类,从而具备饮品的相关属性,但这样会引起子类爆炸。

通过装饰者模式,我们不直接使用继承。通过 Coffee 、 Decorator 作为中间层, Coffee 继承自超类 Drink ,显然 Coffee 具有基本的饮品属性(如 Decaf 、Espresso)。我们想要在 Decaf 上加入 Milk ,只需用 Decorator 中的 Milk 去包裹 Decaf 对象,从而 Decaf 对象增加了 Milk 相关的属性。

这就是装饰者模式实现动态扩展的思路。


 
comments powered by Disqus