设计模式 (三)——装饰者模式(Decorator,结构型)

1.概述

使用设计模式可以提高代码的可复用性、可扩充性和可维护性。装饰者模式( Pattern)属于结构型模式,动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。它是通过创建一个包装对象,通过包装对象来包裹真实的对象,以达到装饰目的。

装饰者模式在现实生活中有很多例子,比如一杯咖啡,我们可以往其中加入一些调料,加入巧克力变成摩卡咖啡,加入牛奶变成拿铁咖啡,也可以继续往摩卡或拿铁中加入焦糖、豆浆、奶泡等其他的调料,变成不同风味的咖啡。如果每一种风味的咖啡都变成单独的类,那么不同调料组合而成的咖啡种类数量会非常庞大,会造成类膨胀。每一种调料应该充当装饰者的角色,来修饰咖啡这一具体组件。

装饰模式类结构图:

Component:抽象组件,给具体类对象动态地添加职责。 ConcreteComponent:抽象组件的具体实现类。 Decorator:抽象装饰者,继承Component,用于拓展Component类的功能,但对于Component来说无需知道Decorator的存在。 ConcreteDecorator:装饰者具体实现类。

2.实例

下面以咖啡为例,以C++为例,使用观察者模式来解决咖啡的计价问题。

2.1丑陋的设计

咖啡添加了不同的调料,将收取不同的费用。因为调料数量众多,我们不能为每一种口味的咖啡创建一个类。于是我们想到了使用布尔变量进行组合后继承。类设计如下:

超类Coffee的数据成员是是否使用对应调料的布尔值,cost()方法需要计算所有调料的价格,而子类覆盖cost()会扩展超类的功能,把指定的调料价格加进去,计算出指定口味咖啡的价格。这样可以大大将少类的个数,但是仔细观察,我们会发现当出现新的调料,不得不修改超类Coffee。此时,我们需要坚持一个OO设计原则:类应该对扩展开放,对修改关闭

开放关闭原则,允许系统在不修改代码的情况下,进行功能扩展。想想观察者模式,可以在任何时候添加和删除观察者而不需要修改主题代码。本文描述的装饰者模式同样可以做到。

2.2使用装饰者模式

从上面了解到,利用继承无法完全解决问题,咖啡的设计中遇到的问题有:类数量爆炸、设计死板,以及基类加入新功能并不适用于有所的子类。所以,我们的做法是:以原味咖啡为主体,在运行时以调料来”装饰”(decorate)原味咖啡。比方说,如果顾客想要加奶泡(whip)和牛奶(milk)的摩卡,我们的做法是: (1)拿一个原味咖啡(Coffee)对象; (2)以巧克力(Chocolate)对象装饰它; (3)以奶泡(Whip)对象装饰它; (4)调用cost()方法,并依赖委托(delegate)将调料的价格加上去。

有了上面的步骤,在具体实现上,如何装饰一个对象,而委托又要如何搭配使用呢?请看下面的类图框架:

四个具体组件,每个代表一种咖啡类型。四个具体调料装饰者继承于抽象调料装饰者类(CondimentDecorator),抽象调料装饰者类又继承于抽象原味咖啡类(Coffee)。借助C++具体实现如下:

咖啡基类Coffee(被装饰者的基类):

class Coffee {
protected:
    string description;
public:
    virtual double cost() = 0;
    virtual string getDescription() {
        return description;
    }
};

以摩卡咖啡为例,实现具体咖啡类Mocha,最开始有自己的简单装饰:

class Mocha :public Coffee {
public:
    Mocha() {
        description="摩卡";
    }
    //重写,返回价格
    virtual double cost() {
        return 10;
    }
};

抽象调料基类(CondimentDecorator,装饰者基类)用来对原味咖啡进行多层装饰,每层装饰增加一些配料。

class CondimentDecorator :public Coffee {
};

装饰者奶泡类(Whip,装饰者的第一层):

class Whip :public CondimentDecorator {
    Coffee* coffee;
public:
    Whip(Coffee* coffee) {
        this->coffee = coffee;
    }
    //价格增加0.5元
    double cost() {
        return 0.5 + coffee->cost();
    }
    //增加第一层装饰
    string getDescription() {
        return coffee->getDescription() + "+奶泡";
    }
};

装饰者牛奶类(Milk,装饰者的第二层):

class Milk :public CondimentDecorator {
    Coffee* coffee;
public:
    Milk(Coffee* coffee) {
        this->coffee = coffee;
    }
    //价格增加1.7元
    double cost() {
        return 1.7 + coffee->cost();
    }
    //增加第二层装饰
    string getDescription() {
        return coffee->getDescription() + "+牛奶";
    }
};

测试代码:

#include <iostream>  
#include <string>
using namespace std;

int main(){
    Coffee* mocha=new Mocha;
    cout << mocha->getDescription() << " 价格:" << mocha->cost()<<endl;

    //添加奶泡
    Coffee* mocha1 = new Whip(mocha);
    cout << mocha1->getDescription() << " 价格:" << mocha1->cost() << endl;

    //添加牛奶
    mocha1 = new Milk(mocha);
    cout << mocha1->getDescription() << " 价格:" << mocha1->cost() << endl;

    //添加奶泡和牛奶
    mocha1 = new Whip(mocha);
    mocha1 = new Milk(mocha1);
    cout << mocha1->getDescription() << " 价格:" << mocha1->cost() << endl;
    system("pause");
}

输出结果:

摩卡 价格:10
摩卡+奶泡 价格:10.5
摩卡+牛奶 价格:11.7
摩卡+奶泡+牛奶 价格:12.2

3.使用场景和优缺点

使用场景: (1)在不必改变原类和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。 (2)当不能采用继承和组合的方式对系统进行扩充或者不利于系统扩展和维护时。

优点: (1)使用Decorator模式,具体组件类与具体装饰类可以独立变化,用户可以根据需要新增具体组件类和具体装饰类,使用时再对其进行继承,原有代码无须改变,符合“开放关闭原则”。 (2)Decorator模式与继承的目的都是扩展对象功能,有效避免了使用继承的方式扩展对象功能而带来的灵活性差,子类无限制扩张的问题。 (3)通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。

缺点: (1)装饰链不能过长,否则会影响效率。 (2)装饰模式会导致设计中出现许多小类,如果过度使用,会使程序变得很复杂。 (3)比继承更加灵活机动,同时意味着装饰模式比继承更加易于出错,排错也很困难。对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐,所以只在必要的时候使用装饰者模式。

4.小结

(1)OO设计原则:对扩展开放,对修改关闭,即开放关闭原则。 (2)装饰者模式:动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。


参考文献

[1]装饰者模式.百度百科 [2]设计模式(七)装饰模式 [3]JAVA设计模式初探之装饰者模式 [4]Freeman E.,Freeman E.,Sierra K.,et al.设计模式[M].第一版O’Reilly Taiwan公司译.北京:中国电力出版社,2015:79-105

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏YoungGy

ML基石_8_NoiseAndError

recap Noise and Probabilistic Target noise来源 Probabilistic Target Error Measure ...

1935
来自专栏葡萄城控件技术团队

Visual Studio 2015速递(1)——C#6.0新特性怎么用

系列文章 Visual Studio 2015速递(1)——C#6.0新特性怎么用 Visual Studio 2015速递(2)——提升效率和质量(VS20...

1908
来自专栏从流域到海域

《笨办法学python》 第14课手记

《笨办法学Python》 第14课手记 本节课将argv和raw_input和起来使用,作者在之前说,这个组合是个蛮顺手的用法。请注意,引入argv并使用arg...

23110
来自专栏HansBug's Lab

1022: [SHOI2008]小约翰的游戏John

1022: [SHOI2008]小约翰的游戏John Time Limit: 1 Sec  Memory Limit: 162 MB Submit: 1322 ...

2874
来自专栏ACM算法日常

过山车(匈牙利算法)- HDU 2063

输入数据的第一行是三个整数K , M , N,分别表示可能的组合数目,女生的人数,男生的人数。0<K<=1000 1<=N 和M<=500.接下来的K行,每行有...

2421
来自专栏ml

HDUOJ----4502吉哥系列故事——临时工计划

吉哥系列故事——临时工计划 Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 65535/3276...

3567
来自专栏阮一峰的网络日志

每行字符数(CPL)的起源

前几天,我收到网友小龙的Email。 他想与我讨论一个问题: "各种计算机语言的编码风格,有的建议源码每行的字符数(characters per line)不...

3466
来自专栏数据和云

大象起舞:用PostgreSQL解海盗分金问题

今天午休期间刷微信,看到云和恩墨的盖总转了一条朋友圈,说杨长老在Oracle中用SQL解海盗分金问题(原文《无往不利:用SQL解海盗分金的利益最大化问题》,看完...

1286
来自专栏JAVA高级架构

七夕情人节,程序员怎样表白更有效?

七夕情人节快乐 2017.08.28 今天是传统节日--七夕节,也是中国人传统意义上的"情人节",在此祝大家开心。然后,各大平台又被七夕节刷屏了... 作为国...

8025
来自专栏Jimoer

Java设计模式学习记录-装饰模式

装饰模式也是一种结构型模式,主要是目的是相对于类与类之间的继承关系来说,使用装饰模式可以降低耦合度。JDK中有不少地方都使用到了装饰模式,例如Java的各种I/...

741

扫码关注云+社区

领取腾讯云代金券