前面我们已经利用过一些设计模式来写过小项目,但是那个时候我们对于设计模式还不了解,本期我们来深入讲解设计模式相关的知识
相关代码已经提交到gitee:设计模式: 本期代码主要是介绍编程中相关的设计模式及其简单地代码示例(以C++语言为主)
目录
[结构型模式:解决类 / 对象的组合与适配痛点](#结构型模式:解决类 / 对象的组合与适配痛点)
设计模式的分类
设计模式按解决的问题分为创建型、结构型、行为型三大类,对应不同的业务与技术痛点场景,同时有通用的适用边界。
创建型模式:解决对象创建的痛点
核心目标:解耦对象的创建与使用,灵活控制实例化逻辑,屏蔽创建细节,适配复杂的对象生成场景。
| 核心模式 | 典型适用场景 |
|---|---|
| 单例模式 | 全局仅需唯一实例的场景,如系统配置管理器、数据库连接池、日志管理器、全局事件总线,避免重复创建资源浪费,保证全局状态一致 |
| 工厂模式(工厂方法 / 抽象工厂) | 对象创建逻辑复杂、需根据场景动态生成不同实现、不想让调用者感知创建细节的场景,如多支付渠道适配、多数据源切换、跨平台组件生成,新增实现无需修改调用方代码 |
| 建造者模式 | 复杂对象构建(多参数、多步骤、不可变对象)的场景,如订单实体、用户详情、报表配置对象,解决构造器参数爆炸问题,通过链式调用清晰控制构建过程 |
| 原型模式 | 对象创建成本极高、需快速复制已有实例的场景,如大对象克隆、游戏角色模板复用、复杂配置对象的批量生成,避免重复初始化开销 |
结构型模式:解决类 / 对象的组合与适配痛点
核心目标:优化类与对象的结构关系,解决接口不兼容、继承泛滥、子系统复杂度高、功能灵活组合等问题。
| 核心模式 | 典型适用场景 |
|---|---|
| 代理模式 | 需控制对象访问、增强核心功能、延迟加载的场景,如 Spring AOP 的事务 / 权限 / 日志切面、RPC 远程调用代理、图片懒加载虚拟代理、敏感接口的访问控制 |
| 适配器模式 | 需兼容不兼容接口、复用现有能力但接口不匹配的场景,如第三方 SDK 适配、老系统接口对接新系统、多数据源统一操作接口、不同日志框架的统一适配 |
| 装饰器模式 | 需动态给对象增减功能、避免继承层级泛滥的场景,如 Java IO 流体系、订单优惠规则叠加(满减 / 折扣 / 优惠券)、接口请求的加解密 / 压缩功能动态组合 |
| 组合模式 | 需处理树形层级结构、统一处理整体与部分的场景,如文件系统、组织架构、菜单权限树、电商商品分类树,无需区分单个节点与组合节点的处理逻辑 |
| 外观模式(门面模式) | 需封装复杂子系统、对外提供极简统一入口的场景,如电商下单流程(封装库存 / 支付 / 物流 / 消息子系统)、SDK 接口封装、复杂微服务的聚合接口,大幅降低调用方的使用成本 |
行为型模式:解决对象间通信与职责分配痛点
核心目标:解耦对象间的交互逻辑,灵活分配职责,规范算法与流程的扩展,解决复杂的状态流转、多对象联动等问题。
| 核心模式 | 典型适用场景 |
|---|---|
| 观察者模式(发布 - 订阅) | 一个对象状态变更需同步通知多个依赖对象、解耦发布者与订阅者的场景,如 UI 事件监听、消息队列、事件总线、订单支付成功后同步通知库存 / 物流 / 积分 / 推送模块 |
| 策略模式 | 同一场景有多种可切换的算法 / 规则、需消除大量 if-else 分支的场景,如支付优惠策略、物流配送方案、排序算法动态选择、风控规则引擎,新增规则无需修改原有核心逻辑 |
| 状态模式 | 对象行为随状态动态变化、存在大量状态判断分支的场景,如订单全生命周期状态流转(待支付 / 已支付 / 待发货 / 已完成 / 已取消)、工单审批状态、用户会员等级行为控制 |
| 责任链模式 | 请求需经过多个处理节点依次处理、解耦请求发送者与处理者的场景,如网关过滤器链(鉴权 / 限流 / 参数校验 / 日志)、多级审批流程、异常处理链、敏感词过滤链路 |
| 模板方法模式 | 固定流程骨架、仅部分步骤可变的场景,如审批通用流程、报表生成流程、自动化测试用例执行流程,通用步骤在父类固化,可变步骤由子类自定义实现 |
通用适用边界
以下场景优先考虑使用设计模式,避免过度设计:
- 中长期迭代、需求频繁变更的项目:设计模式的核心是 "应对变化",通过分离稳定与易变逻辑,大幅降低需求变更的改造成本;
- 团队协作开发:设计模式是研发团队的通用 "设计语言",统一的设计范式可大幅降低沟通成本,提升新人接手代码的效率;
- 框架 / 组件 / SDK 开发:需对外屏蔽内部实现、提供高扩展性、高复用性的底层能力,几乎所有主流开源框架都深度依赖设计模式;
- 遗留代码重构:当代码出现大量 if-else 分支、强耦合、重复代码、继承层级臃肿、难以扩展等问题时,设计模式是重构的核心抓手。、
前置知识
Mixin继承
Mixin(混入)继承 是一种代码复用的设计思想 :把单一、独立、可复用的小功能 封装成一个 Mixin 类,像插件 一样混入 到目标类中,让目标类直接获得这个功能,不需要写重复代码。
大概结构就像下面这样:
cpp
template <typename T>
struct Mixin : public T
{
void mixinFunction()
{
// Implementation of the mixin function
}
};
它允许不同类型分层组合,可以实例化处类似Foo<Bar<Baz>> x;的对象,实现出3个类的特征
cpp
#include <iostream>
#include <string>
// 1. 基础业务类(核心功能)
class Base
{
public:
virtual void show() const
{
std::cout << "Base: 核心业务逻辑\n";
}
virtual ~Base() = default;
};
// 2. Mixin 类 1:添加日志功能
class LogMixin
{
public:
void log(const std::string& msg) const
{
std::cout << "[LOG] " << msg << "\n";
}
};
// 3. Mixin 类 2:添加序列化功能
class SerializeMixin
{
public:
std::string serialize(const std::string& data) const
{
return "[Serialized] " + data;
}
};
// 4. 组合 Mixin:通过多重继承扩展功能
template <typename... Mixins>
class BusinessClass : public Base, public Mixins...
{
private:
std::string data;
public:
BusinessClass(const std::string& d) : data(d) {}
// 重写核心业务,同时使用 Mixin 功能
void show() const override
{
// 调用 LogMixin 的 log()
this->log("执行 show() 方法");
// 调用 SerializeMixin 的 serialize()
std::cout << this->serialize(data) << "\n";
// 调用基类的 show()
Base::show();
}
};
int main()
{
// 组合 LogMixin 和 SerializeMixin
BusinessClass<LogMixin, SerializeMixin> obj("Hello Mixin!");
obj.show();
return 0;
}
奇异递归模板模式
奇异递归模板模式(Curiously Recurring Template Pattern,CRTP) 是 C++ 中一种经典的模板编程惯用法(Idiom) ,是C++中实现Mixin继承的主要方式之一。其核心特征是派生类将自身作为模板参数传递给基类,从而在编译期建立基类与派生类的类型关联,实现静态多态、功能复用等效果。它的基本结构是这样的
cpp
// 基类:模板类,接受派生类作为模板参数
template <typename Derived>
class Base
{
// 基类可通过 Derived 类型访问派生类成员
};
// 派生类:继承自基类,并将自身作为模板参数传递
class Derived : public Base<Derived>
{
// 派生类实现具体逻辑
};
相比普通 Mixin(多重继承),CRTP 有以下独特优势:
- 零运行时开销:通过编译期静态分发替代虚函数,无虚表 / 虚指针成本,性能接近直接调用。
- 静态多态:基类可调用派生类方法("向下调用"),且在编译期完成类型检查,更安全。
- 代码复用更灵活:基类可基于派生类类型生成定制化代码,避免重复逻辑。
- 无菱形继承风险:通过模板而非多重继承组合功能,结构更清晰。
代码示例:
cpp
#include <iostream>
// 1. CRTP 基类:提供对象计数功能
template <typename Derived>
class CountableMixin
{
protected:
// 静态数据:记录派生类对象总数
static inline int count = 0;
CountableMixin() { ++count; }
CountableMixin(const CountableMixin&) { ++count; }
~CountableMixin() { --count; }
public:
// 静态接口:获取当前对象数
static int getObjectCount() { return count; }
};
// 2. CRTP 基类:提供方法调用统计功能
template <typename Derived>
class LoggableMixin
{
protected:
int callCount = 0;
// 基类调用派生类方法(静态多态)
void logAndExecute(const std::string& methodName)
{
++callCount;
std::cout << "[LOG] " << methodName << " 被调用 (总计: " << callCount << "次)\n";
// 关键:CRTP 让基类可直接调用派生类的实现
static_cast<Derived*>(this)->doWork();
}
};
// 3. 派生类:组合两个 CRTP Mixin
class MyBusinessClass :
public CountableMixin<MyBusinessClass>, // 继承计数功能
public LoggableMixin<MyBusinessClass> // 继承日志功能
{
// 让基类 LoggableMixin 可访问派生类的 doWork()
friend class LoggableMixin<MyBusinessClass>;
private:
// 派生类的核心业务逻辑
void doWork()
{
std::cout << "MyBusinessClass: 执行实际业务...\n";
}
public:
// 对外接口,通过 Mixin 增强功能
void execute()
{
logAndExecute("execute()"); // 调用 LoggableMixin 的方法
}
};
int main()
{
// 测试对象计数
{
MyBusinessClass obj1;
MyBusinessClass obj2;
std::cout << "当前对象数: " << MyBusinessClass::getObjectCount() << "\n\n";
// 输出:当前对象数: 2
}
std::cout << "离开作用域后对象数: " << MyBusinessClass::getObjectCount() << "\n\n";
// 输出:离开作用域后对象数: 0
// 测试方法调用统计
MyBusinessClass obj;
obj.execute();
obj.execute();
obj.execute();
// 输出:
// [LOG] execute() 被调用 (总计: 1次)
// MyBusinessClass: 执行实际业务...
// [LOG] execute() 被调用 (总计: 2次)
// MyBusinessClass: 执行实际业务...
// [LOG] execute() 被调用 (总计: 3次)
// MyBusinessClass: 执行实际业务...
return 0;
}
SOLID原则
单一职责原则
单一职责原则就是说:每个类只承担一个职责,也只有一个修改该类的原因。
违反该原则的极端情况就是------上帝对象。上帝对象是指承担了尽可能多的庞大的类,修改它就会变得极其困难
我们以一个代码来示例:
假如我们要通过日记来记录每天的想法,包含标题和记录,我们可以这么写
cpp
struct Journal
{
string title;
vector<string> entries;
explicit Journal(const string& title)
: title(title)
{}
};
现在我们要有一个添加记录的方法
cpp
void addEntry(const string& entry)
{
static int count = 1;
entries.emplace_back(to_string(count++) + ": " + entry);
}
添加记录对于日记是有意义的。
但是如果我们添加一个保存文件的方法
cpp
void save(const string& filename) const
{
// 这里我们简单地模拟保存到文件的操作
ofstream ofs(filename);
if (ofs.is_open())
{
for (const auto& entry : entries)
{
ofs << entry << "\n";
}
}
else
{
cout << "Failed to open file: " << filename << "\n";
}
}
这样就会诞生一个问题:Journal类只是承担日记任务,不承担保存持久化任务。这样后续对于这个保存持久化做任何改动(如可选保存格式或保存云端或本地)都需要去改方法修改。
在实际工程项目中,如果频繁对大量的类做小修改,会导致"代码异味"的诞生。这是很痛苦的。
对于保存持久化,我们需要单独设计。
cpp
//单独为了持久化保存设计的类
struct PersistenceManager
{
static void save(const Journal& journal, const string& filename)
{
ofstream ofs(filename);
if (ofs.is_open())
{
for (const auto& entry : journal.entries)
{
ofs << entry << "\n";
}
}
else
{
cout << "Failed to open file: " << filename << "\n";
}
}
};
struct Journal
{
string title;
vector<string> entries;
explicit Journal(const string& title)
: title(title)
{}
void addEntry(const string& entry)
{
static int count = 1;
entries.emplace_back(to_string(count++) + ": " + entry);
}
void save(const string& filename) const
{
// 方法二:将保存功能委托给 PersistenceManager 类,遵循单一职责原则
PersistenceManager::save(*this, filename);
}
};
开闭原则
开闭原则就是说:对于一个软件来说,良好的设计应当是对于新的需求尽可能是扩展代码而不是修改已有代码(可能引入不稳定因素导致bug或其他风险)
我们接下来以代码示例来讲解一下:
假设我们有一批产品,每个产品颜色和尺寸不同。
cpp
enum class Color
{
Red,
Green,
Blue
};
enum class Size
{
Small,
Medium,
Large
};
struct Product
{
string name;
Color color;
Size size;
};
现在我们需要为产品提供过滤能力,并根据颜色过滤
cpp
struct ProductFilter
{
using Items=vector<Product*>;
static Items by_color(Items items, Color color)
{
Items result;
for (auto& i : items)
{
if (i->color == color)
{
result.push_back(i);
}
}
return result;
}
};
过一段时间,又有新要求要求以尺寸过滤
cpp
static Items by_size(Items items, Size size)
{
Items result;
for (auto& i : items)
{
if (i->size == size)
{
result.push_back(i);
}
}
return result;
}
我们发现代码如此的相似,这时你可能会想到以bool类型为主的std::function来替换。但是实际来说并没有那么简单。因为新增的需求中,可能有些要求被索引,有些没有;有一些要求保存到云端,有一些保存到本地。这些内容过于复杂,难以概况。
如果这时再添加一个同时根据大小和颜色筛选,那仅仅只是加一个接口吗?
那么我们如何在这里实现开闭原则呢?这里我们就要用到单一职责原则,将过滤器分为过滤器和规范两部分
cpp
//规范
template <typename T>
struct Specification
{
virtual bool is_satisfied(T* item) const = 0;
};
//颜色规范
struct ColorSpecification : Specification<Product>
{
Color color;
explicit ColorSpecification(Color color) : color(color) {}
bool is_satisfied(Product* item) const override
{
return item->color == color;
}
};
//过滤器
template <typename T>
struct Filter
{
virtual vector<T*> filter(vector<T*> items, Specification<T>* spec) const = 0;
};
//更好的过滤器
struct BetterFilter : Filter<Product>
{
vector<Product*> filter(vector<Product*> items, Specification<Product>* spec) const override
{
vector<Product*> result;
for (auto& i : items)
{
if (spec->is_satisfied(i))
{
result.push_back(i);
}
}
return result;
}
};
void Test1()
{
Product apple{"Apple", Color::Green, Size::Small};
Product tree{"Tree", Color::Green, Size::Large};
Product house{"House", Color::Blue, Size::Large};
vector<Product*> items{&apple, &tree, &house};
BetterFilter bf;
ColorSpecification green(Color::Green);
auto green_things = bf.filter(items, &green);
for (auto& x : green_things)
{
cout << "绿色的: " << x->name << "\n";
}
}
这里我们就明白了之前的混合过滤怎么写了
cpp
//混合规范
struct AndSpecification : Specification<Product>
{
Specification<Product>& first;
Specification<Product>& second;
AndSpecification(Specification<Product>& first, Specification<Product>& second)
: first(first), second(second)
{}
bool is_satisfied(Product* item) const override
{
return first.is_satisfied(item) && second.is_satisfied(item);
}
};
我们也可以优化一下,对于两个Specification<T>对象,可以重载&&,这样创建多个规范组合会更加简洁
里氏替换原则
里氏替换原则是由Barbara Liskov指出的,它表明:如果一个程序能够接受基类Parent为参数,那么它必须能接受Parent的派生类Child且不存在任何异常
这里对于子类和父类是有要求的------子类需遵守父类的 "契约"
- 前置条件(输入要求)不能比父类更严格;
- 后置条件(输出结果)不能比父类更宽松;
- 不变式(类的固有属性)必须保持。
这里有一个很经典的模型:正方形继承矩形
cpp
//里氏替换原则(Liskov Substitution Principle)
#include <iostream>
// 父类:长方形
class Rectangle
{
protected:
double width, height;
public:
virtual void setWidth(double w) { width = w; }
virtual void setHeight(double h) { height = h; }
double getWidth() const { return width; }
double getHeight() const { return height; }
double area() const { return width * height; }
};
// 子类:正方形(违反 LSP)
class Square : public Rectangle
{
public:
void setWidth(double w) override
{
width = w;
height = w; // 强制宽高一致,破坏父类契约
}
void setHeight(double h) override
{
width = h;
height = h; // 强制宽高一致,破坏父类契约
}
};
// 测试函数:期望长方形的宽高可独立设置
void testRectangle(Rectangle& rect)
{
rect.setWidth(3);
rect.setHeight(4);
std::cout << "期望面积: 12, 实际面积: " << rect.area() << "\n";
}
int main()
{
Rectangle r;
testRectangle(r); // 输出:期望面积: 12, 实际面积: 12(正确)
Square s;
testRectangle(s); // 输出:期望面积: 12, 实际面积: 16(错误!)
return 0;
}
最佳实现是利用组合:
cpp
// 抽象基类:形状
// 为 Shape 基类添加更多通用方法
class Shape
{
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
// 为需要宽高操作的形状创建一个专门的接口
class ResizableShape : public Shape
{
public:
virtual void setWidth(double w) = 0;
virtual void setHeight(double h) = 0;
virtual double getWidth() const = 0;
virtual double getHeight() const = 0;
};
class Rectangle : public ResizableShape
{
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
void setWidth(double w) override { width = w; }
void setHeight(double h) override { height = h; }
double getWidth() const override { return width; }
double getHeight() const override { return height; }
double area() const override { return width * height; }
};
// 正方形不适合 ResizableShape 接口,因为它的宽度和高度必须相等
class Square : public Shape
{
private:
double side;
public:
Square(double s) : side(s) {}
void setSide(double s) { side = s; }
double area() const override { return side * side; }
};
// 测试函数:期望长方形的宽高可独立设置
int main()
{
Rectangle r(3, 4);
std::cout << "期望面积: 16, 实际面积: " << r.area() << "\n"; // 输出:期望面积: 16, 实际面积: 16(正确)
Square s(4);
// testRectangle(s); // 这行会编译错误,因为 Square 不是 ResizableShape
std::cout << "期望面积: 16, 实际面积: " << s.area() << "\n"; // 输出:期望面积: 16, 实际面积: 16(正确)
return 0;
}
接口隔离原则
接口隔离原则是说:建议将所有接口拆分开,让是闲着根据需求挑选接口并实现。
比如我们要实现一个打印机
cpp
struct Document
{
string name;
// 其他文档相关属性和方法
};
struct IMachine
{
virtual void print(vector<Document>& docs) = 0;
virtual void scan(vector<Document>& docs) = 0;
virtual void fax(vector<Document>& docs) = 0;
virtual ~IMachine() = default;
};
struct MyFavoritePrinter : IMachine
{
void print(vector<Document>& docs) override
{
}
void scan(vector<Document>& docs) override
{
}
void fax(vector<Document>& docs) override
{
}
};
这样存在的问题是如果实现者只需要打印功能而不需要扫描问题或者传真功能,这样就会有些累赘。
我们可以这么修改
cpp
struct IPrinter
{
virtual void print(vector<Document>& docs) = 0;
virtual ~IPrinter() = default;
};
struct IScanner
{
virtual void scan(vector<Document>& docs) = 0;
virtual ~IScanner() = default;
};
struct IFax
{
virtual void fax(vector<Document>& docs) = 0;
virtual ~IFax() = default;
};
struct MyPrinter : IPrinter
{
void print(vector<Document>& docs) override
{
}
};
struct MyScanner : IScanner
{
void scan(vector<Document>& docs) override
{
}
};
struct MyMachine : IPrinter, IScanner
{
MyPrinter printer;
MyScanner scanner;
MyMachine() : printer(), scanner() {}
void print(vector<Document>& docs) override
{
printer.print(docs);
}
void scan(vector<Document>& docs) override
{
scanner.scan(docs);
}
};
依赖倒转原则
依赖倒转原则原始定义如下:
1、高层模块不依赖低层模块,两者均应该依赖抽象接口
2、抽象接口不应依赖细节,细节应依赖抽象接口
我们接下来举例说明:
我们有一个小车,小车有引擎有行车日志。大概实现如下:
cpp
// 抽象引擎接口
struct IEngine
{
virtual float getVolume() const = 0;
virtual int getHorsePower() const = 0;
virtual ~IEngine() = default;
friend ostream& operator<<(ostream& os, const IEngine& e) {
return os << "Engine: " << e.getVolume() << "L, " << e.getHorsePower() << "HP";
}
};
// 具体引擎实现
struct Engine : IEngine
{
float volume = 5.0;
int horse_power = 400;
float getVolume() const override { return volume; }
int getHorsePower() const override { return horse_power; }
};
// 抽象日志接口
struct ILogger
{
virtual void log(const string& msg) = 0;
virtual ~ILogger() = default;
};
// 具体日志实现
struct ConsoleLogger : ILogger
{
void log(const string& msg) override
{
cout << "[Console] " << msg << "\n";
}
};
// 具体车实现
struct Car: IEngine
{
unique_ptr<ILogger> logger;
unique_ptr<IEngine> engine;
Car(unique_ptr<ILogger> logger, unique_ptr<IEngine> engine)
: logger(move(logger)), engine(move(engine))
{
logger->log("Car is created");
}
friend ostream& operator<<(ostream& os, const Car& car)
{
return os << "Car with " << *car.engine;
}
void start()
{
logger->log("Car is starting with " + to_string(volume) + "L engine and " + to_string(horse_power) + "HP");
}
};
本期内容就到这里了,喜欢请点个赞,谢谢
封面图自取:
