C++ 设计模式 八:装饰器模式 (读书 现代c++设计模式)

装饰器模式

今天看第八种设计模式:装饰器模式。

装饰器模式作为一种结构型设计模式, 允许在不改变对象自身的基础上动态地为对象添加功能或者行为。

组成

装饰器模式中的类一般有以下职责:

  • Component 抽象类: 定义了所有具体组件和装饰器的公共接口

  • ConcreteComponent 类:实现抽象 Component 接口,是真正被装饰的对象。

  • Decorator 抽象类:持有一个对 Component 对象的引用, Component 接口。

  • ConcreteDecorator 类:继承自 Decorator, 负责向组件添加新的职责。

开头时作者的一个场景引入,假设现在正在使用同事编写的类,并且希望扩展该类的功能。

在不修改原类的情况下,第一种方法是用继承创建一个派生类,然后添加需要的功能,或者对原有方法重写。

不过这并不总可行, 比如我们通常不希望从 std::vector 继承,因为它没有提供默认的虚析构函数。

继承不起作用的最关键原因是:当需要添加多个功能时,我们希望遵循但一直则原则,将这些功能分开。

装饰器模式就允许我们在不修改原始类型(遵守开闭原则)或者导致派生类型数量激增的情况下增加现有类的功能。

场景

假定我们定义一个名为 Shape 的抽象类作为要实现的抽象组件(接口), 然后定义一个抽象类 Decorator 作为抽象装饰器作为具体装饰器的基础内容.

#include <string>
#include <memory>

using namespace std;

// 抽象组件(Component) Shape
struct Shape {
	virtual ~Shape() = default;
	[[nodiscard]] virtual std::string str() const = 0;
	[[nodiscard]] virtual std::unique_ptr<Shape> clone() const = 0;
};

// 显式定义Decorator抽象类
class Decorator : public Shape {
protected:
	std::unique_ptr<Shape> component_;

public:
	explicit Decorator(std::unique_ptr<Shape> component)
		: component_(std::move(component)) {}

	[[nodiscard]] std::unique_ptr<Shape> clone() const override {
		return std::make_unique<Decorator>(component_->clone());
	}
};

Shape 中, str() 是一个虚函数,我们使用他来提供表示特定形状的字符串。现在我们能够用这个接口实现 Circle类和Square类:

#include <string>
#include <sstream>
#include <cstdint>
#include <memory>
#include <iostream>

using namespace std;

// 抽象组件(Component) Shape
struct Shape {
	virtual ~Shape() = default;
	[[nodiscard]] virtual std::string str() const = 0;
	[[nodiscard]] virtual std::unique_ptr<Shape> clone() const = 0;
};

// 显式定义Decorator抽象类
class Decorator : public Shape {
protected:
	std::unique_ptr<Shape> component_;

public:
	explicit Decorator(std::unique_ptr<Shape> component)
		: component_(std::move(component)) {}

	[[nodiscard]] std::unique_ptr<Shape> clone() const override {
		return std::make_unique<Decorator>(component_->clone());
	}
};

// 具体组件(ConcreteComponent)
struct Circle : Shape {
public:
	float radius_;
	explicit Circle(const float& radius) : radius_{radius} {}

	void resize(const float& factor) { radius_ *= factor; }

	[[nodiscard]] unique_ptr<Shape> clone() const override {
		return make_unique<Circle>(*this);
	}

	[[nodiscard]] string str() const override {
		ostringstream oss;
		oss << "A circle of radius " << radius_;
		return oss.str();
	}
};

// 具体组件(ConcreteComponent)
struct Square : Shape {
public:
	float length_;
	explicit Square(const float& length) : length_{length} {}

	void resize(const float& factor) { length_ *= factor; }

	[[nodiscard]] unique_ptr<Shape> clone() const override {
		return make_unique<Square>(*this);
	}

	[[nodiscard]] string str() const override {
		ostringstream oss;
		oss << "A Square of length: " << length_;
		return oss.str();
	}
};

动态装饰器

现在我们有一个需求:想要给形状增加一些颜色,我们可以用组合替代继承实现ColorShape类,引用一个已经构造好的Shape对象并且额外增加一些方法和属性:

// 具体装饰器(ConcreteDecorator)
class ColoredShape final : public Decorator {
	std::string color_;

public:
	ColoredShape(std::unique_ptr<Shape> shape, std::string color)
		: Decorator(std::move(shape)), color_(std::move(color)) {}

	[[nodiscard]] std::string str() const override {
		return component_->str() + " with color " + color_;
	}

	[[nodiscard]] std::unique_ptr<Shape> clone() const override {
		return std::make_unique<ColoredShape>(component_->clone(), color_);
	}
};

如果我们现在想要增加形状的透明度也很简单:

// 具体装饰器(ConcreteDecorator)
class TransparentShape final : public Decorator {
	uint8_t transparency_;

public:
	TransparentShape(std::unique_ptr<Shape> shape, uint8_t transparency)
		: Decorator(std::move(shape)), transparency_(transparency) {}

	[[nodiscard]] std::string str() const override {
		return component_->str() + " with " +
			std::to_string(static_cast<float>(transparency_) / 255 * 100) + "% transparency";
	}

	[[nodiscard]] std::unique_ptr<Shape> clone() const override {
		return std::make_unique<TransparentShape>(component_->clone(), transparency_);
	}
};

现在我们可以把 ColoredShapeTransparentShape 组合起来,使得一个形状既有颜色又有透明度:

void test() {
	auto circle = std::make_unique<Circle>(5.0f);
	auto red_circle = std::make_unique<ColoredShape>(std::move(circle), "red");
	const auto semi_trans_red_circle = std::make_unique<TransparentShape>(std::move(red_circle), 128);

	// 动态扩展
	auto square = std::make_unique<Square>(10.0f);
	const auto decorated_square = std::make_unique<TransparentShape>(
		std::make_unique<ColoredShape>(std::move(square), "blue"), 64
	);

	std::cout << semi_trans_red_circle->str() << "\n"; 
	std::cout << decorated_square->str() << "\n"; 
}

这个测试用例将会输出:

A circle of radius 5 with red with 50% transparency
A square of length 10 with blue with 25% transparency

静态装饰器

你是否注意到,在之前的讨论的场景中,我们给Circle提供了一个名为resize()的函数,不过它并不在Shape接口中。你可能已经猜到的,因为它不是Shape成员函数,所以不能从装饰器中调用它。

Circle circle{3};ColoredShape redCircle{circle, "red"};
redCircle.resize(2); // 编译不能通过

假设你并不真正关心是否可以在运行时组合对象,你真正关心的是:能否访问修饰对象的所有字段和成员函数。有可能建造这样一个装饰器吗?

的确有办法实现,而且它是通过模板和继承完成的——但不是那种会导致状态空间爆炸的继承。相反,我们使用一种叫做Mixin继承的方法,类从它自己的模板参数继承。

为此,我们创建一个新的ColoredShape,它继承自一个模板参数。我们没有办法将模板形参限制为任何特定类型,因此我们将使static_assert用进行类型检查。

template <typename T>
struct ColoredShape : T {
	static_assert(is_base_of<Shape, T>::value, "Template argument must be a Shape");
	string color;

	string str() const override {
		ostringstream oss;
		oss << T::str() << "has the color" << color;
		return oss.str();
	}
};

有了ColorredShape<T>TransparentShape<T的实现,我们现在可以把它们组合成一个有颜色的透明形状。

ColoredShape<TransparentShape<Shape>> square{"bule"};
square.size = 2;
square.transparency = 0.5;
cout << square.str();
square.size();

这的确很棒,但并不完美:我们似乎失去了对构造函数的充分使用:即使我们能够初始化最外层的类,我们也不能在一行代码中完全构造具有特定大小、颜色和透明度的形状。

为了锦上添(即装饰!)花,我们给出ColordshapeTransparentShape转发构造函数。这些构造函数将接受两个参数:第一个参数作用于当前模板类,第二个是传递给基类的泛型形参包。

template <typename T>
struct TransparentShape : T {
	uint8_t transparency;

	template <typename... Args>
	TransparentShape(const uint8_t transparency, Args... args):
		T(std::forward<Args>(args)...),
		transparency{transparency} {}
}; // ColoredShape也类似

只是重申一下,前面的构造函数可以接受任意数量的参数,其中第一个参数用于初始化透明值,其余的只是转发给基类的构造函数。

构造函数的数目必须保证是正确的,如果构造函数的数目或值的类型不正确,程序将无法编译。如果开始向类型中添加默认构造函数,那么整体参数集的使用就会变得灵活得多,但也会引入歧义和混淆。

哦,还要确保永远不要显式地使用这些构造函数,否则在组合这些装饰器时,就会违反c++的复制列表初始化规则。现在,如何真正利用这些好处?

ColoredShape2<TransparentShape2<Square>> sq = { "red", 51, 5 };
cout << sq.str() << endl; // A square with side 5 has 20% transparency has the color red

漂亮!这正是我们想要的。这就完成了静态装饰器的实现。同样,你可以对它进行增强,以避免重复类型,如ColorredShape<ColorredShape<...>>,或循环 ColorredShape<TransparentShape<ColorredShape<...>>>;但在静态环境中,这感觉像是浪费时间。不过,多亏了各种形式的模板魔法,这是完全可行的。

函数装饰器

装饰器一般应用于类, 但是同样可以应用于函数. 假定我们有一个需求: 我们想记录以恶函数被调用的情况, 并且在Excel中分析统计数据. 我们可以通过在调用之前和之后添加一些代码来实现这个需求.

cout << "Entering function\n";

// do the work

cout << "Exiting funcion\n";

上面的代码可以看作一个简单的日志记录, 但是关注点分离这一部分来看这个函数做的不好, 我们希望将日志记录存储在某些地方以便重用并且再需要时可以做一些功能增强. 这可以用不同的方法实现.

第一种方法是将整个工作单元作为一个 lambda 提供给日志组件:

#include <functional>
#include <utility>
#include <string>

using namespace std;

struct Logger {
	function<void()> func_;
	string name_;
	Logger(const function<void()>& func, string name):
		func_{func}, name_{std::move(name)} {}

	void operator()() const {
		cout << "Entering" << name_ << endl;
		func_();
		cout << "Exiting" << name_ << endl;
	}
};

可以写这样一个测试函数

void test() {
	Logger([]() {cout << "Hello" << endl; }, "HelloFunction")();
}

将会输出:

Entering HelloFunction

Hello

Exiting HelloFunction

当然, 也可以将函数作为模板而不是一个 std::fuction 对象传入, 做一些改动即可:

void test() {
	auto call = Logger<void>::make_logger([]() { cout << "Hello!" << endl; }, "HelloFunction");
	call();
}

这样一来, 我们就能够创建一个装饰器(内部包含被装饰的函数)了, 现在我们选择的时候我们就能调用它,

之前定义的 function<void()> func 没有入参和返回值, 现在我们有一个新需求是实现带有返回值和函数参数的 add() 函数调用.

// add 是这样的函数
double add(double a, double b) {
	cout << a << "+" << b << "=" << (a + b) << endl;
	return a + b;
}

为此我们要再实现另一个 logger :

#include <functional>
#include <utility>
#include <string>

template <typename R, typename... Args>
struct Logger {
	function<R(Args...)> func_;
	string name_;

	Logger(const function<R(Args...)>& func, string name):
		func_{func}, name_{std::move(name)} {}

	R operator()(Args... args) const {
		cout << "Entering" << name_ << endl;
		R result = func(args...);
		cout << "Exiting" << name_ << endl;

		return result;
	}
};

template <typename R, typename... Args> 
auto make_logger(R (*func)(Args...), const string& name)
{
	return Logger<R(Args...)>{function<R(Args...)>(func),  name }; // () = call now
}

其中 R 是返回值的类型, Args 是变长参数模板. 装饰器将保留该函数并且在必要时调用. 唯一的区别是 operator() 返回一个 R 类型值, 所以返回值不会丢失.

也可以用依赖注入( Dependency Injection)代替make_logger。这种方法的好处是:

  • 通过提供空的对象(Null Object)来动态打开和关闭日志记录,而不是实际的日志对象

  • 禁用被记录的代码的实际调用(同样,通过替换不同的日志对象)


总结

装饰器模式总结

何时需要使用装饰器模式?

装饰器模式的核心应用场景是 动态地为对象添加功能,同时避免因继承导致类层次结构复杂化。具体需求场景包括:

  1. 运行时扩展对象功能:在不修改原有代码的情况下,动态地为一个对象叠加多个功能(如颜色、透明度、日志记录等)。
  2. 避免类爆炸:当需要为对象提供多种功能的排列组合时,继承会导致大量子类(如 RedCircleWithBorderBlueSquareWithTransparency),而装饰器模式通过组合灵活解决这一问题。
  3. 遵循开闭原则:对扩展开放(允许新增装饰器),对修改关闭(不修改原有类)。

装饰器模式解决的核心问题
  1. 继承的局限性
    • 继承是静态的,功能扩展需要在编译时确定。
    • 多层继承会导致类数量激增(组合爆炸)。
  2. 功能复用与解耦
    • 将功能拆分为独立的装饰器,实现单一职责原则。
    • 通过组合动态叠加功能,增强代码灵活性。

与其他设计模式的协同使用

装饰器模式通常与其他模式结合,以解决更复杂的设计问题:

模式协同场景示例
工厂模式创建复杂装饰链时,通过工厂统一管理装饰器和被装饰对象的构造逻辑。使用抽象工厂生成带颜色和透明度的组合对象。
组合模式处理树形结构时,装饰器可为组合中的节点动态添加功能(如文件系统权限装饰)。为文件夹和文件统一添加压缩、加密等装饰。
原型模式通过 clone() 方法实现装饰链的深拷贝,避免原始对象被意外修改。复制一个已装饰的图形对象(如带红色和半透明的圆),生成完全独立的新实例。
策略模式装饰器封装功能扩展,策略模式封装算法选择,两者结合实现功能与算法的动态组合。支付功能装饰器(如日志、验证) + 支付策略(支付宝、微信)。

与其他模式的对比
  • 代理模式
    • 相似性:都通过包装对象间接操作。
    • 区别:代理控制访问(如延迟加载、权限检查),装饰器增强功能。
  • 适配器模式
    • 适配器解决接口不兼容问题,装饰器解决功能扩展问题。

经典应用场景
  1. GUI 组件扩展:为按钮、文本框动态添加边框、阴影、滚动条。
  2. I/O 流处理:Java 中 BufferedInputStreamGZIPInputStream 通过装饰器叠加缓冲和压缩功能。
  3. 中间件与拦截器:Web 框架中通过装饰器链实现请求日志、身份验证、缓存等功能。

总结

装饰器模式是 组合优于继承 的典型实践,适用于需要灵活扩展对象功能的场景。其核心价值在于:

  • 动态添加职责,避免静态继承的僵化。
  • 通过组合实现功能的自由叠加,提升代码复用性。
  • 与工厂、组合、原型等模式协同,构建高扩展性的系统架构。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值