设计模式 —— 装饰器模式

作者 : admin 本文共6622个字,预计阅读时间需要17分钟 发布时间: 2024-06-10 共2人阅读

设计模式 —— 装饰器模式

  • 什么是装饰器模式
    • 通俗解释
  • Component(组件接口)
  • ConcreteComponent(具体组件)
  • Decorator(装饰器接口)
  • ConcreteDecorator(具体装饰器)
  • 装饰器模式优缺点
      • 优点
      • 缺点

今天我们来看一个新的设计模式——装饰器模式

什么是装饰器模式

装饰器模式(Decorator Pattern)是一种设计模式,属于结构型模式的范畴。它的主要目的是动态地给一个对象添加额外的职责(功能),而不需要修改该对象的结构。装饰器模式通过创建包装对象来包裹原始对象,这些包装对象提供了与原始对象相同的接口,但可以在调用前后添加新的处理或责任。

装饰器模式的组成部分包括:

  1. Component(组件接口):定义了所有对象共有的操作接口,无论是装饰器还是被装饰的对象都要实现这个接口。
  2. ConcreteComponent(具体组件):实现了Component接口的具体对象,也就是被装饰的原始对象。
  3. Decorator(装饰器抽象类):持有Component的引用,定义了与Component接口一致的抽象操作,这样装饰器就可以替代被装饰对象。同时,装饰器抽象类提供了一个构造方法,用于传入被装饰的对象。
  4. ConcreteDecorator(具体装饰器):实现了装饰器抽象类,负责具体的装饰逻辑,可以添加额外的职责或行为。

装饰器模式的优势:

  • 灵活性:可以在运行时动态地添加或移除对象的功能,而无需修改对象的代码。
  • 遵循开放封闭原则:对扩展开放(可以轻松添加新的装饰器),对修改封闭(不需要改变现有的组件代码)。
  • 重用性:装饰器可以复用,而且可以组合使用,通过不同的装饰器组合来创造复杂的对象行为。
  • 替代继承:相比多层继承,装饰器模式提供了更灵活的替代方案来扩展功能,减少了类的复杂度。

使用场景:

  • 需要动态、透明地给对象增加职责时。
  • 当不能采用继承达到扩展目的,或者采用继承会导致类爆炸时(继承层次过深或过多)。
  • 当需要为对象添加职责,但又不想硬编码到对象中时,以保持对象的可复用性。

通俗解释

想象一下你去一家咖啡店买咖啡。基础的咖啡就像是一杯简单的美式,没有任何额外添加。装饰器模式就像是在咖啡店中添加调料的过程。

  • 基础咖啡就像是装饰器模式中的“被装饰的对象”,就是那个最原始、最基本的东西,比如一杯纯咖啡。
  • 装饰器则是那些可以往咖啡里加的东西,比如牛奶、糖、巧克力粉等。每个装饰器都围绕着“咖啡”这个核心,你可以选择添加一个或多个装饰器来定制你的咖啡。

在程序里,如果我们要给一个功能(比如基础的“打印日志”功能)增加新能力,比如添加时间戳、或者改变字体颜色,而不改变原本的“打印日志”代码,就可以用装饰器模式。就像是给咖啡加糖不改变咖啡本质,但让咖啡变甜了一样。

所以,装饰器模式就是一种设计方法,它允许我们在不修改原始对象的基础上,通过添加一层层“装饰”(附加功能)来扩展对象的功能,就像你点咖啡时一层层添加各种调料,却还是那杯咖啡,只是功能(味道)更丰富了。

Component(组件接口)

我们这里以咖啡举例,首先我们要完成一个基础的接口:

class CoffeeBase
{
public:
    // 声明为纯虚函数,要求任何继承此类的子类必须实现该函数
    // 获取咖啡的描述信息,如名称、口味等
    virtual std::string getDescription() const = 0;

    // 声明为纯虚函数,要求任何继承此类的子类必须实现该函数
    // 返回咖啡的成本价格
    virtual double getCost() const = 0;

    // 虚析构函数的声明,确保通过基类指针或引用来删除派生类对象时,
    // 能够正确调用到派生类的析构函数,防止资源泄露
    virtual ~CoffeeBase();
};

// 虚析构函数的定义,这里是一个空的实现
// 对于基类CoffeeBase来说,可能不需要在析构函数中执行具体操作,
// 但定义它是必要的,尤其是在有多态使用场景下,确保析构链的完整
CoffeeBase::~CoffeeBase() {}

ConcreteComponent(具体组件)

我们提供一个具体组件,SimpleCoffee:

// 简单coffee类,继承自CoffeeBase基类
class SimpleCoffee : public CoffeeBase
{
public:
    // 重写从基类CoffeeBase继承的getDescription函数
    // 此函数返回一个描述简单咖啡的字符串,这里是"This is a simple coffee"
    std::string getDescription() const override
    {
        return "This is a simple coffee";
    }

    // 重写从基类CoffeeBase继承的getCost函数
    // 此函数返回简单咖啡的成本价格,设定为5.0元
    double getCost() const override
    {
        return 5.0;
    }
};

Decorator(装饰器接口)

我们还要实现一个装饰器接口:

// 装饰器基类,继承自CoffeeBase,用于给咖啡添加额外功能或装饰
class CoffeeDecorator : public CoffeeBase
{
protected:
    // 持有一个指向CoffeeBase的指针,用于存储被装饰的咖啡对象
    CoffeeBase* baseDecorator;

public:
    // 构造函数,接收一个CoffeeBase类型的指针作为参数
    // 这个参数实际上是被装饰的基础咖啡实例
    CoffeeDecorator(CoffeeBase* coffee)
        : baseDecorator(coffee) // 初始化baseDecorator指向传入的coffee
    {
        // 构造函数体为空,初始化已经在成员初始化列表中完成
    }

    // 重写getDescription函数,委托给被装饰的CoffeeBase对象来获取描述
    // 这样可以保证装饰后的咖啡描述首先展示的是原始咖啡的信息
    std::string getDescription() const override
    {
        return baseDecorator->getDescription();
    }

    // 重写getCost函数,同样委托给被装饰的CoffeeBase对象来计算成本
    // 使得装饰过程不影响原始咖啡成本的计算,装饰器类可在此基础上累加额外成本
    double getCost() const override
    {
        return baseDecorator->getCost();
    }
};

ConcreteDecorator(具体装饰器)

具体装饰器实现具体的功能:

// 实现加牛奶的装饰器,继承自CoffeeDecorator
class MilkAdd : public CoffeeDecorator
{
public:
    // 构造函数,接收一个CoffeeBase类型的指针,用于装饰的原始咖啡
    MilkAdd(CoffeeBase* coffee)
        : CoffeeDecorator(coffee) // 通过基类构造函数初始化基类部分,传入被装饰的咖啡实例
    {
        // 构造函数体为空,因为初始化已经在成员初始化列表中完成
    }

    // 重写getDescription函数,在原有咖啡描述后添加" Milk"
    // 表示这杯咖啡添加了牛奶
    std::string getDescription() const override
    {
        return baseDecorator->getDescription() + " Milk";
    }

    // 重写getCost函数,在原有咖啡成本基础上增加3元
    // 代表添加牛奶的服务费用
    double getCost() const override
    {
        return baseDecorator->getCost() + 3;
    }
};

// 实现加糖的装饰器,同样继承自CoffeeDecorator
class SugarAdd : public CoffeeDecorator
{
public:
    // 构造函数,接收一个CoffeeBase类型的指针,即要装饰的咖啡
    SugarAdd(CoffeeBase* coffee)
        : CoffeeDecorator(coffee) // 通过基类构造函数初始化,传入被装饰的咖啡实例
    {
        // 构造函数体为空
    }

    // 重写getDescription函数,在原有咖啡描述后追加" Sugar"
    // 表明这杯咖啡添加了糖
    std::string getDescription() const override
    {
        return baseDecorator->getDescription() + " Sugar";
    }

    // 重写getCost函数,在原有咖啡成本上增加2元
    // 代表加糖的额外费用
    double getCost() const override
    {
        return baseDecorator->getCost() + 2;
    }
};

我们可以一一对应:

这段代码展示了装饰器模式的各个组成部分,具体如下:

  1. Component(组件接口):在这个例子中,CoffeeBase类扮演了组件接口的角色。它定义了一个咖啡对象的基本操作接口,包括getDescription()用于获取咖啡的描述和getCost()用于获取成本价格,还有一个虚析构函数确保通过基类指针可以安全地删除派生类对象。
  2. ConcreteComponent(具体组件)SimpleCoffee类是CoffeeBase的子类,代表了具体组件。它实现了getDescription()getCost(),提供了基础咖啡的描述和成本。
  3. Decorator(装饰器接口):虽然在这个实现中没有显式定义一个独立的装饰器接口类,但CoffeeDecorator类实际上承担了装饰器接口的角色。它继承自CoffeeBase,并持有一个CoffeeBase* baseDecorator指针,通过这个指针调用基础咖啡的操作,同时定义了与CoffeeBase相同的接口方法,允许装饰器可以像咖啡一样被操作。
  4. ConcreteDecorator(具体装饰器)MilkAddSugarAdd类是具体的装饰器类,它们继承自CoffeeDecorator。每个装饰器类都在其getDescription()getCost()方法中添加了额外的功能(例如价格和描述中的” Milk”或” Sugar”),同时调用基类的相应方法以保持原有的功能,体现了装饰器模式的“叠加”特性。

这是整体的代码:

// 使用#pragma once指令防止头文件被多次包含
#pragma once
// 引入iostream和string头文件,以便进行输入输出和字符串操作
#include 
#include 
// 定义咖啡基础接口类,所有的咖啡类型都需要实现这个接口
class CoffeeBase
{
public:
// 纯虚函数,获取咖啡的描述信息,子类必须实现
virtual std::string getDescription() const = 0;
// 纯虚函数,获取咖啡的成本价格,子类必须实现
virtual double getCost() const = 0;
// 虚析构函数,确保通过基类指针可以正确删除派生类对象
virtual ~CoffeeBase();
};
// 基类CoffeeBase的析构函数定义
CoffeeBase::~CoffeeBase() {}
// 简单咖啡类,实现CoffeeBase接口,代表不加任何调料的原始咖啡
class SimpleCoffee : public CoffeeBase
{
public:
// 重写getDescription函数,返回简单咖啡的描述信息
std::string getDescription() const override
{
return "This is a simple coffee";
}
// 重写getCost函数,返回简单咖啡的成本价格
double getCost() const override
{
return 5.0;
}
};
// 装饰器基类,继承自CoffeeBase,用于给咖啡添加额外属性或装饰
class CoffeeDecorator : public CoffeeBase
{
protected:
// 持有一个指向CoffeeBase的指针,用于存储被装饰的咖啡实例
CoffeeBase* baseDecorator;
public:
// 构造函数,接受一个CoffeeBase指针,初始化被装饰的咖啡对象
CoffeeDecorator(CoffeeBase* coffee)
:baseDecorator(coffee)
{}
// 重写getDescription函数,调用被装饰咖啡的getDescription
std::string getDescription() const override
{
return baseDecorator->getDescription();
}
// 重写getCost函数,调用被装饰咖啡的getCost
double getCost() const override
{
return baseDecorator->getCost();
}
};
// 牛奶装饰器类,为咖啡添加牛奶
class MilkAdd : public CoffeeDecorator
{
public:
// 构造函数,接受一个CoffeeBase指针,用于装饰的咖啡
MilkAdd(CoffeeBase* coffee)
:CoffeeDecorator(coffee)
{}
// 重写getDescription,在原有描述后添加" Milk"
std::string getDescription() const override
{
return baseDecorator->getDescription() + " Milk";
}
// 重写getCost,在原价基础上增加3元(牛奶费用)
double getCost() const override
{
return baseDecorator->getCost() + 3;
}
};
// 糖装饰器类,为咖啡添加糖
class SugarAdd : public CoffeeDecorator
{
public:
// 构造函数,接受一个CoffeeBase指针,用于装饰的咖啡
SugarAdd(CoffeeBase* coffee)
: CoffeeDecorator(coffee)
{}
// 重写getDescription,在原有描述后添加" Sugar"
std::string getDescription() const override
{
return baseDecorator->getDescription() + " Sugar";
}
// 重写getCost,在原价基础上增加2元(糖费用)
double getCost() const override
{
return baseDecorator->getCost() + 2;
}
};

装饰器模式优缺点

装饰器模式(Decorator Pattern)作为一种常用的设计模式,有其独特的优点和缺点,下面是其主要特点概述:

优点

  1. 灵活性和可扩展性:装饰器模式允许在不修改原有类代码的情况下动态地扩展对象功能,为对象添加新的职责。这使得系统易于扩展,遵循了开闭原则。
  2. 组合模式的替代:相比于通过继承来扩展功能,装饰器模式提供了更灵活的选择,可以避免类层次过深的问题,因为装饰器可以按需叠加,每个装饰器只关注添加单一功能。
  3. 透明性:对于客户端而言,装饰过的对象和未装饰的对象使用方式相同,因为它们都遵循相同的接口。这使得客户端代码可以不关心对象是否被装饰以及如何被装饰,提高了代码的可读性和可维护性。
  4. 重用性:装饰器类可以被多次使用,也可以用来装饰不同的对象,增加了代码的复用性。

缺点

  1. 过度使用导致复杂性:如果过度使用装饰器,可能会导致系统中有很多细小的装饰器类,这会使得类的结构变得复杂,难以理解与维护。
  2. 调试困难:由于装饰器层层嵌套,当出现错误时,调试和定位问题可能变得较为复杂,特别是当装饰器链很长时。
  3. 性能考量:每添加一个装饰器,就增加了一层间接调用,极端情况下可能会影响执行效率,尽管在大多数现代系统中这通常不是主要问题。
  4. 不易理解:对于不熟悉装饰器模式的新开发者来说,理解装饰器如何运作以及如何正确使用它们可能需要时间。

总的来说,装饰器模式在需要动态、灵活地扩展对象功能的场景下非常有用,但需要权衡其带来的潜在复杂性与维护成本。合理使用可以极大提高软件的可维护性和灵活性。

本站无任何商业行为
个人在线分享 » 设计模式 —— 装饰器模式
E-->