YAOTU INSIGHTS

C++工厂模式进阶:注册表、参数化与依赖注入实战

C++工厂模式进阶:注册表、参数化与依赖注入实战
工厂模式这名字听着基础但大部分人的认知停留在“一个switch返回new出来的对象”这个层面。真的到了核心业务里你会发现这种写法没法用因为订单状态流转、货币通道适配、推荐规则引擎、客服工单分类这些场景对创建逻辑的要求根本不是“选个类型”那么简单而是创建前要拉数据、创建后要挂拦截器、创建失败要降级、并发下还要控制实例数。这篇文章把我在几个核心系统里用到的工厂模式高级玩法拆开讲包括如何用注册表消灭switch、如何用std::function实现参数化工厂、如何和依赖注入配合以及那些不跑一次根本发现不了的坑。适合谁看有一定C基础写过简单工厂但觉得不够用想升级设计水平的开发。这篇文章全部代码基于C17编写但我会标注哪些可以向下兼容到C11。1. 为什么基础工厂模式撑不住复杂业务工厂模式的核心思想是把“创建对象”这件事封装起来让调用方不知道具体类型也不知道创建细节。这种思路本身没问题但很多人的实现停留在简单工厂层面根据枚举值走switch分支在case里new出来然后返回基类指针。这套写法在小项目里看着还行一旦业务复杂起来到处都会冒烟。1.1 简单工厂的三个致命伤第一个问题是创建逻辑全是线性的。业务里做推荐、做营销、做结算每种策略创建之前要查配置、要校验开关、要处理灰度这些准备工作如果全部写在同一个工厂函数里这个函数就会变成几百行的大泥球。我见过一个订单处理工厂函数里有六七个分支每个分支里塞了十几个if根本没有可读性。第二个问题是违背开闭原则。每新增一种产品你就要打开工厂函数新增一个case。这个操作本身不难难的是你根本不知道应该在哪个文件里加。很多项目里工厂函数散布在好几个文件里想加新策略得先全部找一遍加完还要小心不要破坏别人的分支。第三个问题是单例和复用逻辑没法处理。简单工厂默认每次都new但很多策略对象是轻量的、无状态的根本不需要反复创建。还有些东西本身就必须单例比如日志客户端、数据库连接池的封装层简单工厂完全没有控制权。1.2 真实业务场景中的复杂需求我举一个实际的例子做一个订单履约模块路径有普通快递、同城配送、自提、虚拟商品发货。同一个订单可能走多种履约方式每个履约服务内部要初始化物流渠道、安排取件、对接仓储系统而且不同履约方式之间还有优先级和降级关系。如果用简单工厂所有创建逻辑堆在一起加新的履约方式时必须改动核心工厂代码测试成本高回归风险大。再比如做支付网关适配你接入了微信支付、支付宝、银行卡直连、余额支付每种支付方式的实现类构造参数完全不同微信要appId和商户号支付宝要应用私钥银行卡要读取通道配置余额支付只需要一个用户ID。这种场景统一“走switch然后new一个固定类型”根本处理不了必须在创建之前做差异化的参数准备。这些需求指向一个结论工厂模式要升级升级方向不是增加分支而是要解耦、要自动化注册、要能处理异构参数、要能和其他设计模式配合。2. 从简单工厂到抽象工厂产品族的整体装配简单工厂解决的是单个产品的创建抽象工厂解决的是“一群相关产品”的创建。很多人在理解抽象工厂时停留在概念层面实际用得很少因为C里面“产品族”这个概念落地并不容易真正落地的时候需要设计出“抽象工厂”和“抽象产品”两层接口。2.1 什么时候必须上抽象工厂当一个系统需要同时支持多套“成套”的实现时就该考虑抽象工厂了。我举个典型例子做一个数据分析模块需要支持两种数据源业务库和数仓。连业务库时需要创建主库连接器、只读副本连接器、历史归档连接器连数仓时需要创建批量抽取器、流式读取器、临时表管理器。这三组对象内部是有关联的业务库的主库连接器和只读副本连接器要共享账号体系数仓的三个组件要共享队列深度配置。如果你用简单工厂分别创建这些对象很容易出现混搭业务库的主库连接器配了数仓的归档管理器配置文件里哪两个组件是配套的没个准。抽象工厂的思路是先定义一个“数据源工厂”接口接口里有三个创建方法返回三个不同类型的抽象产品。业务库提供一个具体工厂创建业务库的三个实现数仓提供一个具体工厂创建数仓的三个实现。调用方只依赖抽象工厂接口不关心你底层是哪个工厂保证产品族内部永远是配套的。2.2 C实现抽象工厂的要点C里实现抽象工厂关键是“抽象产品”的含义要做对。连接器不是概念上的抽象产品它需要真正有虚接口open、close、execute。三个抽象产品加一个抽象工厂接口代码结构大概是这样的class DbConnection { public: virtual ~DbConnection() default; virtual void open() 0; virtual void close() 0; virtual QueryResult execute(const std::string sql) 0; }; class ReplicaConnection { public: virtual ~ReplicaConnection() default; virtual void open() 0; virtual bool isReadOnly() 0; }; class ArchiveConnection { public: virtual ~ArchiveConnection() default; virtual void open() 0; virtual void archive(const std::string table) 0; }; class DataSourceFactory { public: virtual ~DataSourceFactory() default; virtual std::unique_ptrDbConnection createMain() 0; virtual std::unique_ptrReplicaConnection createReplica() 0; virtual std::unique_ptrArchiveConnection createArchive() 0; };然后业务库实现一个BusinessDbFactory : public DataSourceFactory数仓实现一个WarehouseFactory : public DataSourceFactory每一个创建方法里按各自数据源的需求构造实现类。这里有几个要点。抽象工厂的成员函数返回的必须是抽象产品指针或智能指针不能返回具体的实现类指针否则调用方就收到了类型信息破坏了封装。抽象工厂一般不注入参数如果不同的具体工厂需要的参数差异很大应该用配置对象创建时内部自行解析。2.3 抽象工厂的坑不要让工厂和策略混在一起工作中最常见的错误是把抽象工厂做成“猜你喜欢”也就是在调用方先用一个路由函数做判断再选择具体工厂。这其实没问题但要注意路由逻辑不应该放在工厂内部。抽象工厂接口里只有一个方法族选择不关心你是因为数据源类型还是环境变量选中的它。如果你发现抽象工厂内部出现了类似“if (type ...)”这种逻辑说明你把路由和装配混在一起了应该把这个判断挪到上层一个专门的工厂选择器里。这样做的好处是路由逻辑可以单独测试可以做成配置驱动而抽象工厂保持纯粹的装配职责。我个人的经验在使用抽象工厂时一定配合智能指针使用。C里返回原始指针调用方要记得delete在异常路径上很容易漏。C11以后返回unique_ptr是最自然的做法C17里意味着调用方可以非常自然地处理。3. 注册表工厂让新类型自动登记简单工厂的分支要手动维护新增类型要改工厂代码这不仅是代码改动问题还容易改出冲突。注册表工厂的思路是每个具体类在定义时自动向工厂注册自己工厂内部维护一个unordered_map映射类型名到构造函数调用方只需传入名称。3.1 注册机制的核心代码先看这个工厂的数据结构class WidgetFactory { public: using Creator std::functionstd::unique_ptrWidget(); static WidgetFactory instance() { static WidgetFactory factory; return factory; } bool reg(const std::string name, Creator creator) { registry_[name] std::move(creator); return true; } std::unique_ptrWidget create(const std::string name) { auto it registry_.find(name); if (it registry_.end()) { return nullptr; } return (it-second)(); } private: WidgetFactory() default; std::unordered_mapstd::string, Creator registry_; };然后为每个具体类写一个注册辅助结构class ButtonWidget : public Widget { public: static constexpr char kRegisteredName[] button; static std::unique_ptrWidget create() { return std::make_uniqueButtonWidget(); } static bool ensureRegistered() { static bool registered WidgetFactory::instance().reg( kRegisteredName, ButtonWidget::create); return registered; } };这个ensureRegistered看起来很绕但实际用法很简单每个具体源文件里放一个全局静态变量初始化时调用ensureRegistered即可// button_widget.cpp namespace { const bool kButtonRegistered ButtonWidget::ensureRegistered(); }这样做的原理是静态变量在程序启动时初始化初始化时执行注册函数注册函数把创建器和名称写入工厂的unordered_map。所有包含这些源文件的模块只要链接进去程序启动后工厂就自动知道所有类型的存在。如果不用注册表方案“新增类型”的工作流就是写类、写注册辅助代码、在工厂里加一个枚举和case。用了注册表新增类型不必改动任何现有文件真正做到了对修改关闭、对扩展开放。3.2 静态初始化顺序的坑这里有一个严重的坑必须讲清楚不同编译单元中静态局部变量的初始化顺序是未定义的。如果WidgetFactory::instance()本身的静态局部变量和某个注册辅助的静态变量如果是在不同编译单元里初始化的那么很可能出现注册时工厂还不存在的情况这是C的静态初始化顺序陷阱。解决方案我在代码里已经做了就是reg函数不会强制要求工厂已初始化因为它可以在第一次调用时创建。严格来说那还不行因为WidgetFactory::instance()返回的引用本身可能还没初始化。实际工程上更常见的做法是让注册函数使用std::once_flag或者干脆在第一次调用时惰性初始化注册本身。在C11里instance()内部的static变量是线程安全的这一层已经没问题了。真正的问题是button_widget.cpp中的静态变量初始化发生在工厂初始化之前导致注册时调用instance()时工厂还没初始化。在实际项目中我遇到过解决办法是把注册这行放在一个专门用来触发注册的函数里在主流程启动时显式调用一次别全依赖静态对象初始化或者使用std::call_once来确保注册发生在主流程初始化阶段。更干净的做法是使用“registration token”模式通过宏包装注册逻辑在生成类时直接指定注册名称并利用内联静态变量在类定义时完成注册#define REGISTER_WIDGET(WidgetClass, WidgetName) \ static bool _reg_##WidgetClass() { \ return WidgetFactory::instance().reg(WidgetName, [] { \ return std::make_uniqueWidgetClass(); \ }); \ } \ static const bool _registered_##WidgetClass _reg_##WidgetClass();这个宏放在头文件里WidgetClass在头文件定义时就会调用注册函数。内联静态变量在C17中保证只有一个实例且每个使用该头文件的编译单元都会初始化一次但由于是内联的实际只初始化一次。这里仍然有跨编译单元的问题所以一定要在主程序的早期阶段调用一次WidgetFactory::ensureInitialized()之类的兜底函数。3.3 注册表工厂适合什么场景注册表工厂最适合的是策略数量多、类型经常扩展、且策略本身有一定独立性的场景。例如推荐系统的召回策略、支付渠道适配器、导出文件格式生成器。这些场景最大的痛点是想加一种新策略必须在工厂代码里加分支而且所有策略实现类都要在工厂所在的模块里被看到形成不必要的耦合。注册表方案完美解决这个问题新增策略只需新增一个文件文件里定义实现类和注册逻辑无需触碰任何现有代码。上层的策略选择逻辑通过名称路由即可甚至可以通过配置决定用哪些策略。这个模式在插件化架构里非常常见是构建可扩展系统的首选。不过注册表方案不适合那种类型数量很少、没有扩展需求的场景。比如系统里只有两种登录方式且基本不会再加了用注册表就有点杀鸡用牛刀代码结构反而显得复杂。这种场景用简单的工厂函数就好。4. 参数化工厂让创建器适配异构构造参数抽象工厂和注册表工厂解决的是“按类型创建”的问题但真实业务里经常出现“按参数创建”的需求。不同的策略类需要的构造参数不同有的要字符串配置有的要数值参数有的要依赖其他对象。这种场景传统的工厂接口很难设计因为不可能预先定义一套参数类型把所有情况都包住。4.1 std::function和可变参数模板的组合C14和C17提供了可变参数模板可以设计一个工厂方法传入多个参数内部自行路由到不同构造器。思路是用std::function作为统一的创建器类型内部通过参数包和参数数量来判别调用哪个构造器。class ParamFactory { public: using AnyCreator std::functionvoid*(); template typename T, typename... Args static T* create(Args... args) { return new T(std::forwardArgs(args)...); } };这个模板本身不强核心在于如何把带有不同参数列表的构造器存储为同一类型。这里要用一个小技巧lambda捕获参数包然后转换为std::functiontemplate typename T, typename... Args auto make_creator(Args... args) { return [args std::make_tuple(std::forwardArgs(args)...)]() mutable { return std::apply([](auto... a) { return std::make_uniqueT(std::forwarddecltype(a)(a)...); }, args); }; }这个写法要求C17的std::apply。它把所有参数封装成tuplelambda捕获后调用时再解包传给T的构造函数。这样make_creator返回的lambda可以作为统一的Creator存储在工厂里。这确实可以做到但代码可读性不理想。我在实际项目中很少把参数包做成tuple这么极端。多数情况下我宁可把参数打包成一个std::variant或多个重载也不愿意让工厂内部处理tuple展开因为一旦参数数量变化排查起来非常痛苦。4.2 更实用的方案把参数做成配置对象或variant与其在工厂层面做参数包展开不如把“创建策略所需的参数”定义为一个结构体或std::variant再在创建时解析。比如定义struct WidgetConfig { std::string name; int width; int height; std::optionalstd::string tooltip; std::optionalstd::string style; };然后创建一个Widgetstd::unique_ptrWidget makeWidgetByConfig(const WidgetConfig cfg) { std::string name cfg.name; if (cfg.style cfg style dark) ... }这样工厂逻辑在makeWidgetByConfig里做各个参数有明确的字段名不会出现参数位置顺序错了还编译通过的情况。这比可变参数模板更能落实在工程里。4.3 多参数时如何维护如果你的策略类型实在太多参数结构体也会变得失控这时候可以按策略分组把每个策略类型的配置文件独立开来工厂内部做一个配置解析函数映射。比如class StrategyConf { public: virtual ~StrategyConf() default; ... }; class PaymentConf : public StrategyConf { public: std::string appId; std::string mchId; std::string apiKey; };工厂创建时根据要创建的产品类型从配置中心取出对应的配置子类再调用对应的构建函数。这一步并没有改变工厂模式的本质而是把一个参数问题转化成了一个对象问题在复杂的业务系统中非常常见。4.4 推荐做法参数差异较大时一定不要在一开始就把所有参数塞进同一个工厂接口。先用配置对象统一入口做简单参数分发参数类型稳定之后再根据场景优化。不要在创建逻辑里塞一堆默认参数重载那会迷失焦点。另外一个常见做法是builder和工厂的结合先让工厂拿到一个builder对象调用方往builder里填参数工厂最后调用builder的build完成创建。这本质上是工厂掺了建造者模式适合参数非常多、必须校验组合合法性的场景。5. 工厂模式和依赖注入的结合依赖注入容器本质上就是一个“高级注册表工厂”。很多现代框架的容器核心就是注册一个类型到实例的映射需要时按名称或类型取出来。C没有自带容器但可以用工厂模式轻松模拟一个简单的DI容器帮助理清对象装配关系特别是在单元测试里非常有用。5.1 手写一个简易DI容器简单版容器的代码结构如下class DiContainer { public: using Factory std::functionstd::shared_ptrvoid(); template typename T void bind(std::shared_ptrT instance) { bindType(typeid(T).name(), [instance]() { return std::shared_ptrvoid(instance); }); } template typename T, typename... Args void bindFactory(std::functionstd::shared_ptrT(Args...) factory) { bindType(typeid(T).name(), [factory]() { return std::shared_ptrvoid(factory()); }); } template typename T std::shared_ptrT resolve() { auto it factories_.find(typeid(T).name()); if (it factories_.end()) { return nullptr; } return std::static_pointer_castT(it-second()); } private: void bindType(const std::string key, Factory factory) { factories_[key] std::move(factory); } std::unordered_mapstd::string, Factory factories_; };这其实就是一个按类型名称路由的注册表工厂。绑定什么类型就能取出什么类型。如果你的业务要求按接口类型绑定实现类加一层接口到实现的映射即可。在实践里这种手写容器最多用于测试和中小型项目。大型项目要考虑生命周期管理、作用域、循环依赖检测这些是一个完整容器的复杂功能工厂模式只是其中最基础的一部分。5.2 在单元测试中替换实现依赖注入最大的好处是测试时可以替换真实依赖。我们用工厂模式实现这一点调用方不直接依赖具体类而是从工厂取。测试时只需要往工厂里注册mock对象class MockOrderService : public IOrderService { public: MOCK_METHOD(bool, submit, (const Order), (override)); }; TEST_F(OrderServiceTest, SubmitOrder) { DiContainer container; auto mock std::make_sharedMockOrderService(); container.bindIOrderService(mock); // 用 container.resolveIOrderService() 创建被测对象 auto serviceUnderTest std::make_sharedOrderProcessor( container.resolveIOrderService()); ... }这里的关键是工厂必须支持覆盖注册测试代码才能在用例执行前替换实现。如果你的工厂注册表是全局单例要注意每个测试结束后清理否则一个用例里面注册的mock会泄漏到下一个用例。这个场景还暴露一个重要细节工厂不应该只创建经过它注册的对象还应该允许调用方为不同上下文传入一个“上下文相关工厂”。最简单的形式是给工厂创建一个供应者接口class IClock { public: virtual std::chrono::system_clock::time_point now() 0; };业务代码通过工厂拿IClock而不是直接调用std::chrono::system_clock::now()。这样在测试时可以注入一个固定时间的假时钟在真实运行中注入真实时钟。这是依赖注入结合工厂最常见的用法。5.3 抽象工厂和注入的边界很多人混淆抽象工厂和注入容器。简单区分抽象工厂调用方直接调用工厂接口的创建方法目标对象由具体工厂负责注入容器则是调用方通过容器按类型获取对象容器内部可以有一系列创建逻辑、装配逻辑。两者并不互斥很多项目里容器内部就是用注册表工厂实现的上层再提供抽象工厂的接口给调用方。实践上不要过度设计。如果只是为了测试时替换一个依赖直接在测试代码new一个mock传进去就好没有必要专门建一个抽象工厂。只有当你需要在多个地方重复选择不同类型、并需要保持选择逻辑一致时抽象工厂才有价值。6. 常见问题与排查技巧实录工厂模式在上手时看着简单真正放到业务里调试时各种隐藏问题会让你怀疑是不是写错了模式。我把这几年踩过坑最多的问题整理出来希望能帮你避开。6.1 注册表中类型名和逻辑名不一致注册表工厂用名称路由最常犯的错误是注册名称和外部传入名称不一致。例如在业务代码里用配置文件中的btn_default请求一个按钮但注册表里注册的是buttoncreate返回nullptr。排查感受代码逻辑没有任何问题就是返回空没有任何错误提示。建议在create函数里做两件事找不到先打一条warning日志同时支持别名注册配置名和类型名可以做映射。别名不一定在注册表里做也可以在路由层做。无论如何日志必须带上注册名和请求名这样一眼就能看到差异。6.2 返回裸指针导致资源泄漏很多老代码习惯返回裸指针调用方也忘了delete。工厂模式如果返回裸指针等于把资源管理的责任重新抛回给了调用方。有些人说“我所有调用方都会delete的没问题”但异常路径上根本来不及delete。更隐蔽的情况是create内部走了分支A正常返回分支B抛了异常但分支B之前new了一个临时对象new完了才抛异常这个对象就泄漏了。所以基本上我只推荐返回智能指针。如果工厂必须返回裸指针那也应该是工厂内部自行管理生命周期返回一个观察用的非拥有指针。大多数场景下用unique_ptr就够了只有在多个对象需要共享时才用shared_ptr。6.3 静态注册不生效create不到对象静态注册最让人抓狂的问题是“明明写了注册代码但运行时查不到”。原因往往是链接器优化把那个编译单元优化掉了或者全局静态变量初始化顺序问题。排查方案显式在main函数开头调用一次注册触发函数例如WidgetFactory::forceLoadAll()强制让所有模块的注册代码都执行。这种“强制加载”函数在插件架构里相当常见做法是遍历某个全局注册列表调用每个模块的初始化函数。在C17标准下内联静态变量的定义解决了一部分初始化顺序问题但不同编译单元间仍有风险。所以我的建议是任何使用了静态注册的工程必须在启动阶段预留一个“热身”函数显式调用一次不要侥幸依赖语言标准保证顺序。6.4 工厂内部使用虚基类构造时调用纯虚函数这个坑其实是面向对象通病但在工厂里特别容易暴露。工厂创建完一个对象后往往会做一些统一的初始化动作比如调用基类虚接口init()。如果你在基类构造函数里调用了虚函数而虚函数被工厂返回的派生类重写了那么这个时候派生类还没有构造完成调用结果是基类的实现甚至是纯虚函数导致崩溃。正确做法是基类构造函数只做基础字段初始化虚函数调用放到一个专门的postConstruct阶段由工厂在对象构造完成后显式调用。很多框架的工厂模式都是这个思路C中 locate 的initialize也是类似分离阶段的设计。6.5 工厂对象管理大量依赖时的循环引用复杂业务里经常出现A依赖BB依赖CC又依赖A的循环依赖。用工厂逐一创建时可能不会报错但如果工厂是单例且持有shared_ptr析构问题就会出现循环引用。这种情况的排查难度比较大表现为程序看起来运行正常但内存不断涨析构永远不走。解决方案有几个层面第一种打破循环链不要让A持有B的所有权改成依赖注入时用观察者模式。第二种给工厂使用weak_ptr只持有观察引用真正的所有权由容器或调用方持有。第三种调整创建时机让循环依赖变成先创建后再注入的关系用setDependency的方式补上引用。如果你在代码review中发现一个工厂内部出现大量循环依赖一定要警觉那往往说明对象边界设计有问题而不是工厂实现有问题。7. 几个容易混淆的模式边界工作中经常有人把工厂模式和其他创建类模式混着用结果代码变得纠缠不清。区分清楚边界能大幅降低维护成本。7.1 工厂和单例的区别工厂是创建逻辑单例是生命周期控制。一个对象既可以是工厂创建出来的又可以是单例。常见误区是给工厂类本身加一个单例方法class WidgetFactory { public: static WidgetFactory instance() { ... } std::unique_ptrWidget create(const std::string name); };这没问题。但有人会把创建对象的结果变成单例std::unique_ptrWidget WidgetFactory::create(const std::string name) { static std::unique_ptrWidget single std::make_uniqueButtonWidget(); return single; }这就违背了工厂模式的意义。如果按钮需要被new两次第二次拿到同一份单例两个调用方就共享了一个带有状态的对象状态互相污染迟早出事。如果确实要单例应该让调用方自己决定通过函数或参数控制不要默默返回全局单例这比默认创建新对象更容易踩坑。7.2 工厂和建造者的区别工厂模式关注“创建哪个类型”建造者模式关注“如何构建一个复杂对象”。两者可以配合工厂负责判断类型builder负责把参数填好最后调build()生成对象。常见误区是工厂内部自己做了builder的事情比如工厂里攒了一堆参数循环设置到对象上。正确的组合方式是std::unique_ptrWidget createWidget(const WidgetConfig config) { auto builder std::make_uniqueWidgetBuilder(); builder-setName(config.name); builder-setWidth(config.width); builder-setHeight(config.height); if (config.tooltip) { builder-setTooltip(*config.tooltip); } return builder-build(); }这个代码里工厂只负责拿到builder并调用build具体的构建细节全部在builder里。这样不管是读代码还是测试职责都非常清楚。8. 最后的经验总结与心得这篇长文写下来我自己的思路也梳理了一遍。工厂模式的本质不是“new一个对象”这个动作而是“把选择实现的职责和创建实现的职责分离”。选择是路由创建是装配两者分开系统的扩展性才会真正起来。对于大部分中小项目我推荐从最简单的方式开始一个工厂函数内部用if或switch返回unique_ptr。不要一开始就设计抽象工厂和注册表那会引入多余复杂度。等代码出现以下信号再重构switch分支超过8个、新增类型时要改动多处文件、创建逻辑里开始出现互相纠缠的ifelse、调用方对具体的配置细节过于了解。如果必须设计一个长期演进的核心系统我个人的倾向是接口层用抽象工厂或者一个工厂接口实现层用注册表机制配置层用外部配置控制路由创建结果统一用unique_ptr返回异常统一在工厂入口处捕获并转换为业务异常。这样设计出来的系统新增一种实现类通常只需要新增一个文件并写一行注册代码不需要改动现有接口和测试个人认为这是一个比较舒服的扩展体验。另外写工厂代码的时候多想想测试。工厂模式的好处之一就是容易测试测试场景可以注册一个假实现然后验证调用方的行为。如果代码里工厂返回裸指针测试还得小心翼翼管理生命周期如果工厂里做了大量业务逻辑测试就很难构造出指定的返回结果。保持工厂轻量、职责单一是长期可维护性的关键。分享最后一个实际经验在代码评审里很多同学看到工厂模式就自然地想加抽象类和虚函数总觉得不搞个多态就不算用工厂。这种倾向要克制。工厂模式可以返回具体类不一定要返回抽象类接口。如果你的调用方根本不需要多态只是需要一个具体对象那返回具体类型完全没问题不要为了设计而设计。很多时候朴素的方案比花哨的设计更耐用。希望这篇长文对你有用。如果你在项目中碰到了工厂模式相关的坑欢迎留言聊聊一起把这段经验补完整。