【设计模式精讲】5.简单工厂 → 工厂方法(Factory Method)
【摘要】:「按类型造一个对象」的需求几乎每个项目都有,也几乎每个项目都写过那坨越积越长的 if-else。本文从消息处理器的新增成本讲起,先实现简单工厂并指出它对开闭原则的违背,再演进到工厂方法模式:GoF 称之为「虚拟构造器」,把「实例化哪个类」的决定权交给子类。C++ 实现以 Refactoring Guru 的跨平台对话框为例,并覆盖 GoF 介绍的两大变体——抽象创建者与默认实现、带参数的工厂方法;随后给出智能指针返回与自动注册工厂表两种现代化改造。文末按类型数量给出选型建议:类型少且稳定用简单工厂,类型多且频繁新增才值得上工厂方法。
【关键词】:工厂方法、简单工厂、开闭原则、虚拟构造器、自动注册、多态
1. 每加一种消息,就要改三处代码
网络模块收到消息后按type字段分发处理:
// 说明性片段(省略各 Handler 的定义)// ❌ 每新增一种消息都要改这里MessageHandler*createHandler(conststd::string&type){if(type=="login")returnnewLoginHandler();if(type=="chat")returnnewChatHandler();if(type=="file")returnnewFileHandler();returnnullptr;}写的时候只有三种消息,半年后这里是二十七个分支:改一个分支可能碰坏另一个;新人提交冲突总在这个函数;测试要为它单独立一个用例。问题不在于 if-else 语法,而在于「新增一种类型」这个变化,落在了「已有代码的修改」上——每次扩展都要重新触碰所有已稳定的逻辑。第 2 篇讲过的开闭原则说的正是这件事:对扩展开放,对修改关闭。
比改代码更深一层的困境,GoF 用框架的视角点破过:框架必须创建对象,但它只认识抽象类——框架知道「何时」该创建文档,却不知道该创建「哪种」文档。这个「知道 when、不知道 what」的错位,才是工厂方法真正要解的题。
2. 模式意图与定义
先澄清一个常见误会:「简单工厂」不是 GoF 23 种模式之一,它是工厂方法前的过渡台阶;但它太常用,值得单独讲透。
- 简单工厂(Simple Factory)一句话定义:由一个工厂函数集中根据参数决定实例化哪个类。
解决的问题:把「创建逻辑」从调用方抽出,收拢到一处。代价是新增类型要修改工厂函数。 - 工厂方法(Factory Method)一句话定义(GoF 原文意图):Define an interface for creating an object, but let subclasses decide which class to instantiate——定义一个用于创建对象的接口,让子类决定实例化哪一个类;工厂方法使一个类的实例化推迟到其子类。GoF 给它起的别名很传神:虚拟构造器(Virtual Constructor)——构造函数在 C++ 里不能是虚函数,这个模式补上了这块短板。Refactoring Guru 的表述则更直白:父类提供一个创建对象的方法,允许子类决定实例化对象的具体类型。
两个容易忽略的要点,分别来自两本参考资料的提醒:其一,工厂方法不一定每次都 new,它也可以返回缓存、对象池或其他来源的已有对象(RG);其二,工厂方法返回的对象通常被统称为「产品」(Product),仅当这些产品具有共同基类或接口时,子类才能返回不同类型(RG)。
本质区别一句话:简单工厂把「造什么」写在一个函数的 if-else 里;工厂方法把「造什么」分散到各个工厂子类的重写里。
3. UML 图 + 结构说明
Refactoring Guru 把参与者拆成四个角色:
- 产品(Product)接口:所有具体产品必须实现的公共接口(
Button); - 具体产品(Concrete Products):接口的不同实现(
WindowsButton、HTMLButton); - 创建者(Creator)类:声明返回产品对象的工厂方法(
createButton()),既可声明为抽象方法强制子类实现,也可提供返回默认产品的实现; - 具体创建者(Concrete Creators):重写工厂方法,使其返回不同类型的产品。
有一个极易被忽略的定性,值得原样抄下来:尽管名字叫「创建者」,它的主要职责并不是创建产品。创建者类通常包含一些与产品相关的核心业务逻辑(如render()画对话框),工厂方法将这些逻辑与具体产品类解耦。RG 的比方是:大型软件公司设有程序员培训部门,但公司的主业是写代码,而不是「生产程序员」。
4. 传统 C++ 写法(C++11 之前)
按 GoF 原书时代的形态实现一遍:裸指针交货、没有override/nullptr/= default,禁拷贝靠「私有且不定义」。示例改编自 Refactoring Guru 的跨平台对话框:基础Dialog用抽象Button渲染窗口,Windows 和 Web 环境各自子类化,无需重写对话框逻辑:
// C++98/03 写法#include<iostream>#include<stdexcept>#include<string>// ---- 产品接口与具体产品 ----classButton{public:virtual~Button(){}virtualvoidrender()=0;virtualvoidonClick()=0;};classWindowsButton:publicButton{public:voidrender(){std::cout<<"Win 风格按钮\n";}voidonClick(){}};classHTMLButton:publicButton{public:voidrender(){std::cout<<"HTML 按钮标签\n";}voidonClick(){}};// ---- 创建者:工厂方法 + 业务逻辑 ----classDialog{public:virtual~Dialog(){}// 业务逻辑:只依赖抽象 Buttonvoidrender(){Button*ok=createButton();// 造ok->onClick();// 用ok->render();deleteok;// 裸指针:谁用谁删}protected:Dialog(){}// 默认构造// 工厂方法:纯虚,强制子类实现virtualButton*createButton()=0;private:Dialog(constDialog&);// 禁拷贝Dialog&operator=(constDialog&);};// ---- 具体创建者:只改写「造什么」----classWindowsDialog:publicDialog{protected:virtualButton*createButton(){returnnewWindowsButton();}};classWebDialog:publicDialog{protected:virtualButton*createButton(){returnnewHTMLButton();}};// ---- 客户端:按环境选择创建者 ----Dialog*makeDialog(conststd::string&os){if(os=="Windows")returnnewWindowsDialog();if(os=="Web")returnnewWebDialog();throwstd::runtime_error("未知环境: "+os);}GoF 的迷宫示例也是这个形态:MazeGame提供MakeMaze()/MakeRoom()/MakeWall()等带默认实现的虚函数,CreateMaze()骨架只调用它们;BombedMazeGame子类只需重写MakeRoom返回RoomWithABomb,即换掉整个产品族。两个例子共同印证了传统写法的两条铁律:产品必须共享基类/接口(否则子类换不了返回类型),裸指针必须明确「谁删」(示例中render()内造内删;跨函数返回时就得写进文档约定,正是裸指针时代的隐患所在)。
注意makeDialog里的 if-else 看起来回到了老路,但它选择的是创建者而非产品,且只在程序装配的一处调用;此后所有拿Dialog&的代码(render()及其下游)与具体产品彻底解耦。新增一种消息类型时,新写Button子类与Dialog子类各一个,已有的Dialog::render()与全部旧子类零改动——这就是「对修改关闭」的直观体验。
5. 现代 C++ 进阶写法
升级零:先解决裸指针。C++11 起工厂返回std::unique_ptr<Button>,render()里的delete消失,所有权由类型自证:
// 节选(Button/WindowsButton 定义见第 4 节)#include<memory>classModernDialog{public:voidrender(){// unique_ptr:离开作用域自动释放std::unique_ptr<Button>ok=createButton();ok->onClick();ok->render();}protected:virtualstd::unique_ptr<Button>createButton()=0;};classModernWindowsDialog:publicModernDialog{protected:std::unique_ptr<Button>createButton()override{returnstd::make_unique<WindowsButton>();}};产品定义沿用第 4 节的Button/WindowsButton即可。顺带的现代化红利:override显式标注重写(拼错函数名直接编译错误)、= default/= delete取代私有不定义的禁拷贝 hack、make_unique把「分配 + 构造」打包。
变体一:默认实现(GoF「两大变体」之一)。GoF 指出工厂方法有两种主要形态:创建者是抽象类、不提供实现,强制子类定义;或创建者是具体类、提供默认实现,子类按需覆盖。后者遵循一条实用规则:「在单独的操作中创建对象,使子类能改变创建方式」。C++ 里只要把= 0换成默认体:
// 节选(Button 系列定义同上,略)#include<memory>classDialog{protected:virtualstd::unique_ptr<Button>createButton(){returnstd::make_unique<WindowsButton>();}};变体二:参数化工厂方法(GoF)。工厂方法带上标识参数,一个方法造多种产品,GoF 给出的 C++ 骨架形如virtual Product* Create(ProductId),子类可以整体换掉映射关系:
#include<memory>enumclassProductId{Mine,Yours};classCreator{public:virtual~Creator()=default;virtualstd::unique_ptr<classProduct>create(ProductId id);};structProduct{virtual~Product()=default;};structMyProduct:Product{};structYourProduct:Product{};std::unique_ptr<Product>Creator::create(ProductId id){// 基类给默认映射,子类可整体覆盖if(id==ProductId::Mine)returnstd::make_unique<MyProduct>();if(id==ProductId::Yours)returnstd::make_unique<YourProduct>();returnnullptr;}改进一:自动注册工厂表。标准形态每加一类要多写一个 Creator,略繁琐;更现代的做法是把「类型名 → 构造函数」登记进一张表,新类型在自家 cpp 里自注册,与 RG「每添加新产品只需新增子类」的精神一致,只是把「子类重写」换成了「自注册」:
// handler_registry.h#include<functional>#include<map>#include<memory>#include<string>structMessageHandler{virtual~MessageHandler()=default;virtualvoidhandle()=0;};structHandlerRegistry{usingMaker=std::function<std::unique_ptr<MessageHandler>()>;std::map<std::string,Maker>makers;staticHandlerRegistry&get(){staticHandlerRegistry r;returnr;// Meyers' 单例(第 4 篇)}staticstd::unique_ptr<MessageHandler>create(conststd::string&type){auto&makers=get().makers;autoit=makers.find(type);returnit==makers.end()?nullptr:it->second();}};// 注册辅助模板template<typenameT>structHandlerReg{explicitHandlerReg(conststd::string&name){HandlerRegistry::get().makers[name]=[]{returnstd::make_unique<T>();};}};// login_handler.cpp// #include "handler_registry.h"// HandlerReg<LoginHandler> g_reg("login");注册动作由全局对象的构造完成(其构造在main前执行,get()里的局部静态恰好规避了静态顺序问题——与第 4 篇呼应)。新增消息类型现在只需一个文件:产品类加一行注册。
改进二(展望):C++20 的 concepts 可约束产品类型(如要求handle()存在),模板工厂make<T>()配合静态多态可去掉虚函数开销,代价是代码膨胀与显式实例化,留待进阶篇展开。
6. 优缺点与适用场景
- ✅ 优点(GoF 后果清单):调用方代码不再绑定具体产品类,只认 Product 接口,可配合任何用户定义的 ConcreteProduct;为子类提供挂钩(hook)——在类内部用工厂方法造对象,永远比直接造更灵活,子类能提供对象的扩展版本;还可连接平行的类层次(两个层次各自演化、由工厂方法配对,如「图形」与「操纵器」)。
- ❌ 缺点:GoF 明说——客户端可能仅仅为了创建一个特定产品,就不得不派生 Creator 子类;若客户端本来就要子类化 Creator 那无所谓,否则就是多了一个演化点。加上每加一种产品要加一对类(产品 + 工厂),类型数量膨胀;注册表方案则引入全局状态与初始化时序的心智负担。
- 🎯 适用场景(综合两份资料):调用方不应/无法知道具体类型(只有字符串、配置、协议号);类型预计持续增加(插件、协议、驱动);框架知道何时创建但不知道创建什么(GoF 的 Document/Application 困境)。类型少而稳定时,简单工厂甚至直接
make_unique<T>就够,别为了模式而模式。
〔辨析〕简单工厂 vs 工厂方法:前者是「一个函数管的 if-else」,后者是「子类各管各的重写」;类型少而稳定选前者,类型多而常增选后者。工厂方法 vs 抽象工厂(第 6 篇):前者造一个产品,后者造一族相关产品,且抽象工厂通常只是「一组工厂方法的打包」——RG 顺带剧透了演进方向:每往对话框里多加一个工厂方法(按钮、输入框……),你就离抽象工厂更近一步。三者是递进关系而非对立关系。
7. 开源项目中的身影
三个真实 C++ 库对「造对象」的封装,分别对应本文的三个层次。
Boost:把工厂做成函数对象。Boost.Functional/Factory 提供了两个极简模板——boost::factory<T*>把new T打包成可调用的函数对象,boost::value_factory<T>则封装「不 new 的构造调用」。它的价值不在代码量,而在工厂的泛型化:任何接收「可造 T 的东西」的泛型代码,都可以把factory<T*>当参数传进去,配合分配器还能接管内存来源:
// 说明性片段(需链接 Boost,略去分配器细节)#include<boost/functional/factory.hpp>#include<boost/functional/value_factory.hpp>boost::factory<Button*>fp;// 封装 new 表达式Button*b=fp();// 调用即创建,需自管释放boost::value_factory<WindowsButton>vf;WindowsButton wb=vf();// 栈上构造,无需 new点评:这是 GoF「在单独操作中创建对象」规则的模板化极致——工厂本身成了一等公民。但在unique_ptr时代它略显尴尬:裸指针交货的语义,使用者仍要自己包一层智能指针。
POCO:注册表式工厂的两层设计。POCO Foundation 用AbstractFactory.h+DynamicFactory.h实现按名字造对象:AbstractFactory<Base>声明纯虚create(),Instantiator<C, Base>模板实现之(内部就是new C);DynamicFactory<Base>在其上叠一张注册表,registerClass("name")/createInstance("name")完成字符串到实例的映射:
// 说明性片段(节选自 POCO,签名有简化)Poco::DynamicFactory<Button>fac;fac.registerClass<WindowsButton>("Windows");// 注册 Instantiatorfac.registerClass<HTMLButton>("Web");std::unique_ptr<Button>b(fac.createInstance("Windows"));点评:与本文第 5 节的手写注册表逐条对应——AbstractFactory是「简单工厂的抽象化」,Instantiator是「具体工厂的模板化」,DynamicFactory是「注册表」。三职责拆成三层,比一坨 if-else 干净得多;代价是registerClass抛异常处理重名、工厂接管 Instantiator 生命周期等细节,都要读文档才能安全使用。
AOSP:框架里的宏工厂。Android 的 Binder 体系大量依赖工厂思想:IInterface::asInterface(const sp<IBinder>&)由DECLARE_META_INTERFACE/IMPLEMENT_META_INTERFACE宏展开,客户端拿到远端 binder 句柄后,由它决定包成本地桩还是代理对象返回——调用方只见sp<IXxx>接口,正是「框架知道何时创建、不知道创建什么」的教科书解法。而defaultServiceManager()这类工厂函数则把「服务管理器从哪来」的细节(进程内单例 + binder 驱动协商)整体封装。
点评:AOSP 的实现带着浓厚的宏文化色彩——用预处理生成样板代码,编译期零开销但可读性打折;它示范了工厂方法在 C++ 里的另一条路线:不靠虚函数子类化,靠模板/宏在编译期生成「具体创建者」。三份代码合看:Boost 把工厂泛型化、POCO 把工厂注册化、AOSP 把工厂宏化——模式意图三十年来没变,变的只是 C++ 表达意图的手段。
顺带一提标准库自己:std::make_unique/std::make_shared就是每个人天天在用的工厂函数,调用方写make_unique<T>却并不new,构造细节(尤其是控制块与对象合并分配)被工厂封装。
本篇小结
从一坨 if-else 出发:简单工厂把创建逻辑收拢到一处,代价是每加类型改一次函数;工厂方法——「虚拟构造器」——把「造什么」下放给子类重写,用一对新类换来旧码零改动,也让框架摆脱「知道何时、不知道造什么」的困境;GoF 的两大变体(抽象强制实现 / 默认实现可选覆盖)与参数化工厂方法给了它足够的弹性;现代 C++ 再用智能指针明确所有权、用注册表 + lambda 把「新增成本」压到一个文件一行注册。选型口诀:两三种稳定类型用简单工厂,频繁新增用工厂方法/注册表,跨平台成族创建留给下一节的抽象工厂。
本文模式定义与角色划分参考了 Refactoring Guru《设计模式》中文版「工厂方法」一章,意图译文、两大变体、参数化工厂方法与后果清单参考了 GoF《Design Patterns》第 3 章 Factory Method 一节。