工厂模式、多态与接口设计
时间:2026/04/09
关键词:抽象接口、虚函数、工厂函数、依赖倒置、override、虚析构、unique_ptr
核心目标:理解什么时候该把“创建对象”和“使用对象”分开。
1. 为什么需要工厂模式
很多代码的问题不是“不会 new”,而是:
- 调用方知道太多具体类型
- 构造逻辑散落各处
- 后续替换实现很痛苦
工厂模式的核心价值是:
- 把对象创建逻辑集中起来
- 让调用方依赖抽象接口,而不是具体实现
2. 多态接口的基础
1 2 3 4 5 6 7 8 9 10 11 12
| struct Pet { virtual ~Pet() = default; virtual void speak() = 0; };
struct Cat : Pet { void speak() override { std::puts("meow"); } };
struct Dog : Pet { void speak() override { std::puts("woof"); } };
|
这里要点有两个:
- 基类析构函数要么虚,要么不允许多态删除
- 派生类重写时用
override
3. 一个最简单的工厂函数
1 2 3 4 5 6 7 8
| #include <memory> #include <string>
std::unique_ptr<Pet> make_pet(const std::string& kind) { if (kind == "cat") return std::make_unique<Cat>(); if (kind == "dog") return std::make_unique<Dog>(); return nullptr; }
|
调用方只关心:
而不关心:
4. 为什么返回 unique_ptr
返回裸指针会引入一个问题:
用 std::unique_ptr 更清晰:
这是现代 C++ 工厂接口最常见的实践。
5. 简单工厂 vs 工厂方法
5.1 简单工厂
一个集中函数,根据参数分支创建对象。
优点:
缺点:
5.2 工厂方法
把“创建哪种对象”交给子类。
适合:
但对小项目来说,简单工厂已经够用。
6. 接口设计比模式名更重要
真正工程里,更该关注这些问题:
- 基类是不是表达了稳定抽象
- 调用方是否真的不需要知道具体类型
- 返回所有权是否清晰
- 是否需要注册表或插件化
很多时候模式本身不复杂,难的是接口边界。
7. 一个更贴近工程的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct Reducer { virtual ~Reducer() = default; virtual int init() const = 0; virtual int combine(int a, int b) const = 0; };
struct SumReducer : Reducer { int init() const override { return 0; } int combine(int a, int b) const override { return a + b; } };
struct MulReducer : Reducer { int init() const override { return 1; } int combine(int a, int b) const override { return a * b; } };
|
这里“算法骨架”不变,“聚合策略”可替换。
这类设计常和工厂、策略模式一起出现。
8. 工厂模式的常见扩展
8.1 注册表工厂
把字符串或类型 id 映射到创建函数:
8.2 抽象工厂
如果你需要创建一整组关联对象,就可能进入抽象工厂场景。
例如:
9. 常见坑
9.1 基类没有虚析构
多态删除会出问题。
9.2 工厂返回裸指针
所有权不清晰。
9.3 为了“用模式而用模式”
小项目里过度抽象只会增加复杂度。
9.4 基类接口设计得太宽
会让派生类被迫实现很多并不需要的东西。
10. 参考实例:注册表工厂
当对象类型会扩展时,可以用“名字 -> 创建函数”的注册表。
调用方只依赖抽象接口,不需要知道具体类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| #include <functional> #include <memory> #include <stdexcept> #include <string> #include <string_view> #include <unordered_map> #include <utility>
struct Shape { virtual ~Shape() = default; virtual double area() const = 0; };
struct Circle : Shape { explicit Circle(double r) : r(r) {} double area() const override { return 3.14159 * r * r; } double r = 1.0; };
struct Square : Shape { explicit Square(double side) : side(side) {} double area() const override { return side * side; } double side = 1.0; };
class ShapeFactory { public: using Creator = std::function<std::unique_ptr<Shape>(double)>;
void register_type(std::string name, Creator creator) { creators_[std::move(name)] = std::move(creator); }
std::unique_ptr<Shape> create(std::string_view name, double arg) const { auto it = creators_.find(std::string(name)); if (it == creators_.end()) { throw std::runtime_error("unknown shape type"); } return it->second(arg); }
private: std::unordered_map<std::string, Creator> creators_; };
int main() { ShapeFactory factory;
factory.register_type("circle", [](double r) { return std::make_unique<Circle>(r); });
factory.register_type("square", [](double side) { return std::make_unique<Square>(side); });
auto shape = factory.create("circle", 2.0); double a = shape->area(); }
|
这个例子里:
- 工厂集中管理创建逻辑
- 返回
unique_ptr 表达所有权转移
- 新类型只需要注册,不需要改调用方
- 运行期根据字符串选择具体类型
11. 一页总结
工厂模式最核心的收益不是“设计模式名词”,而是:
- 创建逻辑集中
- 调用方依赖抽象
- 所有权表达清晰
如果只记一句:
当“对象怎么创建”开始影响“对象怎么使用”时,就该考虑把创建逻辑抽出来。