概述
在软件开发中,我们经常遇到需要给现有对象添加新功能的情况。最直接的方法是通过继承来实现,即创建一个子类,并重写或新增方法。然而,继承这种方式有如下几个缺点。
1、违反开放封闭原则。开放封闭原则指出软件实体应该是对扩展开放的,但是对修改关闭的。使用继承来添加功能会使得我们必须修改已有的类,这显然不符合这一原则。
2、代码膨胀。如果一个类有多种可选的行为,那么为了覆盖所有可能的组合,可能会导致大量的子类产生,使代码库变得臃肿、难以维护。
3、灵活性不足。一旦子类被创建出来,其行为就被固定下来了,无法在运行时动态地改变行为。
相比之下,装饰器模式提供了更好的解决方案,因为它允许我们在不改变原对象的情况下,通过包装的方式为其添加新的行为。而且,这些行为可以在程序运行期间动态地添加或移除,从而提高了代码的灵活性和复用性。
咖啡店点单是运用装饰器模式的一个典型例子:假如我们在咖啡店里点了一杯美式咖啡,然后根据自己的口味选择添加牛奶、糖、奶油等调料;每加一种调料,咖啡就变得更丰富,但基本的美式咖啡并没有改变。我们可以随时决定是否需要这些额外的配料,并且可以灵活组合不同的配料以满足个人喜好。
基本原理
装饰器模式的核心思想是:提供一种动态地给对象添加职责的方式,而不是通过继承来静态地扩展功能。它允许行为和责任的组合在运行时进行,并且不会影响其他对象。装饰器模式特别强调了开放封闭原则,即软件实体应该对扩展开放,对修改关闭。装饰器模式包括如下四个核心组件。
组件:定义了一个接口,所有的具体组件和装饰器都实现了这个接口。
具体组件:实现了组件接口,定义了基本的行为和状态。
装饰器:也实现了组件接口,但持有一个指向其他组件的引用。装饰器可以在调用被包装的对象之前或之后执行额外的操作,从而实现对原始行为的增强。
具体装饰器:每个具体装饰器可以单独提供特定的功能,也可以组合多个装饰器来构建复杂的行为。
基于上面的核心组件,装饰器模式的实现主要有以下五个步骤。
1、定义组件接口。首先,定义一个接口或抽象类,它声明了所有具体组件和装饰器都应该实现的方法。这一步,确保了所有装饰器和组件之间的兼容性。
2、创建具体组件。接下来,实现组件接口的具体类,这些类代表了不需要任何额外功能的基础对象。
3、创建抽象装饰器。定义一个装饰器类,它同样实现了组件接口,并持有一个指向其他组件的引用。通常,这个类会有一个构造函数接受一个组件作为参数,并将其保存起来。这样做的目的是:为了能够将任何实现了组件接口的对象传递给装饰器,包括其他装饰器。
4、创建具体装饰器。为每一个想要添加的新功能创建具体的装饰器类。每个具体装饰器都将覆盖从抽象装饰器继承下来的方法,在其中加入自己的逻辑,同时调用被包装组件相应的方法。这样就实现了在原有行为的基础上,添加新功能的目的。
5、使用装饰器。在应用层代码中,可以通过将具体组件传递给装饰器并逐步叠加更多的装饰器来构建所需的功能组合。最终得到的对象包含了所有期望的行为,而基础组件本身并没有被修改。
实战解析
在下面的实战代码中,我们使用装饰器模式创建了富含各种调料(牛奶、糖、奶油)的咖啡。
首先,我们定义了一个组件接口CCoffee,它声明了所有具体咖啡和装饰器都必须实现的方法GetDesc和CalcCost。其中,GetDesc函数返回咖啡的描述信息,CalcCost函数返回咖啡的价格。
接着,我们定义了具体组件CAmericanoCoffee。它实现了CCoffee接口,代表一杯基础的美式咖啡。装饰器基类CCoffeeDecorator也继承自CCoffee,并持有一个指向其他咖啡对象的指针m_pCoffee。默认情况下,GetDesc函数和CalcCost函数会调用被包装对象相应的方法,即它本身不改变任何行为。
具体装饰器CMilkDecorator、CSugarDecorator、CCreamDecorator分别表示加牛奶、加糖、加奶油的装饰器。每个装饰器继承自CCoffeeDecorator,并重写了GetDesc和CalcCost函数,在原有基础上增加了特定的调料描述和价格。
最后,在main函数中,我们创建了一杯美式咖啡,并将其赋值给pCoffee。然后,我们逐步添加调料,分别创建了CMilkDecorator、CSugarDecorator、CCreamDecorator对象,每次都将前一个对象作为新装饰器的构造参数传递进去。这样就形成了一个装饰器链,每个新的装饰器都会在其内部保存对前一个对象的引用,并在其上添加新的功能。
#include <iostream>
#include <string>
using namespace std;
// 组件:咖啡
class CCoffee
{
public:
virtual ~CCoffee() {}
virtual string GetDesc() const = 0;
virtual double CalcCost() const = 0;
};
// 具体组件:美式咖啡
class CAmericanoCoffee : public CCoffee
{
public:
string GetDesc() const
{
return "Americano Coffee";
}
double CalcCost() const
{
return 30.0;
}
};
// 装饰器基类
class CCoffeeDecorator : public CCoffee
{
public:
CCoffeeDecorator(CCoffee* pCoffee) : m_pCoffee(pCoffee) {}
string GetDesc() const
{
return m_pCoffee->GetDesc();
}
double CalcCost() const
{
return m_pCoffee->CalcCost();
}
protected:
CCoffee* m_pCoffee;
};
// 牛奶装饰器
class CMilkDecorator : public CCoffeeDecorator
{
public:
CMilkDecorator(CCoffee* c) : CCoffeeDecorator(c) {}
string GetDesc() const
{
return CCoffeeDecorator::GetDesc() + ", Milk";
}
double CalcCost() const
{
return CCoffeeDecorator::CalcCost() + 1.5;
}
};
// 糖装饰器
class CSugarDecorator : public CCoffeeDecorator
{
public:
CSugarDecorator(CCoffee* c) : CCoffeeDecorator(c) {}
string GetDesc() const
{
return CCoffeeDecorator::GetDesc() + ", Sugar";
}
double CalcCost() const
{
return CCoffeeDecorator::CalcCost() + 1.0;
}
};
// 奶油装饰器
class CCreamDecorator : public CCoffeeDecorator
{
public:
CCreamDecorator(CCoffee* c) : CCoffeeDecorator(c) {}
string GetDesc() const
{
return CCoffeeDecorator::GetDesc() + ", Cream";
}
double CalcCost() const
{
return CCoffeeDecorator::CalcCost() + 2.0;
}
};
int main()
{
// 创建美式咖啡
CCoffee* pCoffee = new CAmericanoCoffee();
// 加牛奶
CCoffee* pMilkCoffee = new CMilkDecorator(pCoffee);
// 加糖
CCoffee* pSugarCoffee = new CSugarDecorator(pMilkCoffee);
// 加奶油
CCoffee* pCreamCoffee = new CCreamDecorator(pSugarCoffee);
// 输出:Americano Coffee, Milk, Sugar, Cream
cout << pCreamCoffee->GetDesc() << endl;
// 输出:34.5
cout << pCreamCoffee->CalcCost() << endl;
// 释放内存
delete pCreamCoffee;
delete pSugarCoffee;
delete pMilkCoffee;
delete pCoffee;
return 0;
}
总结
可以看到,通过组合不同的装饰器,可以创建出多种多样的功能组合,而无需为每一种可能的功能组合创建新的子类。这样减少了代码量,提高了代码的可维护性和复用性。但随着装饰器数量的增加,系统的整体复杂度也会相应提升。过多的装饰器可能会使代码难以理解和维护,特别是当装饰器之间存在复杂的交互时。另外,每次调用被装饰的方法时,都会经过一系列的装饰器链,这可能导致额外的性能开销。虽然通常情况下这种开销是可以接受的,但在性能要求极高的场景下可能需要特别注意。