简单工厂模式讲解(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. 创建对象的逻辑分散在多个地方,难以维护

简单工厂模式解决方案

简单工厂模式通过引入一个工厂类来负责创建对象,将对象的创建与使用分离。

实现代码

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;
}

模式解释

结构组成

  1. 抽象产品(Product):定义了产品的接口,是所有具体产品类的父类
  2. 具体产品(Concrete Product):实现了抽象产品接口的具体类
  3. 工厂(Factory):负责创建具体产品的类,包含创建产品的业务逻辑

工作流程

  1. 客户端需要产品时,向工厂请求
  2. 工厂根据传入的参数判断应该创建哪种具体产品
  3. 工厂创建产品对象并返回给客户端
  4. 客户端通过抽象产品接口使用产品,无需关心具体实现

设计原则

简单工厂模式体现了以下设计原则:

  • 单一职责原则:将对象创建逻辑集中到工厂类中
  • 依赖倒置原则:客户端依赖于抽象产品接口,而不是具体产品类
  • 开闭原则(部分满足):对扩展开放(可以添加新产品),对修改关闭(但修改类型需要修改工厂类)

更多示例:扩展产品类型

假设我们需要添加一个新的产品类型”智能手表”,只需要:

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: // 新增case
return make_unique<SmartWatch>();
default:
throw invalid_argument("无效的产品类型");
}
}
};

优缺点分析

优点

  1. 分离创建与使用:将对象创建和使用分离,降低系统耦合度
  2. 客户端简化:客户端无需知道具体产品类名,只需要知道具体产品对应的参数
  3. 集中管理:将创建逻辑集中,便于统一管理和维护
  4. 引入新产品容易:添加新产品只需扩展工厂类,不需要修改客户端(但需要修改工厂类)

缺点

  1. 工厂类职责过重:所有产品创建逻辑集中在一个工厂类中
  2. 违反开闭原则:添加新产品需要修改工厂类的逻辑
  3. 难以扩展复杂产品:如果产品之间存在复杂的层次结构,简单工厂难以应对
  4. 静态方法问题:使用静态工厂方法导致工厂角色无法形成基于继承的等级结构

总结

简单工厂模式是一种创建型设计模式,它提供了一个统一的接口来创建不同类型的对象,而无需向客户端暴露创建逻辑。这种模式通过将对象的实例化过程封装在一个工厂类中,实现了创建和使用的分离。

适用场景:

  • 工厂类负责创建的对象比较少
  • 客户端只知道传入工厂类的参数,不关心如何创建对象
  • 需要将对象的创建和使用分离的场景

不适用场景:

  • 需要创建复杂对象或对象之间有复杂关系时
  • 需要频繁添加新产品时(因为需要修改工厂类)
  • 产品类型过多,导致工厂类过于庞大时

简单工厂模式是工厂方法模式和抽象工厂模式的基础,理解简单工厂模式有助于学习更复杂的工厂模式。在实际开发中,应根据具体需求选择合适的设计模式。

小练习

题目:图形绘制工厂

问题描述:
你需要设计一个简单的图形绘制系统,该系统能够创建和绘制不同类型的几何图形(圆形、矩形和三角形)。请使用简单工厂模式来实现这个系统。

具体要求:

  1. 创建一个抽象图形类 Shape,包含一个纯虚函数 draw()
  2. 创建三个具体图形类:CircleRectangleTriangle,继承自 Shape 并实现 draw() 方法
  3. 创建一个图形工厂类 ShapeFactory,根据传入的参数创建相应的图形对象
  4. 编写客户端代码,演示如何使用工厂创建不同类型的图形并调用其绘制方法

扩展要求(可选):

  1. 为每种图形添加计算面积的方法 calculateArea()
  2. 考虑使用枚举类型来标识不同的图形类型
  3. 添加异常处理,当传入无效参数时给出友好提示

提示:

  • 可以使用枚举类型定义图形类型: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;

// 测试异常情况
// auto invalidCircle = ShapeFactory::createShape(CIRCLE, -1.0);

} catch (const exception& e) {
cout << "错误: " << e.what() << endl;
}

return 0;
}

代码说明:

  1. 定义了Shape抽象基类,包含draw()calculateArea()纯虚函数
  2. 实现了三种具体图形类,每个类都有自己特定的属性和计算方法
  3. 工厂类ShapeFactory根据传入的类型和参数创建相应的图形对象
  4. 添加了参数验证和异常处理,确保创建的对象是有效的
  5. 使用unique_ptr管理对象生命周期,避免内存泄漏

这个实现展示了简单工厂模式的核心思想:将对象的创建逻辑封装在一个工厂类中,客户端只需要知道要创建什么类型的对象,而不需要关心具体的创建细节。

为什么工厂类返回指向抽象类的指针而不是抽象类本身

抽象类不能实例化

指针和引用的多态性

虚函数表(vtable)的工作原理

抽象类指针会指向子类的虚函数表! 这就是多态的实现机制:

  1. 虚函数表:每个包含虚函数的类都有一个虚函数表(vtable)

    在 C++ 里,一个子类如果同时继承多个“带虚函数”的基类,就会:

    1. 有几份 vfptr(虚函数表指针)
      每个“带虚函数的基类”都会给子类贡献 1 个 vfptr
      因此

      • 单继承 → 1 个 vfptr
      • N 个带虚函数的基类 → N 个 vfptr(放在子类对象里,顺序与继承顺序一致)
    2. 有几张 vftable(虚函数表)
      每张 vfptr 指向一张独立的 vftable,因此也有 N 张表
      表里列的是“当前子类对于该基类视角”可见的虚函数入口地址(可能被子类重写,也可能直接指向基类实现)。

    3. 内存布局(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),全局唯一,不在对象里,对象里只存指针。
    4. 虚表指针与对象生命周期

      • 构造阶段:进入哪个基类/子类构造函数,就把对应 vfptr 设成 当前正在构造的类的 vftable
      • 析构阶段:相反,层层回退,vfptr 逐级恢复。
    5. 图示(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 区)。
    • 对象里只有指针,表本身在只读全局数据段,与对象生命周期无关。
  2. 虚函数指针:每个对象包含一个指向其类的vtable的指针(vptr)

    cpp

    1
    2
    3
    4
    5
    Circle circle(5.0);
    Shape* shapePtr = &circle; // shapePtr指向Circle对象

    // 运行时通过vtable确定调用Circle::draw()
    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|
    +--------------+
  3. 动态绑定:通过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"; // 隐藏基类的type

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
// cout << animal.breed << endl; // 错误: Animal没有breed成员

animal.makeSound(); // 输出: Some animal sound (不是Woof!)
// animal.fetch(); // 错误: Animal没有fetch方法

return 0;
}

对象切片的机制

当发生对象切片时:

  1. 内存布局变化

    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特有部分完全丢失!
  2. 虚函数表指针被重置

    • 派生类对象的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); // 传递Dog,但函数接收Animal

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(); // 输出: Woof! Woof!
delete dog;

2. 使用引用

1
2
3
Dog dog;
Animal& animalRef = dog; // 不会切片,保持多态
animalRef.makeSound(); // 输出: Woof! Woof!

3. 使用智能指针(推荐)

1
2
3
4
5
6
7
8
#include <memory>

unique_ptr<Animal> createAnimal() {
return make_unique<Dog>(); // 不会切片,保持多态
}

auto animal = createAnimal();
animal->makeSound(); // 输出: Woof! Woof!

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(); // 输出: Woof! Woof!

对象切片的危害

  1. 数据丢失:派生类特有的数据成员被完全丢弃
  2. 多态失效:虚函数调用不再具有多态性
  3. 难以调试:切片常常是隐式发生的,不容易发现
  4. 设计缺陷:暴露了面向对象设计中的问题

实际应用中的注意事项

  1. 基类设计:如果希望避免切片,可以考虑将基类设为抽象类

    1
    2
    3
    4
    5
    class Animal {
    public:
    virtual void makeSound() = 0; // 纯虚函数
    virtual ~Animal() {}
    };

    这样就不能创建Animal对象,只能使用指针或引用。

  2. 禁用拷贝:对于不希望被切片的类,可以禁用拷贝构造函数和赋值运算符

    1
    2
    3
    4
    5
    class NonSliceable {
    public:
    NonSliceable(const NonSliceable&) = delete;
    NonSliceable& operator=(const NonSliceable&) = delete;
    };
  3. 使用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++中一个重要的概念,理解它对于编写正确的面向对象代码至关重要:

  1. 切片发生在值语义操作中:赋值、传值参数、返回值等
  2. 切片导致数据丢失和多态失效:派生类特有部分被丢弃,vptr被重置
  3. 避免切片的方法:使用指针、引用或智能指针
  4. 设计时考虑:使用抽象基类、禁用拷贝或高级模式来防止切片

在工厂模式中,返回抽象类的指针或引用而不是对象本身,正是为了避免对象切片,保持多态性和数据的完整性。