C++工厂模式进阶:注册表、抽象工厂与生命周期管理实践
2026/9/9 14:52:26 网站建设 项目流程

做C++后端这些年,工厂模式是我在项目里见到频率最高、但也用得最糙的设计模式。很多工程起步时很简单,一个if-else或者switch就能搞定对象创建,等产品迭代半年,代码里就长出了一堆分支判断,每加一种消息类型、协议或者组件,就要改分发函数、补case、加头文件,还得小心翼翼不碰坏别人刚提交的逻辑。这篇文章不打算从"什么是工厂模式"这种教科书定义讲起,而是把C++工程里真正能用上的工厂模式进阶玩法串一遍:泛型注册表工厂、抽象工厂和依赖注入怎么结合、工厂返回的生命周期语义怎么设计、以及编译期工厂和运行期工厂怎么取舍,最后用一个可扩展的消息分发系统把前面这些点落进一个完整项目。适合那些已经会用工厂模式、但想在真实工程里把它用得干净、可扩展、可测试的读者。

1. 简单工厂到抽象工厂:先把"类型爆炸"这个病根说清楚

想聊高级应用,得先搞清楚工厂模式到底在解决什么问题。很多人是把工厂当成"一个static方法返回基类指针"来用的,这没问题,但如果你不理解它背后那层"扩展点"逻辑,代码写着写着就会变成新的混乱源。我在重构过的项目里最常见的情况是:有人用简单工厂,但路由逻辑全集中在一个函数里,最后这个函数比小说章节还长。

1.1 简单工厂、工厂方法、抽象工厂的核心差异

基础三件套看起来都是"帮你创建一个对象",但它们的扩展策略完全不同。我习惯用一张表来区分:

工厂形式创建策略新增产品时的改动典型问题
简单工厂一个静态函数集中创建修改工厂函数,加分支违反开闭原则,函数越来越臃肿
工厂方法基类定义纯虚函数,子类各自实现创建新增产品子类 + 新增对应的工厂子类产品一多,工厂类也跟着膨胀
抽象工厂一个接口里定义一组创建方法接口新增方法,所有实现都要改产品族扩展时接口侵入大

简单工厂适合产品类型数量少、且不太会变的场景,比如一个日志系统里创建文件日志或控制台日志。工厂方法适合每个产品确实拥有独立初始化逻辑、又不希望调用方依赖具体类型的场景。抽象工厂则适用于产品有"族"概念的场景,比如数据库访问层需要同时创建Connection、Statement、Transaction,这时候三个create方法必须成套出现。

但真实项目演进的路径根本不是教科书规划好的。往往一开始是简单工厂,后来加了十几个类型,工厂函数里一堆if,再后来有人用工厂方法重构,结果工厂类数量翻倍,编译时间变长,文件数量暴增。走到这一步,才轮到高级玩法出场。

1.2 类型爆炸的真实代价:改老代码的隐形成本

我一直认为,"每加一个类型就要改三处老代码"这件事本身就是技术债的警报。第一改动点是在工厂函数里加分支,第二改动点是在分发逻辑里加类型判断,第三改动点是要把头文件include进工厂文件。这三个点每一个都意味着:改代码的人和写老代码的人可能出现冲突;测试要重新回归;代码评审要仔细确认加分支时没影响已有行为。

更隐蔽的成本是"心智负担"。工厂函数一旦被很多人改过,团队对新成员讲解时就得说"你在这个函数里找地方加一行",这行分支放在哪里完全没有规则,完全靠惯例。一个曾经接入过这种系统的同事跟我说,他每次加完case都担心漏了某个隐蔽的else,因为真实代码根本不像书里那么整齐。

所以高级工厂设计的第一个原则就是:把"新增类型"从修改已有代码变成"新增一个独立文件",这就是注册表模式背后的根本动机。我们追求的是一种登记制的扩展体验:新类型自己报到,而不是让老代码去认识它。

1.3 为什么我排除了"用switch+if暴力演进"的路线

有时候最直接的方案不是坏的,switch在类型很少时性能最好、代码也直观。但类型数量增长到一定程度,这种方案会同时踩三个坑。一个是可读性:每个case后面跟几十行构造参数,读者很难聚焦出"这个case到底和别的case有什么不同"。另一个是编译耦合:工厂文件include了所有具体产品头文件,哪怕只是改其中一个产品的内部实现,所有依赖工厂的编译单元都要重新编译。第三个是测试不方便:如果一个case的分支逻辑很复杂,你没法单独替身掉某个具体产品,只能连着工厂函数一起测。

我在实际项目里的经验是,当case数量超过十个、或者产品类型可能会由外部插件继续扩展时,就该立刻换成注册表方案,不要等它变成二十个再回头还债。注册表方案会引入一点额外复杂度,但换来的是扩展点在编译层面彻底解耦。

2. 泛型注册表工厂:把新增逻辑做成"登记制"而不是"改旧代码"

注册表工厂的核心思路很简单:用一个map把"类型标识符"映射到"创建函数",创建函数通常是一个lambda表达式。新类型要接入时,只需要注册一个lambda进去,工厂本身不用改。这个东西听上去不复杂,但要在C++里做得顺手,有几个细节值得好好打磨。

2.1 模板注册表的核心骨架

先看一个最简版本:

#include <functional> #include <memory> #include <string> #include <unordered_map> #include <stdexcept> template <typename Base> class Factory { public: using Creator = std::function<std::unique_ptr<Base>()>; template <typename Derived> void registerType(const std::string& name) { creators_[name] = []() -> std::unique_ptr<Base> { return std::make_unique<Derived>(); }; } std::unique_ptr<Base> create(const std::string& name) const { auto it = creators_.find(name); if (it == creators_.end()) { throw std::runtime_error("unknown type: " + name); } return it->second(); } private: std::unordered_map<std::string, Creator> creators_; };

使用的时候,调用方只需要拿到一个Factory实例,注册几种类型,然后就能按名字创建:

class Message { public: virtual ~Message() = default; virtual void process() = 0; }; class PingMessage : public Message { public: void process() override { /* ... */ } }; int main() { Factory<Message> factory; factory.registerType<PingMessage>("PingMessage"); auto msg = factory.create("PingMessage"); msg->process(); }

这段代码里真正值得注意的点有两个。第一个是std::function的通用性:任何可调用对象都能作为Creator,不一定是lambda,函数指针、绑定了状态的std::bind都行,这让工厂在后续接对象池、接反射都是可能的。第二个是返回类型直接是std::unique_ptr<Base>,从工厂层面就明确了所有权归属,调用方拿到对象后不用担心什么时候该delete,这个后面专门展开说。

2.2 可变参数模板:让工厂适配不同构造签名

最简版本有个明显的短板:所有Derived类型必须能用无参构造函数创建。但真实产品类常常需要构造参数,比如消息处理器要持有配置对象,数据库连接要传入连接串。一个可靠的做法是让工厂支持参数包:

template <typename Base, typename... CreateArgs> class ParameterizedFactory { public: using Creator = std::function<std::unique_ptr<Base>(CreateArgs...)>; template <typename Derived> void registerType(const std::string& name) { creators_[name] = [](CreateArgs... args) -> std::unique_ptr<Base> { if constexpr (std::is_constructible_v<Derived, CreateArgs...>) { return std::make_unique<Derived>(std::forward<CreateArgs>(args)...); } else { static_assert(std::is_constructible_v<Derived, CreateArgs...>, "registered type cannot be constructed with given args"); return nullptr; } }; } std::unique_ptr<Base> create(const std::string& name, CreateArgs... args) const { auto it = creators_.find(name); if (it == creators_.end()) { throw std::runtime_error("unknown type: " + name); } return it->second(std::forward<CreateArgs>(args)...); } private: std::unordered_map<std::string, Creator> creators_; };

这里用到了C++17的if constexpr,在编译期判断Derived是否可以从参数包构造。用std::is_constructible_v做检查,如果某个Derived类型不能用统一参数构造,就触发static_assert,把错误提前到编译期,而不是运行时才抛出"构造失败"。

不过这里有一个细节要注意:static_assert在if constexpr的else分支里,只有当该分支被实例化时才会触发。如果Derived能构造,不会实例化else分支,所以不会误伤。这个技巧我在好几个项目里用过,真正解决了"一组类型构造参数不一致"的痛点。

2.3 注册宏与静态初始化顺序的坑

注册表方案要真正好用,得让"注册"这件事能够自动发生,否则调用方还得记得在main函数里手动调用registerType。常用的做法是定义注册宏,通过全局对象的构造函数完成静态注册:

#define REGISTER_FACTORY(FactoryType, BaseType, DerivedType) \ namespace { \ const bool reg_##DerivedType = ::RegisterHelper<FactoryType, BaseType, DerivedType>(#DerivedType); \ }

但全局对象在main之前执行,这里有个经典陷阱:如果Factory实例本身也是全局对象,它可能在注册动作执行之前还没构造好,这就是静态初始化顺序问题。我踩过一次很深的坑:两个源文件各自声明了全局工厂和全局注册对象,链接顺序一调整,程序启动就崩溃,定位了大半天。

解决方案是让工厂实例使用函数内局部静态对象,也就是Meyers Singleton:

template <typename Base> Factory<Base>& defaultFactory() { static Factory<Base> factory; return factory; }

C++11之后,函数内局部静态对象的初始化是线程安全的。注册动作发生在main之前的单线程阶段,首次调用defaultFactory()时构造工厂,注册对象把自己的类型写进map,顺序天然可控。跨编译单元的注册顺序虽然不确定,但注册本身只是"往map里插键值",顺序不影响结果,所以这个方案安全可靠。

2.4 注册表的线程安全:main之前注册,运行期读取

工厂注册好之后,运行期创建对象往往是多线程并发的。如果工厂只提供注册和创建,而注册只发生在单线程启动阶段,那么生产环境只需要保证create的并发读安全即可。unordered_mapfind在并发条件下只读是没问题的,但如果插件系统允许运行期动态注册类型,就必须给注册和查找加锁,或者用读写锁。

我通常采用一个简单的分层设计:核心代码通过defaultFactory()拿到工厂并调用create,插件在加载时注册,注册函数内部用std::mutex保护map。读多写少的场景用std::shared_mutex更合适,但要注意create返回的unique_ptr在锁外构造会更好,不要把对象构造放进锁的临界区,否则所有线程创建对象都会互相阻塞。

3. 抽象工厂与依赖注入融合:产品族不该让业务代码来拼装

泛型注册表解决的是"一个类型对应一个创建入口"的问题,但真实系统里常常需要成套创建相关对象。这时候抽象工厂依然有它的价值,只是不能简单套教材写法,要和依赖注入结合起来用。

3.1 产品族接口如何切分才算稳定

先举个例子。一个跨数据库的项目,需要支持MySQL、PostgreSQL、SQLite三种数据库,业务代码希望不直接依赖具体数据库实现,于是我们抽象出三个产品接口:ConnectionStatementTransaction

class Connection { public: virtual ~Connection() = default; virtual void connect(const std::string& dsn) = 0; }; class Statement { public: virtual ~Statement() = default; virtual void execute(const std::string& sql) = 0; }; class Transaction { public: virtual ~Transaction() = default; virtual void begin() = 0; virtual void commit() = 0; virtual void rollback() = 0; };

这三个接口的粒度切分是否合理,直接决定了抽象工厂能不能用。如果接口切得太细,比如把Connection再拆成ConnectableCloseable,产品族关系就会变得松散,工厂维护成本飙升。我自己的经验是:产品族接口应该和业务场景一一对应,而不是跟着数据库供应商的API走。业务需要"连接-执行-事务"三件套,接口就是这三个,多一个都不加。

3.2 把工厂接口注入业务对象

抽象工厂的经典形式是定义一个虚接口,下面挂各个数据库的具体工厂:

class DatabaseFactory { public: virtual ~DatabaseFactory() = default; virtual std::unique_ptr<Connection> createConnection() = 0; virtual std::unique_ptr<Statement> createStatement() = 0; virtual std::unique_ptr<Transaction> createTransaction() = 0; }; class MySqlFactory : public DatabaseFactory { public: std::unique_ptr<Connection> createConnection() override { return std::make_unique<MySqlConnection>(); } std::unique_ptr<Statement> createStatement() override { return std::make_unique<MySqlStatement>(); } std::unique_ptr<Transaction> createTransaction() override { return std::make_unique<MySqlTransaction>(); } };

这里真正的进阶点不是这些实现,而是业务对象如何拿到工厂。教科书里经常说"客户端持有一个工厂引用",但客户端到底从哪里获得工厂?直接在构造函数里new一个MySqlFactory,等于还是在硬编码具体实现。推荐的做法是把工厂作为依赖注入到业务对象中:

class ReportGenerator { public: explicit ReportGenerator(std::shared_ptr<DatabaseFactory> dbFactory) : dbFactory_(std::move(dbFactory)) {} void generate() { auto conn = dbFactory_->createConnection(); auto stmt = dbFactory_->createStatement(); // ... } private: std::shared_ptr<DatabaseFactory> dbFactory_; };

ReportGenerator完全不知道数据库厂商是谁,它只知道能拿到Connection、Statement、Transaction。要换数据库,只需要把不同的具体工厂实例注入进来。这个组合比单独用抽象工厂强在一点:创建时机和场景由业务方掌控,工厂的生命周期也由客户端管理,测试时可以轻松注入一个mock工厂。

3.3 测试替身:给工厂换掉真实实现

用抽象工厂加依赖注入之后,写单元测试会舒服非常多。以前测业务逻辑时要连真数据库,现在只要做一个FakeDatabaseFactory:

class FakeDatabaseFactory : public DatabaseFactory { public: std::unique_ptr<Connection> createConnection() override { return std::make_unique<FakeConnection>(); } std::unique_ptr<Statement> createStatement() override { return std::make_unique<FakeStatement>(); } std::unique_ptr<Transaction> createTransaction() override { return std::make_unique<FakeTransaction>(); } }; TEST(ReportGeneratorTest, GenerateWorksWithFakeDb) { auto factory = std::make_shared<FakeDatabaseFactory>(); ReportGenerator generator(factory); EXPECT_NO_THROW(generator.generate()); }

这个经验的本质是:工厂模式的价值不只是运行时的多态创建,它同时也是一个测试替身注入点。如果你发现某个类用了工厂但没法在测试里替换成替身,那这个工厂的设计十有八九有问题,要么工厂被静态方法邦死了,要么业务代码在某个隐蔽的位置直接new了具体类。

4. 工厂返回的生命周期语义:从裸指针到池化回收

工厂模式讨论得最多的往往是"怎么创建对象",但返回值怎么带走、销毁时谁来负责,这个被忽略的问题在C++里反而是事故高发区。我在Code Review里看过太多次裸指针从工厂里甩出来,调用方用完忘记delete,或者delete了之后又被别的地方继续使用。

4.1 裸指针的麻烦:所有权归属不清

如果工厂返回Base*,调用方拿到指针后根本无从判断这个对象是自己独占还是工厂还持有副本。更麻烦的是异常安全性,创建对象过程中如果中间某个步骤抛异常,裸指针很容易泄漏。

C++ Core Guidelines里写得很明确:从工厂创建出来的对象应该以智能指针返回,因为工厂天然是所有权转移的源头。调用方和工厂之间的契约应该是"我把所有权交给你,你拿着用,用完自动销毁"。

4.2 以unique_ptr作为工厂的统一出口

unique_ptr是我在工厂里最常用的返回类型。它的语义是独占所有权,对象生命周期完全由调用方控制,析构发生在作用域结束时,自动释放,不需要手工delete。上面泛型注册表工厂示例里已经用了这个模式:

std::unique_ptr<Base> create(const std::string& name) { auto it = creators_.find(name); if (it == creators_.end()) { throw std::runtime_error("unknown type: " + name); } return it->second(); }

如果某些场景确实需要多个持有者共享对象,那就返回std::shared_ptr。但要注意:共享所有权意味着对象的析构时机变得不可预知,对于持有锁、文件句柄、数据库连接这种资源型对象,最好明确只让一个所有者持有,其他角色只借不借所有权。工厂内部可以把shared_ptr进行转换,比如返回值统一是shared_ptr<Base>,但内部用make_shared<Derived>创建。

我习惯的约定是:除非有明确的共享需求,否则工厂一律返回unique_ptr。能用unique_ptr表达的语义,就不要用shared_ptr。这个约定让工厂的语义极其清晰,调用方看到返回类型就知道自己独占了这个对象。

4.3 自定义删除器和对象池:把"销毁"变成"归还"

工厂模式的另一个进阶用法是通过自定义删除器实现资源复用。典型场景是数据库连接池:连接对象创建开销大,每次用完直接销毁很浪费,更好的做法是构造一个池化的unique_ptr,删除器不再调用delete,而是把对象归还给池子。

class ConnectionPool { public: std::unique_ptr<Connection, std::function<void(Connection*)>> acquire() { std::lock_guard<std::mutex> lock(mutex_); if (!idle_.empty()) { auto conn = std::move(idle_.back()); idle_.pop_back(); return std::unique_ptr<Connection, std::function<void(Connection*)>>( conn.release(), [this](Connection* raw) { release(raw); }); } auto raw = new Connection(); return std::unique_ptr<Connection, std::function<void(Connection*)>>( raw, [this](Connection* raw) { release(raw); }); } private: void release(Connection* raw) { std::lock_guard<std::mutex> lock(mutex_); idle_.push_back(std::unique_ptr<Connection>(raw)); } std::mutex mutex_; std::vector<std::unique_ptr<Connection>> idle_; };

这样调用方拿到的还是unique_ptr,但销毁时对象不消失,而是回到池子里,下次acquire能直接复用。这套思路看起来是"智能指针+删除器"的玩法,但它需要工厂来统一创建和回收,工厂不再只是一个创建者,而是变成了资源管理器。我在网络服务里用这种模式管理数据库连接和客户端连接,连接建立次数明显下降,服务高峰期的延迟也稳定了不少。

要小心的是,归还池子时有遗漏就会导致连接泄漏。我的建议是回收函数幂等,并且要防止同一个连接被重复归还。上面这个示例用unique_ptr转移所有权的方式,天然保证了每个裸指针在同一时刻只有一个智能指针管理,重复归还的问题在正常使用流程里不会出现。

5. 实战拆解:一个可扩展消息分发系统是如何落地的

讲了不少理论,接下来用一个真实项目里非常常见的场景把前面这些点串起来。假设我要设计一个网络消息分发系统,服务器收到不同消息ID,需要创建对应的Handler去处理。消息类型还在持续增加,团队同时有多个人在开发不同的消息处理逻辑。

5.1 需求拆解:消息类型持续增多的场景

需求本身不复杂:收到一个消息,根据消息类型ID,找到对应的Handler,调用handle()。但有几个约束让整体设计必须有讲究。

第一,消息类型非常多,预计半年内会增加到上百种。第二,不同类型Handler的构造参数并不相同,有的需要配置对象,有的需要数据库访问接口。第三,有些人会并行开发新的Handler,不能让他们去改公共代码。第四,为了做联调,测试时需要轻松替换某个Handler实现,方便模拟异常场景。

如果不用工厂注册表,这个系统会演化成一个大switch,而且越到后面越难维护。用注册表模式就是最合适的解法。

5.2 核心实现:注册宏+局部静态单例+类型擦除

先定义Handler接口和基础工厂:

class MessageHandler { public: virtual ~MessageHandler() = default; virtual void handle(const Message& msg) = 0; }; class Config; // 需要注入的配置对象 class MessageHandlerFactory { public: using Creator = std::function<std::unique_ptr<MessageHandler>(const Config&)>; static MessageHandlerFactory& instance() { static MessageHandlerFactory factory; return factory; } template <typename Handler> void registerHandler(const std::string& name) { creators_[name] = [](const Config& config) -> std::unique_ptr<MessageHandler> { return std::make_unique<Handler>(config); }; } std::unique_ptr<MessageHandler> createHandler(const std::string& name, const Config& config) { auto it = creators_.find(name); if (it == creators_.end()) { throw std::runtime_error("unknown handler: " + name); } return it->second(config); } private: std::unordered_map<std::string, Creator> creators_; }; #define REGISTER_HANDLER(HandlerType) \ namespace { \ const bool reg_##HandlerType = []() { \ MessageHandlerFactory::instance().registerHandler<HandlerType>(#HandlerType); \ return true; \ }(); \ }

新同事加入项目要开发一个Ping消息的处理逻辑,他只需要做两件事:写一个继承Handler的类,然后在自己的源文件底部加一行REGISTER_HANDLER(PingHandler)。每个人各写各的文件,互不打扰,公共的工厂代码一行都不用改。

这里面的类型擦除体现在哪里?注册时把具体的Handler类型放进一个lambda,lambda被包装成std::function,类型信息被擦除。工厂在运行时只需要知道字符串名字,就能获取到那个lambda并调用,而不需要知道究竟返回什么具体类型。这种把"类型"转换成"可调用对象+字符串标识"的手法,在C++没有反射机制的现状下,是解决运行期动态创建问题最自然的路径。

5.3 配置驱动创建和测试替身

另一个诉求是配置驱动创建。比如配置文件里写了把哪种消息ID映射到哪个Handler名字,业务层启动时读配置,然后让工厂按名字创建。这个写法的好处是,同样的二进制包换个配置就能启用不同的处理逻辑,不需要重新编译。

测试的时候,工厂的注册表同样能派上用场。想在测试里替换某个Handler,不需要改业务代码,只要在注册表里用同名的测试替身覆盖注册即可。不过要注意,因为注册动作发生在全局初始化阶段,覆盖顺序不保证,稳妥的做法是让工厂的注册函数支持覆盖,并允许在测试初始化时显式调用registerHandler<FakePingHandler>("PingHandler"),先注册真的,再注册假的,后者覆盖前者。工厂API要把"覆盖"行为明确写出来,而不是隐式覆盖,否则别人看代码时会困惑为什么同一个名字被注册了两次。我通常会给regiserter加一个返回值或者日志,反馈"这个名字之前已注册过,将被覆盖",这样踩坑的时候能立刻定位。

6. 高级工厂的边界:性能和可维护性之间的取舍

工厂不是一个越多越好的模式。它在带来可扩展性的同时,也有自己的代价。如果一个项目不分青红皂白到处套工厂,反而会造成过度设计。

6.1 哈希查找与虚函数:工厂的开销到底有多大

很多人担心工厂的性能,其实拆开看,开销主要有三块:unordered_map的哈希查找、std::function的间接调用、多态虚函数调用。这三者在单次创建里的开销都是纳秒到亚微秒级别,相比对象构造本身(比如数据库连接、IO缓冲区分配)几乎可以忽略不计。

但如果你处在真正的热点路径上,每秒要创建几十万个小对象,工厂的哈希查找就不再是无所谓了。我有一次写网络网关时做过性能对比,同样的状态机复用工厂创建对象,profile下来unordered_map::find占了大概1.2%的CPU,不算严重,但却是工厂链路里最大的可省开销。优化方案通常有两个方向:一个是把创建结果缓存起来,减少频繁创建;另一个是使用完美的查找结构,比如基于字符串的perfect hash,或者直接用枚举和数组索引。要注意的是,这些优化都带来了额外复杂度,不要一开始就做,先profile,再决定优化点。

6.2 编译期工厂:if constexpr 和类型列表

如果创建时所需的类型信息在编译期就能确定,那运行期工厂就是多余的。C++17的if constexpr可以写出编译期分派:

template <typename ProductId> auto createById() { if constexpr (std::is_same_v<ProductId, ProductA>) { return std::make_unique<ProductA>(); } else if constexpr (std::is_same_v<ProductId, ProductB>) { return std::make_unique<ProductB>(); } }

这段代码在编译期就会丢弃不必要的分支,没有运行时查找,也没有虚调用。这种编译期工厂适合的场景是类型列表已知、且每个分支的逻辑差异比较大。它的劣势是:分支之外的代码如果对返回类型有统一要求,很难做到真正的类型统一,调用方几乎只能用auto推导,不能把它当多态接口来用。

更实用的混合方案是:编译期用if constexpr做静态分发,运行期用注册表工厂做动态创建,两者同时存在,互不冲突。编译期工厂用在类型已知的编译期调度路径,运行期工厂用在与外部配置或协议数据交互的路径上。

6.3 原型模式当替身:什么时候clone比create更合适

有些场景下"复制已有对象"比"从头创建"更能解决问题。如果对象构造代价很大,或者对象需要保留某个初始化状态的模板值,用原型模式更合适。原型模式和工厂模式可以结合起来:注册表里不存创建函数,而是存一个共享的原型对象指针,创建时调用clone()

class PrototypeFactory { public: template <typename Derived> void registerPrototype(const std::string& name, std::shared_ptr<const Derived> sample) { prototypes_[name] = [sample]() { return std::unique_ptr<Base>(sample->clone()); }; } };

举个例子,游戏引擎里一个怪物类型的初始属性、骨骼、贴图都加载好了,每次生成新怪物只要clone一下,就不再需要从头加载资源。这个做法本质上还是工厂模式,只是把创建动作从构造换成了克隆。它在内存复用和初始化开销上的优势非常明显,但前提是clone()要能正确实现深拷贝,否则多个对象会共享内部状态,出现诡异bug。

6.4 别硬用工厂:那些反例

最后说说不该用工厂的反例。如果一个类型只有两个产品、产品构造参数完全一致、未来也没有扩展计划,那直接switch反而更简单、更直接。工厂模式的价值在于应对需求变化,而不是应对今天的代码规模。

还有一类反例是把工厂套在简单值类型上,比如PointColor这种轻量数据结构。它们既不需要多态,也不会有复杂的构造逻辑,工厂只会增加调用方的心智负担,读者一眼看不懂为什么要绕个弯去创建。

我自己的判断标准很简单:有三个迹象存在,才值得用工厂模式。第一,创建逻辑处在频繁变化的扩展点上;第二,调用方需要依赖抽象而不是具体类型;第三,测试时需要替身注入。如果三个都不占,别犹豫,直接new就好。工厂是工具,不是装饰品。把工具用在真正需要它的位置,才是"高级应用"的本意。


最后分享一个我在实际项目里的小体会:工厂模式的上限不是某个写法,而是你能不能把"类型标识、创建逻辑、缓存策略、生命周期"这四个维度拆清楚。很多设计复杂的工厂,根源其实是这四个维度揉在一起了。先把它们分开想,再动手写,大部分问题自己就消失了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询