一、什么是设计模式?为什么学它?

✅ 定义

设计模式(Design Patterns) 是在特定上下文中解决常见软件设计问题的可复用的解决方案模板。它不是可以直接复制粘贴的代码,而是经过验证的设计思想和最佳实践

“软件设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过 分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码 更容易被他人理解并且保证代码可靠性。”

我们可用一句大白话:“在一定环境下,用固定套路解决问题。”

✅ 为什么学习设计模式(尤其对 C++ 开发者)?

好处 说明
提高代码质量 写出更清晰、模块化、低耦合、高内聚的代码
增强可维护性 易于修改、扩展和调试
提升沟通效率 团队中可以用“策略模式”、“观察者”等术语快速交流设计意图
避免重复造轮子 前人已总结出成熟解决方案,直接借鉴
理解大型框架 STL、Boost、Qt、游戏引擎等广泛使用设计模式

二、GoF 的 23 种设计模式分类

1994 年,Erich Gamma 等四位作者(GoF)在《设计模式:可复用面向对象软件的基础》中提出 23 种经典模式,分为三类:

1. 创建型模式(Creational Patterns)

关注:如何创建对象,隐藏创建细节

模式 核心思想 典型应用场景
单例(Singleton) 保证一个类只有一个实例 日志器、配置管理、线程池
工厂方法(Factory Method) 子类决定创建哪个对象 框架中让子类扩展产品类型
抽象工厂(Abstract Factory) 创建一系列相关对象 跨平台 UI 组件(WinButton, MacButton)
建造者(Builder) 分步构建复杂对象 构造配置复杂的对象(如 HTTP 请求)
原型(Prototype) 通过克隆创建新对象 对象创建成本高,需大量相似实例

2. 结构型模式(Structural Patterns)

关注:类或对象如何组合成更大的结构

模式 核心思想 典型应用场景
适配器(Adapter) 转换接口,兼容不匹配的类 集成第三方库、旧系统升级
装饰器(Decorator) 动态添加功能,替代继承 I/O 流包装(BufferedStream)、UI 效果叠加
代理(Proxy) 控制对对象的访问 远程代理、虚拟代理、保护代理
外观(Facade) 提供统一高层接口 简化复杂子系统调用(如游戏引擎启动)
桥接(Bridge) 抽象与实现分离,独立变化 图形渲染 API + 不同平台实现
组合(Composite) 树形结构,统一处理个体与整体 文件系统、菜单树、场景图
享元(Flyweight) 共享细粒度对象,节省内存 文本编辑器中的字符格式、游戏中的子弹纹理

3. 行为型模式(Behavioral Patterns)

关注:对象间的职责分配与通信机制

模式 核心思想 典型应用场景
观察者(Observer) 一对多依赖,状态变化自动通知 事件系统、MVC 模式、GUI 通知
策略(Strategy) 封装算法,可动态切换 排序算法、支付方式、AI 行为
命令(Command) 请求封装为对象 撤销/重做、任务队列、远程调用
状态(State) 状态改变行为随之改变 有限状态机(游戏角色状态)
模板方法(Template Method) 父类定义流程,子类实现步骤 框架中固定算法骨架(如测试框架)
迭代器(Iterator) 统一访问聚合对象元素 遍历容器,隐藏内部结构
中介者(Mediator) 减少对象间直接引用 复杂 UI 组件交互、聊天室
备忘录(Memento) 捕获并恢复对象状态 撤销操作、游戏存档
访问者(Visitor) 操作与数据结构分离 编译器 AST 遍历、报表生成

三、设计模式基本原则(面向对象设计基石)

最终目的:高内聚,低耦合;所有设计模式本质上都是为了更好地实践这些设计原则。

1)  开放封闭原则  (OCP,Open For Extension, Closed For Modification Principle)

类的改动是通过增加代码进行的,而不是修改源代码。

2)  单一职责原则  (SRP,Single Responsibility Principle)

类的职责要单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。

3)   依赖倒置原则 (DIP,Dependence Inversion Principle)

依赖于抽象(接口),不要依赖具体的实现(类),也就是针对接口编程。

4)   接口隔离原则 (ISP,Interface Segegation Principle)

不应该强迫客户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有操作都封装到一个接口中去。

5)   里氏替换原则 (LSP, Liskov Substitution Principle)

  任何抽象类出现的地方都可以用他的实现类进行替换。实际就是虚拟机制,语言级别实现面向对象功能。

6)   优先使用组合而不是继承原则(CARP,Composite/Aggregate Reuse Principle)

如果使用继承,会导致父类的任何变换都可能影响到子类的行为。

如果使用对象组合,就降低了这种依赖关系。

7)  迪米特法则(LOD,Law of Demeter)

一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。例如在一个程序中,各个模块之间相互调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解另外一个模块的内部实现细节,这样当一个模块内部的实现发生改变时,不会影响其他模块的使用。(黑盒原理)

1. 单一职责原则(SRP: Single Responsibility Principle)

定义:一个类应该只有一个引起它变化的原因,即只负责一项职责。

📌 通俗理解:一个类只做一件事,做好这件事。

❌ 反例(违反 SRP)
class User {
public:
    void setName(const std::string& name) { this->name = name; }
    void saveToDatabase();        // 职责1:业务数据管理
    void sendEmail(const std::string& msg); // 职责2:网络通信
private:
    std::string name;
};

👉 User 类既管数据又管发邮件,如果数据库结构或邮件协议变了,都要改这个类。

✅ 正确做法
class User {
    // 只负责用户数据
};

class UserRepository {
public:
    void save(const User& user); // 负责持久化
};

class EmailService {
public:
    void send(const std::string& to, const std::string& msg); // 负责发邮件
};

2. 开闭原则(OCP: Open/Closed Principle)

定义:对扩展开放,对修改关闭

📌 通俗理解:当需求变化时,应通过添加新代码来实现,而不是修改已有代码。

✅ 示例:使用多态实现 OCP
class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape {
    double r;
public:
    double area() const override { return 3.14 * r * r; }
};

class Rectangle : public Shape {
    double w, h;
public:
    double area() const override { return w * h; }
};

// 计算总面积的函数无需修改
double totalArea(const std::vector<Shape*>& shapes) {
    double sum = 0;
    for (auto* s : shapes) sum += s->area();
    return sum;
}

👉 新增 Triangle 类时,只需新增类,无需修改 totalArea 函数。


3. 里氏替换原则(LSP: Liskov Substitution Principle)

定义:子类对象必须能够替换其父类对象,而程序行为不变。

📌 通俗理解:继承不是为了复用代码,而是为了多态;子类不能“破坏”父类的行为契约。

❌ 反例(违反 LSP)
class Rectangle {
protected:
    int width, height;
public:
    virtual void setWidth(int w) { width = w; }
    virtual void setHeight(int h) { height = h; }
    int area() const { return width * height; }
};

class Square : public Rectangle {
public:
    void setWidth(int w) override { width = height = w; }
    void setHeight(int h) override { width = height = h; }
};

👉 如果函数期望一个 Rectangle,传入 Square 可能导致面积计算错误(因为宽高联动)。

✅ 解决方案

重构继承关系,或使用组合。


4. 接口隔离原则(ISP: Interface Segregation Principle)

定义:客户端不应依赖它不需要的接口。应将大接口拆分为多个小接口。

📌 通俗理解:不要强迫实现类实现它用不到的方法。

❌ 反例
class Machine {
public:
    virtual void print() = 0;
    virtual void scan() = 0;
    virtual void fax() = 0;
};

class OldPrinter : public Machine {
public:
    void print() override { /* OK */ }
    void scan() override { throw std::runtime_error("Not supported!"); } // 强迫实现
    void fax() override { throw std::runtime_error("Not supported!"); }
};
✅ 正确做法
struct Printable {
    virtual void print() = 0;
};

struct Scannable {
    virtual void scan() = 0;
};

struct Faxable {
    virtual void fax() = 0;
};

class ModernPrinter : public Printable, public Scannable {
public:
    void print() override { /* ... */ }
    void scan() override { /* ... */ }
};

5. 依赖倒置原则(DIP: Dependency Inversion Principle)

定义

  • 高层模块不应依赖低层模块,二者都应依赖抽象。
  • 抽象不应依赖细节,细节应依赖抽象。

📌 通俗理解:面向接口编程,而不是面向实现编程。

❌ 反例(高层依赖低层)
class MySQLDatabase {
public:
    void connect() { /* MySQL 连接 */ }
};

class UserService {
    MySQLDatabase db; // 直接依赖具体实现
public:
    void addUser() {
        db.connect();
        // ...
    }
};

👉 如果换为 PostgreSQL,UserService 必须修改。

✅ 正确做法(依赖抽象)
class Database {
public:
    virtual void connect() = 0;
    virtual ~Database() = default;
};

class MySQLDatabase : public Database { /* 实现 */ };
class PostgreSQLDatabase : public Database { /* 实现 */ };

class UserService {
    Database* db; // 依赖抽象
public:
    UserService(Database* d) : db(d) {}
    void addUser() { db->connect(); }
};

👉 通过构造函数注入依赖,符合 DIP,也便于测试(可用 Mock)。

6. 迪米特法则(LoD: Law of Demeter) / 最少知识原则

定义:一个对象应该对其他对象保持最少的了解。只和“朋友”说话。

📌 通俗理解:不要“链式调用”过深,如 a.getB().getC().doSomething() 应避免。

✅ 示例
// ❌ 违反
user.getWallet().getBalance().getValue();

// ✅ 改进
user.getBalance(); // 封装细节


7. 合成/聚合复用原则(CARP: Composite Reuse Principle)

定义:尽量使用组合/聚合,而不是继承来达到复用的目的。

📌 通俗理解:优先用“有一个”(has-a),而不是“是一个”(is-a)。

✅ 示例
class Engine {
public:
    void start() { /* 启动引擎 */ }
};

class Car {
    Engine engine; // 组合
public:
    void start() { engine.start(); }
};

👉 比继承 Engine 更灵活,易于更换引擎类型。


四、设计原则与设计模式的关系

设计模式 主要遵循的原则
单例模式 SRP(单一职责)
工厂模式 DIP、OCP(依赖抽象,易于扩展)
策略模式 OCP、DIP(算法可替换)
观察者模式 DIP、OCP(观察者依赖抽象)
装饰器模式 OCP(动态扩展功能)
适配器模式 DIP、ISP(转换接口)

🔁 所有设计模式本质上都是为了更好地实践这些设计原则。

五、C++ 实现设计模式的关键特性支持

C++ 的强大之处在于它能以多种方式实现同一模式,灵活性极高:

C++ 特性 在设计模式中的应用
虚函数 / 多态 实现运行时多态(如工厂、策略、命令)
模板(泛型) 实现编译时多态,性能更高(策略模式可用模板)
智能指针 自动管理对象生命周期(避免单例内存泄漏)
std::function & lambda 简化命令、策略模式的实现
RAII 确保资源安全释放(如代理模式中的锁)
运算符重载 增强接口一致性(如迭代器)


 

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐