以"咖啡店饮品制作系统"为例,从零开始用简单工厂模式组织 C++ 代码。
一、什么是简单工厂模式
简单工厂模式的核心思想:客户端不直接 new 具体的产品类,而是通过一个工厂类来创建对象。客户端只需要告诉工厂"我要什么",工厂负责决定创建哪个具体类的实例。
没有工厂的写法(直接 new)
cpp
Coffee* latte = new Latte(); // 客户端直接依赖 Latte 类
Coffee* americano = new Americano(); // 客户端直接依赖 Americano 类
问题:客户端需要知道所有具体类的名字,每新增一种咖啡,所有创建它的地方都要改。
使用简单工厂的写法
cpp
Coffee* latte = CoffeeSimpleFactory::createCoffee("latte"); // 只依赖工厂
Coffee* americano = CoffeeSimpleFactory::createCoffee("americano"); // 只依赖工厂
好处 :客户端只依赖基类 Coffee 和工厂 CoffeeSimpleFactory,完全不需要知道 Latte、Americano 等具体类的存在。
类图
Coffee (抽象基类)
/ | \
Americano Latte Cappuccino
\ | /
CoffeeSimpleFactory
|
main()
二、项目结构
标准的 C++ 项目将**声明(.h)和实现(.cpp)**分离:
project/
├── include/ # 头文件(只放声明)
│ ├── Coffee.h # 产品基类
│ ├── Americano.h # 具体产品:美式咖啡
│ ├── Latte.h # 具体产品:拿铁
│ ├── Cappuccino.h # 具体产品:卡布奇诺
│ └── CoffeeSimpleFactory.h # 简单工厂
├── src/ # 源文件(放具体实现)
│ ├── main.cpp # 主函数入口
│ ├── Americano.cpp # 美式咖啡实现
│ ├── Latte.cpp # 拿铁实现
│ ├── Cappuccino.cpp # 卡布奇诺实现
│ └── CoffeeSimpleFactory.cpp # 工厂实现
└── build/ # 编译输出
└── main.exe
为什么要分离 .h 和 .cpp?
| 文件 | 放什么 | 作用 |
|---|---|---|
.h |
类的声明(函数签名) | 告诉别人"我有什么功能" |
.cpp |
函数的具体实现 | 定义"功能怎么做" |
好处:修改实现时只需重新编译对应的 .cpp,不影响其他文件。
三、逐步实现
第 1 步:定义产品基类 --- Coffee.h
cpp
#ifndef COFFEE_H
#define COFFEE_H
#include<string>
using namespace std;
class Coffee
{
public:
virtual string getName()=0; // 纯虚函数:获取名称
virtual double getPrice()=0; // 纯虚函数:获取价格
virtual ~Coffee(){} // 虚析构函数(重要!)
};
#endif
要点:
=0表示纯虚函数,Coffee是抽象类,不能直接实例化virtual ~Coffee(){}虚析构函数确保delete基类指针时能正确释放子类资源#ifndef COFFEE_H/#define COFFEE_H/#endif是头文件保护,防止同一文件被重复包含
第 2 步:定义具体产品类
每个具体咖啡类都继承 Coffee,分为 .h(声明)和 .cpp(实现)两个文件。
Americano.h --- 声明
cpp
#ifndef AMERICANO_H
#define AMERICANO_H
#include"Coffee.h"
class Americano:public Coffee{
public:
string getName() override; // override 表示重写基类的虚函数
double getPrice() override;
};
#endif
Americano.cpp --- 实现
cpp
#include"Americano.h"
string Americano::getName(){ // 类名::函数名 的写法
return "Americano";
}
double Americano::getPrice(){
return 30;
}
Latte 和 Cappuccino 的写法完全相同,只是返回值不同:
Latte.h
cpp
#ifndef LATTE_H
#define LATTE_H
#include"Coffee.h"
class Latte:public Coffee{
public:
string getName() override;
double getPrice() override;
};
#endif
Latte.cpp
cpp
#include"Latte.h"
string Latte::getName(){
return "Latte";
}
double Latte::getPrice(){
return 35;
}
Cappuccino.h
cpp
#ifndef CAPPUCCINO_H
#define CAPPUCCINO_H
#include"Coffee.h"
class Cappuccino:public Coffee{
public:
string getName() override;
double getPrice() override;
};
#endif
Cappuccino.cpp
cpp
#include"Cappuccino.h"
string Cappuccino::getName(){
return "Cappuccino";
}
double Cappuccino::getPrice(){
return 40;
}
第 3 步:定义简单工厂
CoffeeSimpleFactory.h --- 声明
cpp
#ifndef COFFEE_SIMPLE_FACTORY_H
#define COFFEE_SIMPLE_FACTORY_H
#include"Coffee.h"
class CoffeeSimpleFactory{
public:
static Coffee* createCoffee(string type); // 静态方法,不需要创建工厂实例
};
#endif
注意 :头文件中工厂只依赖 Coffee.h(基类),不需要 include 具体产品类。
CoffeeSimpleFactory.cpp --- 实现
cpp
#include"CoffeeSimpleFactory.h"
#include"Americano.h" // 实现中才需要知道具体类
#include"Latte.h"
#include"Cappuccino.h"
Coffee* CoffeeSimpleFactory::createCoffee(string type){
if(type=="americano") return new Americano();
if(type=="latte") return new Latte();
if(type=="cappuccino") return new Cappuccino();
return nullptr; // 未知类型返回空指针
}
核心逻辑 :根据传入的字符串参数,用 if/else 决定创建哪个具体产品。这就是"简单工厂"的本质------一个工厂类,通过参数决定创建哪个产品。
第 4 步:编写主函数
main.cpp
cpp
#include<iostream>
#include<cstdlib>
#include"CoffeeSimpleFactory.h" // 只需要引入工厂头文件
using namespace std;
int main()
{
// 通过工厂创建,不直接 new 具体类
Coffee* americano=CoffeeSimpleFactory::createCoffee("americano");
Coffee* latte=CoffeeSimpleFactory::createCoffee("latte");
Coffee* cappuccino=CoffeeSimpleFactory::createCoffee("cappuccino");
// 使用产品
cout<<americano->getName()<<" - "<<americano->getPrice()<<"元"<<endl;
cout<<latte->getName()<<" - "<<latte->getPrice()<<"元"<<endl;
cout<<cappuccino->getName()<<" - "<<cappuccino->getPrice()<<"元"<<endl;
// 释放内存
delete americano;
delete latte;
delete cappuccino;
system("pause");
return 0;
}
注意 :main.cpp 只 include 了 CoffeeSimpleFactory.h,完全不知道 Americano、Latte、Cappuccino 的存在------这就是简单工厂的解耦效果。
四、编译和运行
多文件项目需要把所有 .cpp 一起编译:
powershell
g++ -g src/*.cpp -I include -fexec-charset=GBK -finput-charset=UTF-8 -o build/main.exe
| 参数 | 含义 |
|---|---|
src/*.cpp |
编译 src 目录下所有 .cpp 文件 |
-I include |
告诉编译器去 include 目录找头文件 |
-fexec-charset=GBK |
解决 Windows 控制台中文乱码 |
-o build/main.exe |
输出到 build 目录 |
运行结果:
Americano - 30元
Latte - 35元
Cappuccino - 40元
五、常见错误和踩坑记录
错误 1:变量名和类名重复
cpp
Coffee* Americano = new Americano(); // 错误!变量名和类名都叫 Americano
报错 :编译器分不清 Americano 是变量还是类型。
修复 :变量名用小写 americano。
错误 2:子类缺少 public:
cpp
class Latte:public Coffee{
string getName() override{...} // 错误!默认是 private
};
报错 :外部无法调用 getName()。
修复 :在成员函数前加 public:。
错误 3:把简单工厂写成抽象类
cpp
class CoffeeSimpleFactory(string type){ // 错误!类定义不能带参数
virtual Coffee* createCoffee(...)=0; // 错误!简单工厂不需要虚函数
};
原因:混淆了简单工厂和工厂方法模式。
修复 :简单工厂就是一个普通类 + 一个 static 方法,用 if/else 选择创建哪个产品。
错误 4:特殊字符编码报错
error: converting to execution character set: Illegal byte sequence
原因 :¥ 符号在 UTF-8 转 GBK 时没有对应编码。
修复 :用中文 元 替代 ¥。
六、简单工厂模式总结
角色分工
| 角色 | 本例中 | 职责 |
|---|---|---|
| 抽象产品 | Coffee |
定义产品接口 |
| 具体产品 | Americano, Latte, Cappuccino |
实现具体产品 |
| 工厂 | CoffeeSimpleFactory |
根据参数创建具体产品 |
| 客户端 | main() |
使用工厂获取产品,不关心具体实现 |
优点
- 客户端与具体产品类解耦
- 创建逻辑集中在工厂中,便于管理
缺点
- 每新增一种产品,都要修改工厂的
if/else(违反开闭原则) - 工厂类职责越来越重
什么时候用
- 产品种类不多、变化不频繁
- 想把创建逻辑从客户端抽离出来
下一步 :当产品种类经常变化时,就需要用工厂方法模式------每种产品对应一个工厂子类,新增产品只需新增工厂子类,不用修改现有代码。