十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【设计模式精讲】5.工厂方法(Factory Method)

【设计模式精讲】5.工厂方法(Factory Method) 【设计模式精讲】5.简单工厂 → 工厂方法Factory Method【摘要】「按类型造一个对象」的需求几乎每个项目都有也几乎每个项目都写过那坨越积越长的 if-else。本文从消息处理器的新增成本讲起先实现简单工厂并指出它对开闭原则的违背再演进到工厂方法模式GoF 称之为「虚拟构造器」把「实例化哪个类」的决定权交给子类。C 实现以 Refactoring Guru 的跨平台对话框为例并覆盖 GoF 介绍的两大变体——抽象创建者与默认实现、带参数的工厂方法随后给出智能指针返回与自动注册工厂表两种现代化改造。文末按类型数量给出选型建议类型少且稳定用简单工厂类型多且频繁新增才值得上工厂方法。【关键词】工厂方法、简单工厂、开闭原则、虚拟构造器、自动注册、多态1. 每加一种消息就要改三处代码网络模块收到消息后按type字段分发处理// 说明性片段省略各 Handler 的定义// ❌ 每新增一种消息都要改这里MessageHandler*createHandler(conststd::stringtype){if(typelogin)returnnewLoginHandler();if(typechat)returnnewChatHandler();if(typefile)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 图 结构说明依赖抽象«abstract»CreatorcreateButton() : Button*render() : voidWindowsCreatorcreateButton() : WindowsButtonWebCreatorcreateButton() : HTMLButton«interface»Buttonrender() : voidonClick() : voidWindowsButtonrender() : voidonClick() : voidHTMLButtonrender() : voidonClick() : voidRefactoring Guru 把参与者拆成四个角色产品Product接口所有具体产品必须实现的公共接口Button具体产品Concrete Products接口的不同实现WindowsButton、HTMLButton创建者Creator类声明返回产品对象的工厂方法createButton()既可声明为抽象方法强制子类实现也可提供返回默认产品的实现具体创建者Concrete Creators重写工厂方法使其返回不同类型的产品。有一个极易被忽略的定性值得原样抄下来尽管名字叫「创建者」它的主要职责并不是创建产品。创建者类通常包含一些与产品相关的核心业务逻辑如render()画对话框工厂方法将这些逻辑与具体产品类解耦。RG 的比方是大型软件公司设有程序员培训部门但公司的主业是写代码而不是「生产程序员」。4. 传统 C 写法C11 之前按 GoF 原书时代的形态实现一遍裸指针交货、没有override/nullptr/ default禁拷贝靠「私有且不定义」。示例改编自 Refactoring Guru 的跨平台对话框基础Dialog用抽象Button渲染窗口Windows 和 Web 环境各自子类化无需重写对话框逻辑// C98/03 写法#includeiostream#includestdexcept#includestring// ---- 产品接口与具体产品 ----classButton{public:virtual~Button(){}virtualvoidrender()0;virtualvoidonClick()0;};classWindowsButton:publicButton{public:voidrender(){std::coutWin 风格按钮\n;}voidonClick(){}};classHTMLButton:publicButton{public:voidrender(){std::coutHTML 按钮标签\n;}voidonClick(){}};// ---- 创建者工厂方法 业务逻辑 ----classDialog{public:virtual~Dialog(){}// 业务逻辑只依赖抽象 Buttonvoidrender(){Button*okcreateButton();// 造ok-onClick();// 用ok-render();deleteok;// 裸指针谁用谁删}protected:Dialog(){}// 默认构造// 工厂方法纯虚强制子类实现virtualButton*createButton()0;private:Dialog(constDialog);// 禁拷贝Dialogoperator(constDialog);};// ---- 具体创建者只改写「造什么」----classWindowsDialog:publicDialog{protected:virtualButton*createButton(){returnnewWindowsButton();}};classWebDialog:publicDialog{protected:virtualButton*createButton(){returnnewHTMLButton();}};// ---- 客户端按环境选择创建者 ----Dialog*makeDialog(conststd::stringos){if(osWindows)returnnewWindowsDialog();if(osWeb)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 进阶写法升级零先解决裸指针。C11 起工厂返回std::unique_ptrButtonrender()里的delete消失所有权由类型自证// 节选Button/WindowsButton 定义见第 4 节#includememoryclassModernDialog{public:voidrender(){// unique_ptr离开作用域自动释放std::unique_ptrButtonokcreateButton();ok-onClick();ok-render();}protected:virtualstd::unique_ptrButtoncreateButton()0;};classModernWindowsDialog:publicModernDialog{protected:std::unique_ptrButtoncreateButton()override{returnstd::make_uniqueWindowsButton();}};产品定义沿用第 4 节的Button/WindowsButton即可。顺带的现代化红利override显式标注重写拼错函数名直接编译错误、 default/ delete取代私有不定义的禁拷贝 hack、make_unique把「分配 构造」打包。变体一默认实现GoF「两大变体」之一。GoF 指出工厂方法有两种主要形态创建者是抽象类、不提供实现强制子类定义或创建者是具体类、提供默认实现子类按需覆盖。后者遵循一条实用规则「在单独的操作中创建对象使子类能改变创建方式」。C 里只要把 0换成默认体// 节选Button 系列定义同上略#includememoryclassDialog{protected:virtualstd::unique_ptrButtoncreateButton(){returnstd::make_uniqueWindowsButton();}};变体二参数化工厂方法GoF。工厂方法带上标识参数一个方法造多种产品GoF 给出的 C 骨架形如virtual Product* Create(ProductId)子类可以整体换掉映射关系#includememoryenumclassProductId{Mine,Yours};classCreator{public:virtual~Creator()default;virtualstd::unique_ptrclassProductcreate(ProductId id);};structProduct{virtual~Product()default;};structMyProduct:Product{};structYourProduct:Product{};std::unique_ptrProductCreator::create(ProductId id){// 基类给默认映射子类可整体覆盖if(idProductId::Mine)returnstd::make_uniqueMyProduct();if(idProductId::Yours)returnstd::make_uniqueYourProduct();returnnullptr;}改进一自动注册工厂表。标准形态每加一类要多写一个 Creator略繁琐更现代的做法是把「类型名 → 构造函数」登记进一张表新类型在自家 cpp 里自注册与 RG「每添加新产品只需新增子类」的精神一致只是把「子类重写」换成了「自注册」// handler_registry.h#includefunctional#includemap#includememory#includestringstructMessageHandler{virtual~MessageHandler()default;virtualvoidhandle()0;};structHandlerRegistry{usingMakerstd::functionstd::unique_ptrMessageHandler();std::mapstd::string,Makermakers;staticHandlerRegistryget(){staticHandlerRegistry r;returnr;// Meyers 单例第 4 篇}staticstd::unique_ptrMessageHandlercreate(conststd::stringtype){automakersget().makers;autoitmakers.find(type);returnitmakers.end()?nullptr:it-second();}};// 注册辅助模板templatetypenameTstructHandlerReg{explicitHandlerReg(conststd::stringname){HandlerRegistry::get().makers[name][]{returnstd::make_uniqueT();};}};// login_handler.cpp// #include handler_registry.h// HandlerRegLoginHandler g_reg(login);注册动作由全局对象的构造完成其构造在main前执行get()里的局部静态恰好规避了静态顺序问题——与第 4 篇呼应。新增消息类型现在只需一个文件产品类加一行注册。改进二展望C20 的 concepts 可约束产品类型如要求handle()存在模板工厂makeT()配合静态多态可去掉虚函数开销代价是代码膨胀与显式实例化留待进阶篇展开。6. 优缺点与适用场景✅ 优点GoF 后果清单调用方代码不再绑定具体产品类只认 Product 接口可配合任何用户定义的 ConcreteProduct为子类提供挂钩hook——在类内部用工厂方法造对象永远比直接造更灵活子类能提供对象的扩展版本还可连接平行的类层次两个层次各自演化、由工厂方法配对如「图形」与「操纵器」。❌ 缺点GoF 明说——客户端可能仅仅为了创建一个特定产品就不得不派生 Creator 子类若客户端本来就要子类化 Creator 那无所谓否则就是多了一个演化点。加上每加一种产品要加一对类产品 工厂类型数量膨胀注册表方案则引入全局状态与初始化时序的心智负担。 适用场景综合两份资料调用方不应/无法知道具体类型只有字符串、配置、协议号类型预计持续增加插件、协议、驱动框架知道何时创建但不知道创建什么GoF 的 Document/Application 困境。类型少而稳定时简单工厂甚至直接make_uniqueT就够别为了模式而模式。〔辨析〕简单工厂 vs 工厂方法前者是「一个函数管的 if-else」后者是「子类各管各的重写」类型少而稳定选前者类型多而常增选后者。工厂方法 vs 抽象工厂第 6 篇前者造一个产品后者造一族相关产品且抽象工厂通常只是「一组工厂方法的打包」——RG 顺带剧透了演进方向每往对话框里多加一个工厂方法按钮、输入框……你就离抽象工厂更近一步。三者是递进关系而非对立关系。7. 开源项目中的身影三个真实 C 库对「造对象」的封装分别对应本文的三个层次。Boost把工厂做成函数对象。Boost.Functional/Factory 提供了两个极简模板——boost::factoryT*把new T打包成可调用的函数对象boost::value_factoryT则封装「不 new 的构造调用」。它的价值不在代码量而在工厂的泛型化任何接收「可造 T 的东西」的泛型代码都可以把factoryT*当参数传进去配合分配器还能接管内存来源// 说明性片段需链接 Boost略去分配器细节#includeboost/functional/factory.hpp#includeboost/functional/value_factory.hppboost::factoryButton*fp;// 封装 new 表达式Button*bfp();// 调用即创建需自管释放boost::value_factoryWindowsButtonvf;WindowsButton wbvf();// 栈上构造无需 new点评这是 GoF「在单独操作中创建对象」规则的模板化极致——工厂本身成了一等公民。但在unique_ptr时代它略显尴尬裸指针交货的语义使用者仍要自己包一层智能指针。POCO注册表式工厂的两层设计。POCO Foundation 用AbstractFactory.hDynamicFactory.h实现按名字造对象AbstractFactoryBase声明纯虚create()InstantiatorC, Base模板实现之内部就是new CDynamicFactoryBase在其上叠一张注册表registerClass(name)/createInstance(name)完成字符串到实例的映射// 说明性片段节选自 POCO签名有简化Poco::DynamicFactoryButtonfac;fac.registerClassWindowsButton(Windows);// 注册 Instantiatorfac.registerClassHTMLButton(Web);std::unique_ptrButtonb(fac.createInstance(Windows));点评与本文第 5 节的手写注册表逐条对应——AbstractFactory是「简单工厂的抽象化」Instantiator是「具体工厂的模板化」DynamicFactory是「注册表」。三职责拆成三层比一坨 if-else 干净得多代价是registerClass抛异常处理重名、工厂接管 Instantiator 生命周期等细节都要读文档才能安全使用。AOSP框架里的宏工厂。Android 的 Binder 体系大量依赖工厂思想IInterface::asInterface(const spIBinder)由DECLARE_META_INTERFACE/IMPLEMENT_META_INTERFACE宏展开客户端拿到远端 binder 句柄后由它决定包成本地桩还是代理对象返回——调用方只见spIXxx接口正是「框架知道何时创建、不知道创建什么」的教科书解法。而defaultServiceManager()这类工厂函数则把「服务管理器从哪来」的细节进程内单例 binder 驱动协商整体封装。点评AOSP 的实现带着浓厚的宏文化色彩——用预处理生成样板代码编译期零开销但可读性打折它示范了工厂方法在 C 里的另一条路线不靠虚函数子类化靠模板/宏在编译期生成「具体创建者」。三份代码合看Boost 把工厂泛型化、POCO 把工厂注册化、AOSP 把工厂宏化——模式意图三十年来没变变的只是 C 表达意图的手段。顺带一提标准库自己std::make_unique/std::make_shared就是每个人天天在用的工厂函数调用方写make_uniqueT却并不new构造细节尤其是控制块与对象合并分配被工厂封装。本篇小结从一坨 if-else 出发简单工厂把创建逻辑收拢到一处代价是每加类型改一次函数工厂方法——「虚拟构造器」——把「造什么」下放给子类重写用一对新类换来旧码零改动也让框架摆脱「知道何时、不知道造什么」的困境GoF 的两大变体抽象强制实现 / 默认实现可选覆盖与参数化工厂方法给了它足够的弹性现代 C 再用智能指针明确所有权、用注册表 lambda 把「新增成本」压到一个文件一行注册。选型口诀两三种稳定类型用简单工厂频繁新增用工厂方法/注册表跨平台成族创建留给下一节的抽象工厂。本文模式定义与角色划分参考了 Refactoring Guru《设计模式》中文版「工厂方法」一章意图译文、两大变体、参数化工厂方法与后果清单参考了 GoF《Design Patterns》第 3 章 Factory Method 一节。
返回列表