1. 结构型模式在C++中的核心价值
结构型模式的核心在于如何将类或对象组合成更大的结构,同时保持这些结构的灵活性和可扩展性。在C++这种强类型静态语言中,结构型模式尤为重要,因为它们能帮助我们在编译期就建立清晰的类型关系,同时又不失运行时的灵活性。
现代C++(C++11及以后版本)为结构型模式提供了更多实现选择。比如通过智能指针管理对象生命周期,用lambda表达式简化接口适配,以及利用模板元编程在编译期完成部分结构组合工作。这些特性让经典的结构型模式在C++中焕发出新的活力。
2. 适配器模式:老代码与新世界的桥梁
2.1 基本概念与典型场景
适配器模式就像电子设备中的转换插头,让原本接口不兼容的类能够协同工作。在C++中,适配器模式常见于以下场景:
- 将遗留代码接口适配到新系统
- 让第三方库接口符合项目规范
- 统一不同模块间的数据格式
STL中就有许多适配器的经典实现,比如stack和queue本质上就是对底层容器(如deque)的适配。
2.2 现代C++实现方案
传统实现通常采用对象适配器(组合)或类适配器(多继承)。在现代C++中,我们可以有更多选择:
cpp复制// 对象适配器示例
class LegacyRectangle {
public:
void draw(int x1, int y1, int x2, int y2) { /*...*/ }
};
class RectangleAdapter {
LegacyRectangle adaptee;
public:
void draw(int x, int y, int width, int height) {
adaptee.draw(x, y, x + width, y + height);
}
};
// 使用C++17的std::variant实现多接口适配
using Shape = std::variant<CircleAdapter, RectangleAdapter>;
提示:在C++中优先选择对象适配器而非类适配器,因为多重继承容易带来复杂性。如果必须使用类适配器,确保遵循明确的接口继承规范。
2.3 性能考量与实现技巧
适配器模式会引入少量间接调用开销。在性能敏感场景下,可以考虑:
- 将适配器类标记为final,帮助编译器优化
- 使用内联函数减少调用开销
- 对于模板化的适配器,利用编译期多态
3. 装饰器模式:运行时扩展的利器
3.1 模式本质与实现要点
装饰器模式通过嵌套包装的方式,在不修改原有类的情况下动态添加功能。它与继承的主要区别在于:
- 装饰器是运行时行为
- 可以动态组合多个功能
- 避免类爆炸问题
现代C++实现装饰器模式时,可以利用智能指针自动管理装饰链的生命周期:
cpp复制class Component {
public:
virtual ~Component() = default;
virtual void operation() = 0;
};
class ConcreteComponent : public Component {
public:
void operation() override { /* 基础实现 */ }
};
class Decorator : public Component {
std::unique_ptr<Component> wrapped;
public:
explicit Decorator(std::unique_ptr<Component> c) : wrapped(std::move(c)) {}
void operation() override { wrapped->operation(); }
};
class ConcreteDecoratorA : public Decorator {
public:
using Decorator::Decorator;
void operation() override {
Decorator::operation();
addedBehavior();
}
private:
void addedBehavior() { /* 新增功能 */ }
};
3.2 实际应用案例
一个典型应用是I/O流处理。假设我们需要为一个文件读取器添加加密、压缩和缓存功能:
cpp复制auto reader = std::make_unique<CachingDecorator>(
std::make_unique<CompressionDecorator>(
std::make_unique<EncryptionDecorator>(
std::make_unique<FileReader>("data.bin")
)
)
);
这种嵌套结构允许我们灵活组合各种功能,且每个装饰器只需关注自己的单一职责。
3.3 注意事项与陷阱
- 装饰器顺序很重要:加密应该在压缩之前,否则压缩后的数据无法被正确加密
- 避免过度装饰导致调用栈过深,影响性能
- 装饰器接口应该保持最小化,避免暴露不需要的方法
4. 组合模式:树形结构的优雅处理
4.1 统一叶子与容器的接口
组合模式的核心思想是用一致的方式处理单个对象和对象组合。在C++中实现时需要注意:
cpp复制class Graphic {
public:
virtual ~Graphic() = default;
virtual void draw() = 0;
virtual void add(std::unique_ptr<Graphic>) {
throw std::runtime_error("Unsupported operation");
}
};
class Line : public Graphic {
public:
void draw() override { /* 绘制直线 */ }
};
class Picture : public Graphic {
std::vector<std::unique_ptr<Graphic>> children;
public:
void draw() override {
for (auto& child : children)
child->draw();
}
void add(std::unique_ptr<Graphic> g) override {
children.push_back(std::move(g));
}
};
4.2 现代C++优化技巧
- 使用std::variant实现类型安全的组合节点:
cpp复制using GraphicElement = std::variant<Line, Circle, Picture>;
class ModernPicture {
std::vector<GraphicElement> elements;
public:
void draw() {
for (auto& el : elements)
std::visit([](auto& g){ g.draw(); }, el);
}
};
- 考虑使用flyweight模式共享叶子节点的内在状态
4.3 内存管理策略
组合模式常涉及复杂的对象所有权关系。现代C++提供了多种管理方案:
- unique_ptr用于明确的单一所有权
- shared_ptr用于共享所有权
- weak_ptr打破循环引用
在组合结构中,通常父节点拥有子节点的唯一所有权(unique_ptr),而子节点不应持有父节点指针。如果必须引用父节点,应使用原始指针或weak_ptr。
5. 外观模式:复杂系统的简约视图
5.1 设计原则与实现
外观模式为子系统提供统一的高层接口,降低使用复杂度。一个好的外观类应该:
- 了解子系统各组件职责
- 提供最常用的功能组合
- 隐藏不必要的细节但不限制高级功能
cpp复制class HomeTheaterFacade {
Amplifier& amp;
Tuner& tuner;
DvdPlayer& dvd;
//...其他组件
public:
void watchMovie(const std::string& movie) {
amp.on();
amp.setVolume(5);
dvd.on();
dvd.play(movie);
//...其他操作
}
void endMovie() {
dvd.stop();
dvd.off();
amp.off();
//...其他操作
}
};
5.2 与中介者模式的区别
外观模式与中介者模式都涉及对多个组件的协调,但关键区别在于:
- 外观模式是单向的:客户端通过外观使用子系统
- 中介者模式是多向的:各组件通过中介者相互通信
5.3 性能优化建议
- 避免在外观方法中创建临时对象,尽量复用现有资源
- 对于频繁调用的外观方法,考虑使用完美转发减少拷贝
- 在多线程环境中,确保外观类提供适当的线程安全保证
6. 桥接模式:解耦抽象与实现
6.1 模式动机与实现
桥接模式解决类爆炸问题,将抽象部分与实现部分分离,使它们可以独立变化。典型应用场景包括:
- 跨平台UI开发
- 驱动程序设计
- 支持多种后端的服务
cpp复制class WindowImpl {
public:
virtual ~WindowImpl() = default;
virtual void drawText() = 0;
virtual void drawRect() = 0;
};
class XWindowImpl : public WindowImpl {
// X11系统具体实现
};
class WindowsWindowImpl : public WindowImpl {
// Windows系统具体实现
};
class Window {
std::unique_ptr<WindowImpl> impl;
public:
explicit Window(std::unique_ptr<WindowImpl> p) : impl(std::move(p)) {}
virtual void draw() = 0;
protected:
WindowImpl& getImpl() { return *impl; }
};
class TransientWindow : public Window {
public:
using Window::Window;
void draw() override {
getImpl().drawRect();
getImpl().drawText();
}
};
6.2 现代C++实现变体
利用模板可以在编译期确定实现类型:
cpp复制template<typename Impl>
class WindowTemplate {
Impl impl;
public:
void draw() {
impl.drawRect();
impl.drawText();
}
};
这种静态桥接方式牺牲了一些运行时的灵活性,但能获得更好的性能。
6.3 设计考量
- 确定真正的变化维度:只有那些确实需要独立变化的维度才值得用桥接模式
- 接口设计要足够稳定:抽象接口和实现接口一旦确定,后续修改成本较高
- 考虑使用pimpl惯用法作为桥接模式的简化形式
7. 享元模式:内存优化的艺术
7.1 内在状态与外在状态
享元模式通过共享相同内在状态的对象来减少内存使用。关键是将对象属性分为:
- 内在状态:可共享的、不变的部分
- 外在状态:不可共享的、变化的部分
cpp复制class Font {
std::string name;
int size;
bool bold;
//...其他内在属性
public:
Font(std::string name, int size, bool bold)
: name(std::move(name)), size(size), bold(bold) {}
//...其他方法
};
class FontFactory {
std::map<std::tuple<std::string, int, bool>, std::shared_ptr<Font>> pool;
public:
std::shared_ptr<Font> getFont(const std::string& name, int size, bool bold) {
auto key = std::make_tuple(name, size, bold);
if (!pool.count(key))
pool[key] = std::make_shared<Font>(name, size, bold);
return pool[key];
}
};
7.2 线程安全实现
享元工厂通常需要是线程安全的:
cpp复制class ThreadSafeFontFactory {
std::mutex mtx;
std::map</*...*/> pool;
public:
std::shared_ptr<Font> getFont(/*...*/) {
std::lock_guard<std::mutex> lock(mtx);
//...原有逻辑
}
};
或者使用C++11的std::call_once:
cpp复制std::shared_ptr<Font> getFont(/*...*/) {
std::call_once(flag, [&]{
// 初始化代码
});
return pool[key];
}
7.3 与其他模式的结合
- 与组合模式结合:共享叶子节点的内在状态
- 与装饰器模式结合:装饰器可以持有享元对象的共享指针
- 与工厂模式结合:通过工厂管理享元对象的创建和共享
8. 结构型模式综合应用实例
8.1 图形编辑器设计
让我们设计一个支持多种图形元素、可扩展的图形编辑器框架:
cpp复制// 基础组件接口
class Graphic {
public:
virtual ~Graphic() = default;
virtual void draw() const = 0;
virtual std::unique_ptr<Graphic> clone() const = 0;
};
// 享元工厂管理共享属性
class AttributeFactory {
std::unordered_map<std::string, std::shared_ptr<Attribute>> pool;
public:
std::shared_ptr<Attribute> getAttribute(const std::string& key);
};
// 具体图形元素
class Circle : public Graphic {
std::shared_ptr<Attribute> attr; // 共享属性
Point center; // 外部状态
double radius;
public:
void draw() const override;
std::unique_ptr<Graphic> clone() const override;
};
// 组合图形
class CompositeGraphic : public Graphic {
std::vector<std::unique_ptr<Graphic>> children;
public:
void add(std::unique_ptr<Graphic> g);
void draw() const override;
std::unique_ptr<Graphic> clone() const override;
};
// 装饰器:为图形添加边框
class BorderDecorator : public Graphic {
std::unique_ptr<Graphic> wrapped;
BorderStyle style;
public:
void draw() const override;
std::unique_ptr<Graphic> clone() const override;
};
8.2 性能优化策略
- 使用连续内存存储图形元素,提高缓存命中率
- 对不变的部分使用享元模式
- 利用多线程并行绘制独立的图形子树
- 实现延迟加载和按需绘制
8.3 扩展性设计
- 通过装饰器模式动态添加新功能
- 使用桥接模式支持不同的渲染后端
- 通过适配器模式兼容第三方图形库
- 外观模式提供简化的高级操作接口
在实际C++项目中,结构型模式很少单独使用,通常需要根据具体问题组合多种模式。理解每种模式的适用场景和权衡取舍,才能设计出既灵活又高效的软件架构。
