简单工厂模式讲解(C++实现)
前期问题引入
假设我们正在开发一个电子产品商店系统,需要创建不同类型的电子产品对象,如手机、平板电脑和笔记本电脑。在没有使用设计模式的情况下,我们可能会这样写代码:
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 <iostream> #include <string> using namespace std;
enum ProductType { PHONE, TABLET, LAPTOP };
class Phone { public: void showInfo() { cout << "这是一部手机" << endl; } };
class Tablet { public: void showInfo() { cout << "这是一台平板电脑" << endl; } };
class Laptop { public: void showInfo() { cout << "这是一台笔记本电脑" << endl; } };
int main() { int type; cout << "请输入产品类型 (0:手机, 1:平板, 2:笔记本): "; cin >> type; if (type == PHONE) { Phone* phone = new Phone(); phone->showInfo(); delete phone; } else if (type == TABLET) { Tablet* tablet = new Tablet(); tablet->showInfo(); delete tablet; } else if (type == LAPTOP) { Laptop* laptop = new Laptop(); laptop->showInfo(); delete laptop; } else { cout << "无效的产品类型" << endl; } return 0; }
|
存在的问题:
- 客户端代码与具体产品类耦合度高
- 如果需要添加新产品,需要修改客户端代码,违反开闭原则
- 创建对象的逻辑分散在多个地方,难以维护
简单工厂模式解决方案
简单工厂模式通过引入一个工厂类来负责创建对象,将对象的创建与使用分离。
实现代码
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| #include <iostream> #include <string> #include <memory> using namespace std;
enum ProductType { PHONE, TABLET, LAPTOP };
class Product { public: virtual void showInfo() = 0; virtual ~Product() {} };
class Phone : public Product { public: void showInfo() override { cout << "这是一部智能手机" << endl; } };
class Tablet : public Product { public: void showInfo() override { cout << "这是一台平板电脑" << endl; } };
class Laptop : public Product { public: void showInfo() override { cout << "这是一台高性能笔记本电脑" << endl; } };
class ProductFactory { public: static unique_ptr<Product> createProduct(ProductType type) { switch (type) { case PHONE: return make_unique<Phone>(); case TABLET: return make_unique<Tablet>(); case LAPTOP: return make_unique<Laptop>(); default: throw invalid_argument("无效的产品类型"); } } };
int main() { try { int type; cout << "请输入产品类型 (0:手机, 1:平板, 2:笔记本): "; cin >> type; unique_ptr<Product> product = ProductFactory::createProduct(static_cast<ProductType>(type)); product->showInfo(); } catch (const exception& e) { cout << "错误: " << e.what() << endl; } return 0; }
|
模式解释
结构组成
- 抽象产品(Product):定义了产品的接口,是所有具体产品类的父类
- 具体产品(Concrete Product):实现了抽象产品接口的具体类
- 工厂(Factory):负责创建具体产品的类,包含创建产品的业务逻辑
工作流程
- 客户端需要产品时,向工厂请求
- 工厂根据传入的参数判断应该创建哪种具体产品
- 工厂创建产品对象并返回给客户端
- 客户端通过抽象产品接口使用产品,无需关心具体实现
设计原则
简单工厂模式体现了以下设计原则:
- 单一职责原则:将对象创建逻辑集中到工厂类中
- 依赖倒置原则:客户端依赖于抽象产品接口,而不是具体产品类
- 开闭原则(部分满足):对扩展开放(可以添加新产品),对修改关闭(但修改类型需要修改工厂类)
更多示例:扩展产品类型
假设我们需要添加一个新的产品类型”智能手表”,只需要:
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
| enum ProductType { PHONE, TABLET, LAPTOP, SMARTWATCH };
class SmartWatch : public Product { public: void showInfo() override { cout << "这是一只智能手表" << endl; } };
class ProductFactory { public: static unique_ptr<Product> createProduct(ProductType type) { switch (type) { case PHONE: return make_unique<Phone>(); case TABLET: return make_unique<Tablet>(); case LAPTOP: return make_unique<Laptop>(); case SMARTWATCH: return make_unique<SmartWatch>(); default: throw invalid_argument("无效的产品类型"); } } };
|
优缺点分析
优点
- 分离创建与使用:将对象创建和使用分离,降低系统耦合度
- 客户端简化:客户端无需知道具体产品类名,只需要知道具体产品对应的参数
- 集中管理:将创建逻辑集中,便于统一管理和维护
- 引入新产品容易:添加新产品只需扩展工厂类,不需要修改客户端(但需要修改工厂类)
缺点
- 工厂类职责过重:所有产品创建逻辑集中在一个工厂类中
- 违反开闭原则:添加新产品需要修改工厂类的逻辑
- 难以扩展复杂产品:如果产品之间存在复杂的层次结构,简单工厂难以应对
- 静态方法问题:使用静态工厂方法导致工厂角色无法形成基于继承的等级结构
总结
简单工厂模式是一种创建型设计模式,它提供了一个统一的接口来创建不同类型的对象,而无需向客户端暴露创建逻辑。这种模式通过将对象的实例化过程封装在一个工厂类中,实现了创建和使用的分离。
适用场景:
- 工厂类负责创建的对象比较少
- 客户端只知道传入工厂类的参数,不关心如何创建对象
- 需要将对象的创建和使用分离的场景
不适用场景:
- 需要创建复杂对象或对象之间有复杂关系时
- 需要频繁添加新产品时(因为需要修改工厂类)
- 产品类型过多,导致工厂类过于庞大时
简单工厂模式是工厂方法模式和抽象工厂模式的基础,理解简单工厂模式有助于学习更复杂的工厂模式。在实际开发中,应根据具体需求选择合适的设计模式。
小练习
题目:图形绘制工厂
问题描述:
你需要设计一个简单的图形绘制系统,该系统能够创建和绘制不同类型的几何图形(圆形、矩形和三角形)。请使用简单工厂模式来实现这个系统。
具体要求:
- 创建一个抽象图形类
Shape,包含一个纯虚函数 draw()
- 创建三个具体图形类:
Circle、Rectangle 和 Triangle,继承自 Shape 并实现 draw() 方法
- 创建一个图形工厂类
ShapeFactory,根据传入的参数创建相应的图形对象
- 编写客户端代码,演示如何使用工厂创建不同类型的图形并调用其绘制方法
扩展要求(可选):
- 为每种图形添加计算面积的方法
calculateArea()
- 考虑使用枚举类型来标识不同的图形类型
- 添加异常处理,当传入无效参数时给出友好提示
提示:
- 可以使用枚举类型定义图形类型:
CIRCLE, RECTANGLE, TRIANGLE
- 工厂类可以包含一个静态方法
createShape(ShapeType type)
- 考虑使用智能指针管理对象生命周期
请尝试实现上述要求,完成后可以对比下面的参考答案。
参考答案
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
| #include <iostream> #include <memory> #include <cmath> #include <stdexcept> using namespace std;
enum ShapeType { CIRCLE, RECTANGLE, TRIANGLE };
class Shape { public: virtual void draw() = 0; virtual double calculateArea() = 0; virtual ~Shape() {} };
class Circle : public Shape { private: double radius; public: Circle(double r) : radius(r) {} void draw() override { cout << "绘制圆形,半径: " << radius << endl; } double calculateArea() override { return 3.14159 * radius * radius; } };
class Rectangle : public Shape { private: double width; double height; public: Rectangle(double w, double h) : width(w), height(h) {} void draw() override { cout << "绘制矩形,宽度: " << width << ", 高度: " << height << endl; } double calculateArea() override { return width * height; } };
class Triangle : public Shape { private: double sideA, sideB, sideC; public: Triangle(double a, double b, double c) : sideA(a), sideB(b), sideC(c) {} void draw() override { cout << "绘制三角形,边长: " << sideA << ", " << sideB << ", " << sideC << endl; } double calculateArea() override { double s = (sideA + sideB + sideC) / 2; return sqrt(s * (s - sideA) * (s - sideB) * (s - sideC)); } };
class ShapeFactory { public: static unique_ptr<Shape> createShape(ShapeType type, double param1 = 0, double param2 = 0, double param3 = 0) { switch (type) { case CIRCLE: if (param1 <= 0) throw invalid_argument("圆的半径必须大于0"); return make_unique<Circle>(param1); case RECTANGLE: if (param1 <= 0 || param2 <= 0) throw invalid_argument("矩形的宽高必须大于0"); return make_unique<Rectangle>(param1, param2); case TRIANGLE: if (param1 <= 0 || param2 <= 0 || param3 <= 0) throw invalid_argument("三角形的边长必须大于0"); if (param1 + param2 <= param3 || param1 + param3 <= param2 || param2 + param3 <= param1) throw invalid_argument("提供的边长无法构成三角形"); return make_unique<Triangle>(param1, param2, param3); default: throw invalid_argument("不支持的图形类型"); } } };
int main() { try { auto circle = ShapeFactory::createShape(CIRCLE, 5.0); circle->draw(); cout << "圆形面积: " << circle->calculateArea() << endl << endl; auto rectangle = ShapeFactory::createShape(RECTANGLE, 4.0, 6.0); rectangle->draw(); cout << "矩形面积: " << rectangle->calculateArea() << endl << endl; auto triangle = ShapeFactory::createShape(TRIANGLE, 3.0, 4.0, 5.0); triangle->draw(); cout << "三角形面积: " << triangle->calculateArea() << endl << endl; } catch (const exception& e) { cout << "错误: " << e.what() << endl; } return 0; }
|
代码说明:
- 定义了
Shape抽象基类,包含draw()和calculateArea()纯虚函数
- 实现了三种具体图形类,每个类都有自己特定的属性和计算方法
- 工厂类
ShapeFactory根据传入的类型和参数创建相应的图形对象
- 添加了参数验证和异常处理,确保创建的对象是有效的
- 使用
unique_ptr管理对象生命周期,避免内存泄漏
这个实现展示了简单工厂模式的核心思想:将对象的创建逻辑封装在一个工厂类中,客户端只需要知道要创建什么类型的对象,而不需要关心具体的创建细节。
为什么工厂类返回指向抽象类的指针而不是抽象类本身
抽象类不能实例化
指针和引用的多态性
虚函数表(vtable)的工作原理
抽象类指针会指向子类的虚函数表! 这就是多态的实现机制:
虚函数表:每个包含虚函数的类都有一个虚函数表(vtable)
在 C++ 里,一个子类如果同时继承多个“带虚函数”的基类,就会:
有几份 vfptr(虚函数表指针)
每个“带虚函数的基类”都会给子类贡献 1 个 vfptr。
因此
- 单继承 → 1 个 vfptr
- N 个带虚函数的基类 → N 个 vfptr(放在子类对象里,顺序与继承顺序一致)
有几张 vftable(虚函数表)
每张 vfptr 指向一张独立的 vftable,因此也有 N 张表。
表里列的是“当前子类对于该基类视角”可见的虚函数入口地址(可能被子类重写,也可能直接指向基类实现)。
内存布局(Itanium C++ ABI 典型,Linux x86-64)
1 2 3 4 5 6 7 8
| |---------------------------| | 子类对象内存映像 | |---------------------------| | offset 0: Base1 vfptr | --> 指向 Base1-vftable(子类视角) | ...非静态数据... | | offset X: Base2 vfptr | --> 指向 Base2-vftable(子类视角) | ...非静态数据... | |---------------------------|
|
- vfptr 位于 对象最前端(或紧跟基类子对象的数据区之前)。
- vftable 本身放在 进程只读数据段(.rodata),全局唯一,不在对象里,对象里只存指针。
虚表指针与对象生命周期
- 构造阶段:进入哪个基类/子类构造函数,就把对应 vfptr 设成 当前正在构造的类的 vftable。
- 析构阶段:相反,层层回退,vfptr 逐级恢复。
图示(32 位简化)
1 2 3 4 5 6 7 8 9
| 子类 Derived 对象地址 +0: vfptr --------┐ +4: derived_data | +8: vfptr2 ----┐ | +12: more_data | | | | .rodata 段 | | Base1-vftable <--+ | Base2-vftable <-----+
|
一句话总结
- 几个带虚函数的基类 → 子类里就有几个 vfptr(对象内)。
- 每张 vfptr 指向一张全局 vftable(.rodata 区)。
- 对象里只有指针,表本身在只读全局数据段,与对象生命周期无关。
虚函数指针:每个对象包含一个指向其类的vtable的指针(vptr)
cpp
1 2 3 4 5
| Circle circle(5.0); Shape* shapePtr = &circle;
shapePtr->draw();
|
内存布局示意图:
1 2 3 4 5 6 7 8 9 10 11
| Circle对象: +--------------+ | vptr | --> 指向Circle的vtable | radius=5.0 | +--------------+
Circle的vtable: +--------------+ | &Circle::draw| | &Circle::~Circle| +--------------+
|
动态绑定:通过vptr,在运行时确定要调用的实际函数
返回指针/引用允许我们在运行时确定对象的实际类型,而返回值会在编译时确定类型,无法实现多态。 这就是为什么在工厂模式中总是返回指针或引用而不是对象本身。
对象切片(Object Slicing)详解
对象切片是C++中一个常见但容易忽视的问题,它发生在将派生类对象赋值给基类对象时。让我详细解释这个概念。
什么是对象切片?
对象切片是指当派生类对象被赋值给基类对象时,派生类特有的成员变量和方法会被”切掉”,只保留基类部分。这会导致数据丢失和多态行为失效。
简单示例
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
| #include <iostream> #include <string> using namespace std;
class Animal { public: string type = "Animal"; virtual void makeSound() { cout << "Some animal sound" << endl; } virtual Animal clone() { return *this; } };
class Dog : public Animal { public: string breed = "Unknown"; string type = "Dog"; void makeSound() override { cout << "Woof! Woof!" << endl; } void fetch() { cout << "Fetching the ball!" << endl; } };
int main() { Dog dog; dog.breed = "Golden Retriever"; Animal animal = dog; cout << "Animal type: " << animal.type << endl; animal.makeSound(); return 0; }
|
对象切片的机制
当发生对象切片时:
内存布局变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Dog对象 (切片前): +-----------------+ | Animal部分 | | - vptr | --> 指向Dog的vtable | - type="Animal" | +-----------------+ | Dog特有部分 | | - breed | = "Golden Retriever" | - type="Dog" | +-----------------+
Animal对象 (切片后): +-----------------+ | Animal部分 | | - vptr | --> 指向Animal的vtable | - type="Animal" | +-----------------+ // Dog特有部分完全丢失!
|
虚函数表指针被重置:
- 派生类对象的vptr原本指向派生类的虚函数表
- 切片后,vptr被设置为指向基类的虚函数表
- 因此多态行为失效
对象切片的常见场景
1. 赋值操作
1 2
| Dog dog; Animal animal = dog;
|
2. 函数传值参数
1 2 3 4 5 6
| void processAnimal(Animal animal) { animal.makeSound(); }
Dog dog; processAnimal(dog);
|
3. 函数返回值
1 2 3 4
| Animal createAnimal() { Dog dog; return dog; }
|
4. 容器存储
1 2 3
| vector<Animal> animals; Dog dog; animals.push_back(dog);
|
如何避免对象切片
1. 使用指针
1 2 3 4
| Dog* dog = new Dog(); Animal* animal = dog; animal->makeSound(); delete dog;
|
2. 使用引用
1 2 3
| Dog dog; Animal& animalRef = dog; animalRef.makeSound();
|
3. 使用智能指针(推荐)
1 2 3 4 5 6 7 8
| #include <memory>
unique_ptr<Animal> createAnimal() { return make_unique<Dog>(); }
auto animal = createAnimal(); animal->makeSound();
|
4. 使用clone模式(正确实现)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Animal { public: virtual unique_ptr<Animal> clone() { return make_unique<Animal>(*this); } };
class Dog : public Animal { public: unique_ptr<Animal> clone() override { return make_unique<Dog>(*this); } };
Dog dog; auto cloned = dog.clone(); cloned->makeSound();
|
对象切片的危害
- 数据丢失:派生类特有的数据成员被完全丢弃
- 多态失效:虚函数调用不再具有多态性
- 难以调试:切片常常是隐式发生的,不容易发现
- 设计缺陷:暴露了面向对象设计中的问题
实际应用中的注意事项
基类设计:如果希望避免切片,可以考虑将基类设为抽象类
1 2 3 4 5
| class Animal { public: virtual void makeSound() = 0; virtual ~Animal() {} };
|
这样就不能创建Animal对象,只能使用指针或引用。
禁用拷贝:对于不希望被切片的类,可以禁用拷贝构造函数和赋值运算符
1 2 3 4 5
| class NonSliceable { public: NonSliceable(const NonSliceable&) = delete; NonSliceable& operator=(const NonSliceable&) = delete; };
|
使用CRTP模式:一种高级技术,可以在编译时检测切片
1 2 3 4 5 6 7 8 9
| template <typename Derived> class Base { public: Derived& derived() { return static_cast<Derived&>(*this); } };
class Dog : public Base<Dog> { };
|
总结
对象切片是C++中一个重要的概念,理解它对于编写正确的面向对象代码至关重要:
- 切片发生在值语义操作中:赋值、传值参数、返回值等
- 切片导致数据丢失和多态失效:派生类特有部分被丢弃,vptr被重置
- 避免切片的方法:使用指针、引用或智能指针
- 设计时考虑:使用抽象基类、禁用拷贝或高级模式来防止切片
在工厂模式中,返回抽象类的指针或引用而不是对象本身,正是为了避免对象切片,保持多态性和数据的完整性。