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

设计模式学习笔记


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



1 装饰者模式


1.1 简介

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

1.2 思路

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

1.3 使用场景

在不影响其它对象的情况下,想要动态地给单个对象添加新的属性或功能。例如,Java IO 就采用了装饰者模式。

1.4 优缺点

优点:

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

缺点:

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


2 实例


模拟咖啡店:

假设,咖啡店有两种咖啡可选: DecafEspresso ,两种配料可选: ChocalateMilk

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

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

2.1 糟糕的解决方案

一种解决方案是设计父类 Drink ,子类 Decaf 、 Espresso 、 Decaf + Chocalate 、 Decaf + Milk 、 Espresso + Chocalate 、 Espresso + Milk (对应不同的饮品)继承自父类 Drink

显然,每种组合的饮品都要写成一个子类,这样将导致子类爆炸。如果有 1000 种不同的饮品,那父类 Drink 下面就有 1000 个子类,这是绝对不能允许的。

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

本方案类图如下:

父类 Drink 下面继承了各种各样的饮品子类,眼花缭乱。


2.2 稍好的解决方案

另一种方案是将不同的配料写入父类 Drink 中,子类通过 setter 来添加不同配料

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

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

本方案类图如下:

父类 Drink 中包括不同配料的 setter 方法,子类只需调用相应的 setter 方法组合不同的配料。


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

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

(1)父类 Drink

首先创建父类 Drink ,其包含饮品的基本属性(描述、价格),并定义了 setter 和 getter 方法,以及抽象方法 cost 。

/* 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 :

/* Coffee.java */

public class Coffee extends Drink {     //主体中间层
    @Override
    public float cost() {               //返回价格
        return super.getPrice();    
    }
}

被装饰者 Decaf :

/* Decaf.java */

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

被装饰者 Espresso :

/* Espresso.java */

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

(3)装饰者(Decorator)

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

中间层 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 :

/* Chocolate.java */

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

装饰者 Milk :

/* 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 :点一杯 Espresso,加一份 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());   //输出饮品描述
    }
}

程序运行结果:

输出了不同订单对应的价格和描述。

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