ARTICLE DETAIL

资讯详情

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

重新审视工厂模式:现代C++下的安全与灵活实践

重新审视工厂模式:现代C++下的安全与灵活实践 为什么我建议你重新审视工厂模式如果你在C项目里写过这样的代码一个if/else判断type字段然后分别new出不同的对象再丢给上层使用——你已经用上了工厂模式。但如果你以为这就叫“高级应用”那这篇文章可能正好适合你。我在很多项目里见过两种极端一种是把工厂模式当成银弹到处套结果一个简单功能被包了五层间接另一种是彻底排斥设计模式觉得C只要写好RAII和模板就够了工厂全是“Java味”。说实话这两种态度都不太对。工厂模式的高级应用核心不在于你知道多少种变体简单工厂、工厂方法、抽象工厂而在于你能否回答这几个问题何时该引入工厂工厂边界画在哪对象生命周期归谁管返回裸指针还是智能指针类型怎么注册、怎么扩展这些问题在你深入做大型项目、写可测试代码、做插件系统的时候全都绕不开。这篇文章先讲清楚为什么学完工厂模式不会用再顺着一条真实业务场景把几种工厂变体串起来讲然后重点落在现代CC17/20下工厂模式怎么写得既安全又灵活最后聊聊生命周期管理、依赖注入、以及哪些场景真的不该用工厂。全程以我的实操经验为主不会只堆UML图和抽象定义。1. 为什么大多数工厂模式教程“学了不会用”1.1 教程教你的是类图思维实战考你的是生命周期思维大多数工厂模式教程长这样画一个Product接口、几个ConcreteProduct再画一个Creator工厂类然后告诉你“工厂方法模式符合开闭原则新增产品不用改工厂代码”。你背下来了也觉得有道理。可一回到自己的项目你就懵了我的类构造函数有一大堆依赖有配置文件路径、有日志实例、有std::shared_ptrConnection这个工厂里到底要怎么写创建出来的对象应该由谁持有如果创建的Product还需要去事件总线里注册自己是工厂管还是调用方管这些都是真实工程里天天遇到的事但绝大多数教程不会给你答案因为它们停留在“类图逻辑”层面没有到“内存和生命周期”层面。// 教程风格的工厂 class ProductFactory { public: virtual std::unique_ptrProduct create() 0; }; class ConcreteFactoryA : public ProductFactory { public: std::unique_ptrProduct create() override { return std::make_uniqueConcreteProductA(); } };这段代码本身没有错但它只解决了一个最简单的问题根据类型创建对象。它没有回答依赖从哪来、对象创建完怎么装配、复杂产品如何分步构建、以及失败时资源如何回收。所以说学完工厂模式不会用不是你理解力不行而是你需要的不是“定义”你需要的是一套在真实工程中决策和落地的方法论。1.2 工厂真正解决的三个问题我个人的理解里工厂模式在被滥用之前先要搞清楚它到底解决什么。缩小到C工程实践它真正解决的是这三件事第一个问题是创建逻辑复用。如果系统的多个地方都需要按照某个配置项创建对应类型的对象把这个创建逻辑抽到工厂里避免每个调用点各写一份if/else将来加新类型时也只改一个地方。第二个问题是控制对象的创建时机和生命周期。有些对象不希望在调用方随意new出来而是希望由工厂统一管理比如数据库连接池里的Connection比如线程池里的Worker比如单例资源。工厂可以做成“创建后登记在册”也可以做成“受限生命周期范围内的作用域对象”这些控制逻辑放在工厂里对调用方透明。第三个问题是拖解耦和可测试。高层业务代码只需要知道一个std::functionstd::unique_ptrIService()或者unique_ptrIService不需要知道具体类名和构造函数签名。测试时注入一个工厂的fake实现就能把数据库/网络全部挡在单元测试之外。这三个其实是分不同层次的别把工厂只当成“根据参数new对象”的工具它更底层的价值是把“怎么建”和“怎么用”拆开。想明白这一点后面所有高级用法都是水到渠成。2. 从简单工厂到抽象工厂用一条真实业务场景串起来2.1 场景假设一个跨平台图形渲染器的资源加载器为了把几种工厂变体真正串起来我构造一个贴近真实的业务场景假设你在做一个跨平台的图形渲染器需要从磁盘加载不同类型的图片纹理PNG、JPG、HDR、TGA加载完成后要封装成统一的Texture对象内部可能要区分是普通2D纹理还是Cubemap纹理再往下还可能区分是OpenGL纹理还是Vulkan纹理。初学者往往会写这样一段代码std::unique_ptrTexture loadTexture(const std::string filePath, const std::string format) { if (format png) { return std::make_uniquePngTexture(filePath); } else if (format jpg) { return std::make_uniqueJpgTexture(filePath); } else if (format hdr) { return std::make_uniqueHdrTexture(filePath); } else { return nullptr; } }这就是最朴素的简单工厂。它最大的优点是一目了然最大的缺点是每加一个图片格式都要往这个函数里塞一个分支函数会越来越长违背了开闭原则而且它只能做“无脑创建”如果不同格式加载流程差异很大比如HDR需要做色调映射初始化PNG可能需要处理alpha通道预乘这个函数就会塞进越来越多的业务逻辑变成一个“上帝函数”。2.2 工厂方法把创建逻辑下沉到子类当不同产品创建步骤差异变大时就该考虑工厂方法了。思路是把“创建”本身变成一个虚函数让每个具体格式的加载器都自己管自己的创建细节。class TextureLoader { public: virtual ~TextureLoader() default; std::unique_ptrTexture load(const std::string filePath) { // 公共流程打开文件、读取头信息等 auto header readHeader(filePath); // 调用子类实现的具体创建 auto texture createFromHeader(header); texture-setPath(filePath); return texture; } protected: virtual std::unique_ptrTexture createFromHeader(const FileHeader header) 0; }; class PngTextureLoader : public TextureLoader { protected: std::unique_ptrTexture createFromHeader(const FileHeader header) override { // PNG特有的解码、预乘、校验... return std::make_uniquePngTexture(header.width, header.height, header.bits); } };你可能会觉得这比简单工厂复杂多了。确实是的所以工厂方法并不是“所有场景下都应该优先选”它的适用前提是各产品创建过程有公共骨架但具体步骤差异较大需要把变化点隔离到子类。在我实际经验里工厂方法非常适合这类场景消息解析器、协议处理器、渲染资源加载器、推理引擎中不同模型的加载器。这些场景的产品类型不多但创建流程差异很大需要子类各自发挥。2.3 抽象工厂面对“产品族”时的选择再往上一层如果系统的产品不是孤立的单个对象而是一组相关的对象——注意是一组——那就轮到抽象工厂出场了。还是渲染器这个例子。假设你不只要支持多格式还要支持多后端OpenGL后端需要GLTexture、GLShader、GLBufferVulkan后端需要VkTexture、VkShader、VkBuffer。这时如果每个类型单独建工厂会出现严重的“组合爆炸”——2个后端 × 3种资源 6个工厂类。更好的做法是定义一个抽象工厂每个后端提供一个具体工厂负责生成整套资源对象class RenderResourceFactory { public: virtual ~RenderResourceFactory() default; virtual std::unique_ptrTexture createTexture(const TextureDesc desc) 0; virtual std::unique_ptrShader createShader(const ShaderDesc desc) 0; virtual std::unique_ptrBuffer createBuffer(const BufferDesc desc) 0; }; class GlResourceFactory : public RenderResourceFactory { public: std::unique_ptrTexture createTexture(const TextureDesc desc) override { // 调用OpenGL的glGenTextures、glTexImage2D等 return std::make_uniqueGLTexture(desc); } std::unique_ptrShader createShader(const ShaderDesc desc) override { return std::make_uniqueGLShader(desc); } // ... }; class VulkanResourceFactory : public RenderResourceFactory { // 每个函数都调用Vulkan API... };抽象工厂的关键护城河在于它保证了一组产品之间的兼容性你不会不小心在OpenGL后端用上了Vulkan的Texture因为整个后端的产品族都来自同一个具体工厂。在大型系统里这种“族级一致性”比单个对象的创建灵活度重要得多。这里用一张表格对比一下三种变体帮助读者在不同场景下快速做决策变体解决的核心问题适用场景过度使用时的风险简单工厂集中管理创建逻辑产品类型少、创建逻辑直接函数膨胀开闭原则被破坏工厂方法隔离创建步骤差异各产品创建流程差异大类数量膨胀过度设计抽象工厂保证产品族内一致性多个后端/平台/主题成组创建叠加新维度时改动面巨大2.4 演进路线总结不是为了模式而模式很多读者会问我应该直接用抽象工厂吗我的回答是从简单的开始遇到具体问题再演进。上来就设计抽象工厂很容易把系统架构撑得很大结果真实需求根本不需要产品族一致性。我的习惯是先写一个简单工厂/或者连工厂都不写直接在调用点构造当出现“第二处使用相同创建逻辑”“新产品导致分支蔓延”“后端/平台维度出现”这三个信号时再逐步重构为更重的工厂变体。代码重构时有一个很实用的技巧用git做一次“纯重构提交”把变化集中到工厂封装不掺任何功能改动。这样如果后续评估发现这个工厂抽象不值得revert掉一个提交就好不用在功能代码里捞逻辑。3. 现代C下的工厂重构unique_ptr、注册表与变参构造3.1 一律返回unique_ptr别返回裸指针如果你搜过老版C的工厂模式代码大概率会看到std::auto_ptr甚至裸指针的返回方式。在C17/20时代我强烈建议工厂的接口统一返回std::unique_ptrT或std::shared_ptrT视所有权模型而定。两个理由第一返回值本身就在传达所有权语义——调用方拿到的是独占所有权这让资源管理一目了然第二异常安全。如果工厂内部在new完之后、返回之前抛了异常裸指针版本很容易泄漏而unique_ptr可以自动回收。// 推荐接口明确、异常安全 virtual std::unique_ptrIAnimal createAnimal(AnimalType type) 0; // 不推荐所有权不明、容易泄漏 virtual IAnimal* createAnimal(AnimalType type) 0;有人会说“我需要返回shared_ptr因为对象要共享”。没问题工厂里可以返回shared_ptr内部实现用std::make_shared。但默认情况下优先unique_ptr——因为如果调用方需要共享它自己可以.release()转移或者直接构造shared_ptr顺便说一句shared_ptrT(unique_ptrT)是允许的语义干净。3.2 注册表式工厂想扩展时不需要改代码回到前面“简单工厂每加一个格式就要改函数”的问题。现代C里有一种非常优雅的解法注册表模式。工厂内部维护一个从字符串或枚举/类型ID到创建函数的映射新类型通过注册函数“插进来”工厂本身保持封闭。class TextureFactory { public: using Creator std::functionstd::unique_ptrTexture(const TextureDesc); bool registerCreator(std::string_view tag, Creator creator) { return creators_.emplace(tag, std::move(creator)).second; } std::unique_ptrTexture create(std::string_view tag, const TextureDesc desc) { auto it creators_.find(tag); if (it creators_.end()) { // 统一错误处理打日志、抛异常、或返回空 spdlog::warn(No texture creator registered for tag: {}, tag); return nullptr; } return it-second(desc); } private: std::unordered_mapstd::string, Creator creators_; };这样做带来两个明显的工程收益一是新增类型时只需要写一个创建函数然后在某处registerCreator(hdr, createHdrTexture)即可工厂的实现文件不需要再动编译期依赖也被切断——插件甚至可以在运行时注册二是如果某个创建的模块不再需要用户可以选择不注册实现按需加载。我在真实项目里用注册表式工厂做过一个渲染器插件架构底层渲染模块不知道有哪些场景处理器每个场景处理器在自己模块的静态初始化函数中调用factory.registerCreator(shadow, ...)。这样连所谓的中央工厂列表都不存在模块自己管自己主程序只负责初始化全局工厂实例。3.3 注册时机与静态初始化顺序陷阱注册表式工厂有一个非常经典的坑静态初始化顺序问题。如果你用“模块内静态对象注册”的方式一个模块的静态创建函数尝试注册到另一个模块尚未构造的全局工厂里就会触发“静态初始化顺序失败”。// TextureFactory.cpp TextureFactory globalTextureFactory() { static TextureFactory instance; return instance; } // HdrModule.cpp危险写法 static bool registered [](){ globalTextureFactory().registerCreator(hdr, createHdrTexture); return true; }();这段代码的问题在于如果HdrModule.cpp的静态初始化先于TextureFactory.cpp的静态初始化执行顺序在不同翻译单元之间未定义它就会访问一个尚未构造的工厂。推荐的做法有两种一是Meyers Singleton 延迟注册——把globalTextureFactory()设计成函数内静态变量确保首次访问时才构造此时注册逻辑也在访问时才触发顺序可控二是不用静态注册改成显式的initRegistry(TextureFactory factory)函数在主函数/初始化流程里手动调用注册。显式调用虽然少了一点“自动发现的魔法”但排查问题非常方便我倾向于在关键模块上使用显式注册在插件式架构中才用延迟静态注册。3.4 构造参数不一致时变参模板工厂工厂创建的类如果构造函数参数完全相同注册表很好写。但现实很骨感PngTexture的构造函数可能是(width, height, bitDepth)HdrTexture的构造函数可能是(const HdrData data)参数天差地别。这种情况下我推荐把“参数不统一”外包给lambda工厂只统一接受一个“构造上下文”或者变参。方案一统一上下文结构体struct TextureCreateParams { std::string filePath; int requestedWidth 0; int requestedHeight 0; bool generateMipmaps true; // 更多通用参数... }; using Creator std::functionstd::unique_ptrTexture(const TextureCreateParams);这是我最常用的方式。各具体创建函数从参数包里取出自己关心的字段其余忽略。缺点是参数结构会不断膨胀变成“上帝参数”。所以只适用于参数比较稳定的场景。方案二变参模板工厂如果你真的希望不同注册类型接受不同签名可用变参模板包装class AFactory { public: templatetypename ConcreteProduct, typename... Args bool bind(std::string_view tag) { creators_[tag] [](Args... args) - std::unique_ptrBase { return std::make_uniqueConcreteProduct(std::forwardArgs(args)...); }; return true; } templatetypename... Args std::unique_ptrBase create(std::string_view tag, Args... args) { return creators_.at(tag)(std::forwardArgs(args)...); } private: std::unordered_mapstd::string, std::functionstd::unique_ptrBase(AnyArgs...) creators_; };不过说实话这个方案在C里写起来相当费劲——AnyArgs...那行的实现基本要靠std::any、std::variant或者把参数先打包成std::tuple代码很容易变得非常晦涩。为了可维护性我还是建议除非真的需要否则优先方案一让工厂接口保持简单。如果你发现参数统一不了、上下文结构体也在膨胀那这个工厂可能设计错了应该考虑拆成多个专门工厂而不是做一个“万能工厂”。4. 工厂模式与对象生命周期、依赖注入的界限4.1 工厂只是“出生地”生命周期管理是另一回事实际项目中对象创建之后不会自己独立存活它可能需要被事件总线观察、需要被连接池回收、需要从外部拿到依赖后完成初始化。如果工厂把所有事情都干了它就不再是一个“工厂”而是一个“上帝容器”。我见过最夸张的一个“工厂”里面干了五件事根据类型new对象、读配置文件设置属性、把对象注册到全局事件总线、给对象注入日志器和数据库连接、创建完后还自动“启动线程”。结果这个类三四千行每次测试都要mock掉一半以上的全局状态。我的分界线是工厂负责创建和基础装配生命周期管理继承、单例、作用域、回收交给容器级组件业务装配也尽量拆出去。图简单点理解工厂解决的是“怎么生”容器解决的是“怎么养”。4.2 单例工厂与作用域工厂控制“存活范围”有些对象在进程生命周期内只需存在一份比如日志器、配置中心、渲染设备。这种对象很多团队会用一个全局单例工厂来管理。但我会提醒一句全局单例非常不利于测试。一旦测试A用例改了配置中心的内容测试B用例拿到的就是一个被污染的状态。更优雅的方案是作用域工厂在需要时创建生命周期限定在某个作用域/事务/请求中作用域结束统一销毁。class ScopedFactory { public: ScopedFactory() default; ~ScopedFactory() { // 按逆序销毁所有创建出来的对象 for (auto it objects_.rbegin(); it ! objects_.rend(); it) { it-second.reset(); } } templatetypename T, typename... Args T* create(Args... args) { auto obj std::make_uniqueT(std::forwardArgs(args)...); auto* raw obj.get(); objects_.emplace_back(typeid(T).hash_code(), std::move(obj)); return raw; } private: std::vectorstd::pairsize_t, std::unique_ptrvoid, void(*)(void*) objects_; };上面代码示意为主实际实现中可以用std::unique_ptrvoid, function_deleter来统一保存所有类型对象析构时逆序释放。这种模式我在实现“每帧渲染资源缓存”时用过每个帧的临时纹理/绘图命令都在帧的ScopedFactory里创建帧结束自动全部清理相当于一个迷你版按帧垃圾回收。4.3 工厂并不等于依赖注入但可以配合得很好“依赖注入”和“工厂模式”经常被混为一谈。依赖注入关心的是一个类自己不new依赖而是由外部传入依赖对象。工厂模式关心的是如何集中创建对象。二者可以结合起来类构造函数不接收具体的Product而是接收一个“创建器”——std::function或工厂接口。最简单的DI式工厂可以这样写class Scene { public: explicit Scene(std::functionstd::unique_ptrIRenderer() rendererFactory) : rendererFactory_(std::move(rendererFactory)) {} void renderFrame() { auto r rendererFactory_(); r-draw(); } private: std::functionstd::unique_ptrIRenderer() rendererFactory_; };测试时直接把rendererFactory换成一个返回FakeRenderer的lambda完全不需要建立一整套“IoC容器”。我个人觉得C世界里一个轻量的std::function工厂字段基本能解决90%的可测试性需求没有必要上来就引入一整套依赖注入框架。4.4 什么时候该上“对象容器”级别的组件如果你遇到以下情况再考虑更重的容器/IoC方案对象依赖关系成网不是简单的一层依赖同一个接口的多个实现需要按配置动态切换对象创建后需要自动注入AOP、拦截器、代理你有非常多的内部服务且这些服务互相依赖。这时候工厂模式就不够用了。你会需要类似boost.di、fruit这样的现代C DI库或者直接上手一套注册表实现。但务必要控制复杂度容器能解决99%的创建问题也会带来99%的“这依赖到底从哪来的”排查地狱。5. 哪些场景真的不该用工厂模式5.1 编译期多态能解决问题时优先模板工厂模式用动态多态解决“运行时类型选择”但如果类型在编译期就已经确定用模板CRTP或直接模板更好省去虚函数开销、省去工厂封装、省去一堆类。// 不需要工厂 templatetypename Loader void loadAndProcess(Loader loader, const std::string path) { auto data loader.load(path); process(data); }热路径上尤其如此。比如每帧要处理大量的消息、变换、绘制命令如果每个对象都走一次虚函数工厂性能影响会被放大到肉眼可见。我自己优化过一个事件循环把多态工厂全部改成模板派发总帧耗时降了8%——这还只是基本没做缓存优化的情况。5.2 创建逻辑太简单时工厂只是多余的中间层如果一个类型创建时只需要一个std::make_unique而且调用点不多那就直接make_unique没必要包一层工厂。工厂模式最大的成本不是运行时开销而是代码阅读的间接性读者必须先打开工厂再跳转多次才能找到真正的构造逻辑。对于刚入门的同事或者维护者来说这种“为了设计而设计”的做法非常不友好。5.3 创建参数来自不同维度时工厂会变成怪物前面提到过“上帝参数”的问题。如果操作系统的不同后端创建的Texture所需的参数差异实在太大把参数统一到一个context里反而会让接口变得不符合直觉。此时更适合做独立的创建函数或者Builder模式比如VulkanCubemapBuilder和OpenGL2DTexBuilder各走各的构建流程没有统一工厂接口也没关系。5.4 运行时开销不得不考虑时虚函数工厂在大多数应用里开销可以忽略但在嵌入式、游戏引擎的每帧热路径、或者大量小对象创建场景下还是需要算一算。每次new一次就在堆上分配一次再走虚函数调用积累起来并不小。此时考虑对象池 工厂组合工厂不真正new而是从池子里取一个闲置对象用完放回池子连续内存/多态vector如果对象种类有限用std::variantvisit替代虚函数工厂返回variant模板生成用策略模板避免虚函数。不适合工厂的场景更好的替代方案热路径大量创建对象池、std::variant、模板生成类型在编译期确定模板函数直接构造创建逻辑简单且调用点少直接make_unique参数差异巨大Builder模式、独立创建函数需要拦截/代理等重逻辑引入DI容器或AOP框架我见过不少团队把“所有对象都要通过工厂创建”当成硬性规范结果业务代码里到处都是auto x xFactory-createX(...)而创建函数里就一句return make_uniqueX(...)。这种规范化确实统一了创建入口但跟“高可维护”基本没关系。规范应该是创建逻辑至少达到一定复杂度或者将来大概率会变复杂时才值得用工厂。6. 工厂模式实战中的踩坑与改进实录6.1 析构函数不是虚函数导致的内存泄漏第几次踩了如果基类没有虚析构函数而工厂返回的是unique_ptrBase那么删除对象时只有基类析构被调用派生类资源就泄漏了。class Base { public: ~Base() default; // 错必须 virtual virtual void doWork() 0; }; class Derived : public Base { public: ~Derived() { /* 释放内部Buffer */ } };工厂模式里这是最安静、也最致命的问题。能跑小内存泄漏十几分钟看不出来跑到压测或者长跑服务时内存曲线一路向上。排查手段就是valgrind/AddressSanitizer修复很简单基类析构函数声明为virtual ~Base() default;。我建议在写工厂基类的第一行就把这个写上永远不要让一个多态基类不带虚析构。6.2 工厂里塞进业务逻辑导致无法测试我接手过一个模块工厂的create函数里除了创建对象还会自动读取配置文件、连接数据库、并且注册到全局事件总线。结果就是单元测试想创建对象必须准备一份配置文件、一个可连接的数据库、还要启动事件循环。我重构时把工厂拆成三层纯工厂层只做make_unique和基础属性装配装配层负责从配置/容器中取依赖并传给工厂生命周期层负责注册到事件总线等。测试时直接调第一层传入固定假依赖秒过。这个拆分的经验后来我在很多项目里复用效果相当好。6.3 注册表键冲突与假注册注册表式工厂中emplace会静默地忽略重复注册。如果两个模块都想注册hdr先注册的会赢后者没生效还不报错。特别坑的是在插件系统里两个插件可能都在不同目录下提供了同名资源类型。为了避免这种问题我一般把registerCreator返回bool并且在重复注册时打warn日志bool registerCreator(std::string_view tag, Creator creator) { auto [it, inserted] creators_.emplace(std::string(tag), std::move(creator)); if (!inserted) { spdlog::warn(Duplicate creator registration: {}, tag); return false; } return true; }生产环境宁可用assert(inserted)快速失败也不要沉默吞掉重复注册。6.4 多线程环境下的工厂注册与创建如果工厂被多个线程并发调用unordered_map的读取和写入都需要加锁。最粗的做法是整体加std::mutex但并发较高时锁竞争也会成为瓶颈。经验做法注册阶段加写锁创建阶段加读锁或直接用读写锁std::shared_mutex如果创建函数内部也有状态比如缓存要确保该状态也是线程安全的注册动作通常是启动阶段一次性完成的可以在这些动作完成后把锁换成无锁只读表或者干脆创建阶段用一个fmt::format作为key然后用只读查找。class ThreadSafeTextureFactory { public: bool registerCreator(std::string_view tag, Creator creator) { std::unique_lock lock(mutex_); return creators_.emplace(std::string(tag), std::move(creator)).second; } std::unique_ptrTexture create(std::string_view tag, const TextureDesc desc) { std::shared_lock lock(mutex_); auto it creators_.find(tag); return it creators_.end() ? nullptr : it-second(desc); } private: mutable std::shared_mutex mutex_; std::unordered_mapstd::string, Creator creators_; };6.5 对象创建一半抛异常工厂里的异常安全工厂的create函数内部可能做很多事解码图片、分配GPU资源、加载着色器。其中任何一步抛异常已经创建的部分资源必须被回收。std::unique_ptrTexture createHdrTexture(const TextureDesc desc) { auto tex std::make_uniqueHdrTexture(desc.width, desc.height); // 假设这里失败抛异常 tex-uploadGpuData(desc.data); return tex; }如果uploadGpuData抛异常tex已经存在而调用方还没来得及接收返回值此时std::make_unique构造的HdrTexture对象管理的资源比如GPU句柄必须在其析构函数中释放。所以检查的关键是HdrTexture的析构函数是否正确释放了它内部持有的GPU资源如果是异常安全就没问题如果HdrTexture的析构函数里忘记释放句柄这里就会产生资源泄漏。我做这类代码时习惯先在内部使用unique_ptr或RAII包装类持有GPU句柄而不是裸句柄。这样即使创建流程中途失败清理也会自动完成。写完了再用std::make_unique返回基本不会出问题。6.6 给读者一个自查清单我在写代码评审时对工厂模式相关的代码有套固定检查项这里分享给读者基类析构函数是否虚返回值是智能指针而不是裸指针工厂内部是否处理了重复注册创建函数是否只做“创建装配”不掺业务逻辑创建失败时是否有统一处理路径返回nullptr或抛异常多线程访问时是否有锁或不可变设计工厂的间接性是否真的换来了价值新增类型是否真的更简单了最后分享一个小技巧如果你也想在项目中把工厂模式用得很顺手不妨从今天开始定一条规矩在代码里搜索所有出现if (type xxx) return new Yyy(...);的地方这些就是“散落的简单工厂”。逐个审视如果创建逻辑重复出现或者分支已经膨胀到两个以上就抽成一个注册表式工厂如果只是单个调用点的简单创建尽管留着别为了“设计感”乱动它。用工厂模式的最佳时机往往不是设计评审会上画UML图的时候而是真实代码里出现“创建逻辑重复”“分支蔓延”“调用点需要一个可替换的创建入口”等信号的时候。跟随信号走工厂模式带给你的就是真切的工程收益而不是一层昂贵的包装。
返回列表