1. 工厂模式家族全景从简单工厂到抽象工厂的演进逻辑很多人在学习C#设计模式时第一次遇到“工厂”这个概念都会有点懵。尤其是同时看到简单工厂、工厂方法、抽象工厂这三个名词很容易陷入一种“好像都是 new 对象有什么区别”的困惑。我当年也是这样光是区分这三个模式就花了不少时间直到后来在真实项目里反复重构成千上万行代码才真正摸清了它们各自的定位。先说说它们最核心的共同点都是用来处理“对象创建”的问题。在面向对象编程里最直观的创建方式就是直接 new 一个具体类比如new SqlConnection()、new RedisCache()。这种方式本身没什么问题但当项目规模变大、创建逻辑变复杂时直接 new 会让调用方和具体实现类死死耦合在一起。一旦需要替换实现、增加新的产品类型或者调整创建参数你就要到处去改调用代码这显然不是一种健康的结构。工厂模式家族要解决的就是这种耦合问题。它们把“创建对象”这件事从业务代码中抽离出来交给一个专门的“工厂”去处理调用方只需要告诉工厂“我要什么”至于这个对象到底怎么 new、要初始化哪些依赖调用方一概不关心。但同样是做“创建对象”这件事三个模式的抽象层次和设计意图完全不同。我的理解是它们其实是一条演进路径简单工厂是一个“函数”工厂方法是一组“虚方法”抽象工厂则是一个“产品族生产线”。简单工厂把所有创建逻辑塞进一个静态方法里工厂方法把创建逻辑下沉到子类抽象工厂则进一步把我们关心的对象按“族”来组织从而支持多套配套产品的切换。为了让大家有一个直观的认识先用三个最简化的代码片段感受一下它们各自的结构。简单工厂是这样public static class LoggerFactory { public static ILogger Create(string type) { if (type file) return new FileLogger(); if (type db) return new DbLogger(); throw new ArgumentException(未知的日志类型); } }工厂方法模式的核心结构则变成抽象工厂类加子类工厂public abstract class LoggerCreator { public abstract ILogger CreateLogger(); public void Write(string message) { var logger CreateLogger(); logger.Log(message); } } public class FileLoggerCreator : LoggerCreator { public override ILogger CreateLogger() new FileLogger(); }而抽象工厂模式的关注点从“单产品”变成“产品族”public interface IUIFactory { IButton CreateButton(); ITextBox CreateTextBox(); }看到这里你可能已经有点感觉了三者的抽象层级是递增的。简单工厂是最直观的“路由”工厂方法把路由权交给了继承体系抽象工厂则把多个相关产品的创建捆绑在一起。这个演进关系是理解它们区别的关键。不过这里要特别说明一个容易混淆的点简单工厂并不属于 GoF 的 23 种设计模式它更像是一种编程习惯或代码风格。很多教材把它当作“工厂三兄弟”之一来讲但在实际面试和架构讨论中GoF 官方只有工厂方法模式和抽象工厂模式两种。搞清楚这个背景很重要因为你可能会发现不同资料对简单工厂的评价不一原因就在此。2. 简单工厂模式入门之选也是争议之源2.1 核心结构一个静态方法管所有简单工厂模式最简单直接的理解就是把创建逻辑集中到一个静态工厂方法中方法内部通过参数判断要创建哪一个具体产品。下面是一个典型的支付场景例子。假设我们在做一个支付模块需要支持微信支付、支付宝支付、银行卡支付。如果不做任何设计业务层写出来的代码可能是这样的IPayment payment; if (payType wechat) { payment new WechatPayment(); } else if (payType alipay) { payment new AlipayPayment(); } else { payment new CardPayment(); }这种 if-else 散落在业务层各处一旦新增一种支付方式所有调用点都要改动。用简单工厂重构把这些判断集中到一处public class PaymentFactory { public static IPayment Create(string payType) { switch (payType) { case wechat: return new WechatPayment(); case alipay: return new AlipayPayment(); case card: return new CardPayment(); default: throw new NotSupportedException($不支持的支付类型: {payType}); } } }调用方就变成一行IPayment payment PaymentFactory.Create(payType); payment.Pay(order);这样做的好处非常直观创建逻辑收拢到一个类中业务层不用再关心支付对象怎么生成新增支付方式时只需要改工厂方法业务层代码一行不动。2.2 简单工厂的边界与局限简单工厂虽然好用但它有一个天然的缺陷工厂类承担了所有产品的创建职责一旦产品种类爆炸式增长这个 switch 或 if-else 会变得又臭又长而且每次新增产品都要修改工厂类违反了开闭原则。我在实际项目中就见过一个骑手调度系统里的对象工厂代码长达几百行switch 分支有二十多个每次新增一种调度策略都要小心翼翼地找到对应分支插入稍不留神就会改坏其他分支。这种场景下简单工厂就不再是“简单”而是变成了一种维护负担。所以我的经验是简单工厂适用于产品种类有限、扩展频率不高的场景。比如日志组件、配置解析、算法策略选择这类场景通常一年也新增不了几次产品用一个静态方法集中管理反而最清晰。但如果你预见到产品种类会频繁扩展就应该考虑升级为工厂方法模式。另外还有一个容易被忽略的技巧除了用字符串参数简单工厂还可以结合枚举、Type 类型或者泛型来增强类型安全。比如public static T CreateT() where T : IPayment, new() { return new T(); }但这种做法其实已经偏离了简单工厂的“按需路由”语义它变成了一种工厂方法的技术变体。在团队协作中使用何种方式应该明确约定避免有人用字符串、有人用泛型导致风格不统一。3. 工厂方法模式把选择权交还给子类3.1 一个“反耦合”重命名引发的思考工厂方法模式的出现正是为了解决简单工厂那个越来越大的 switch。它的核心思路是不再由一个工厂类全权负责所有产品的创建而是把工厂本身也抽象化每种产品对应一个工厂子类由子类决定具体实例化哪个产品。这样说有点抽象我们还是用日志系统来分析。假设系统需要支持文件日志和数据库日志并且后期很可能再接入消息队列日志。用简单工厂来做会有一个LogFactory.Create(file)之类的入口。但如果用工厂方法模式结构是这样的public abstract class LogManager { // 工厂方法由子类决定具体产出的日志对象 public abstract ILogger CreateLogger(); // 模板操作利用工厂方法创建的对象执行统一动作 public void WriteLog(string message) { ILogger logger CreateLogger(); logger.Log(message); } } public class FileLogManager : LogManager { public override ILogger CreateLogger() new FileLogger(); } public class DbLogManager : LogManager { public override ILogger CreateLogger() new DbLogger(); }这时业务层不需要在创建时传入任何字符串参数只需要选择一个具体的 Manager 实例即可LogManager manager new DbLogManager(); manager.WriteLog(订单已创建);注意到区别了吗简单工厂是把“决策”放在一个静态方法里调用方要告诉工厂“要哪个”工厂方法模式则是把“决策”放在子类工厂里调用方只需要决定“用哪条生产线”生产线自己知道该产出什么产品。3.2 为什么说它比简单工厂更“开闭”开闭原则是设计模式的核心思想之一它要求对扩展开放、对修改关闭。简单工厂新增产品时必须修改工厂类的 switch 分支而工厂方法模式新增产品时只需要新增一个具体的 Manager 子类和一个产品类完全不需要动已有的类。举个例子现在要接入 Redis 日志。我们只需要写一个RedisLogManager继承LogManager再写一个RedisLogger实现ILogger然后业务层在使用时 new 一下新 Manager 即可。原有的FileLogManager、DbLogManager全部不需要改动。这个特性在项目规模变大、参与人员变多之后价值很大因为“不修改已有代码”意味着不引入回归风险。但要注意工厂方法模式也不是没有缺点。它最大的代价是类的数量翻倍每增加一个产品就要同时增加一个产品类和一个工厂子类。如果产品种类有二十个那就可能产生四十个类文件。这让一些人觉得“过度设计”。我的态度是如果产品的扩展频率不高用简单工厂完全够用如果确认会有持续扩展并且希望扩展时不动旧代码那工厂方法模式就值得引入。3.3 与简单工厂的关键差异为了让你在代码层面快速判断自己用的是哪种模式我整理了一个区分技巧看创建方法的“归属”和“调用方式”。简单工厂的创建方法通常是一个静态方法且工厂类不参与继承体系工厂方法的创建方法是一个抽象的实例方法且必须由子类实现。另外简单工厂的调用方需要传入一个标识参数而工厂方法的调用方直接选择工厂对象即可。关于工厂方法还有一个容易忽略的价值模板方法模式的联姻。正如上面的例子LogManager.WriteLog本身就是模板方法它在父类定义算法骨架把某些步骤延迟到子类实现。工厂方法经常配合模板方法一起出现因为父类在算法流程中需要使用产品对象但它并不知道具体是哪个产品此时由子类工厂提供。4. 抽象工厂模式产品族的批量生产4.1 从“单产品”到“产品族”的跳跃工厂方法模式解决了“一个产品”的创建问题但如果我们需要创建的不是一个对象而是一组相互关联、必须配套使用的对象工厂方法模式就有些力不从心了。抽象工厂模式正是为了应对这个场景而生的。举个最经典的例子跨平台 UI 组件。假设我们要开发一个跨平台的桌面应用在 Windows 上按钮和文本框是 Windows 风格在 macOS 上按钮和文本框是 macOS 风格。这两个风格的组件之间是有配套关系的你不能在一个 Windows 风格的窗口里突然放一个 macOS 风格的按钮。这时我们就需要把这个“风格族”当做一个整体来管理。抽象工厂的结构是这样的public interface IUIThemeFactory { IButton CreateButton(); ITextBox CreateTextBox(); } public class WindowsThemeFactory : IUIThemeFactory { public IButton CreateButton() new WindowsButton(); public ITextBox CreateTextBox() new WindowsTextBox(); } public class MacThemeFactory : IUIThemeFactory { public IButton CreateButton() new MacButton(); public ITextBox CreateTextBox() new MacTextBox(); }调用方在使用时只需要选定一个 ThemeFactory后续创建的所有组件都天然保持了风格一致IUIThemeFactory factory GetCurrentThemeFactory(); IButton button factory.CreateButton(); ITextBox textBox factory.CreateTextBox();如果后面要支持 Linux 风格只需要新增LinuxThemeFactory以及对应的LinuxButton、LinuxTextBox已有的 Windows 和 Mac 工厂完全不需要修改。这与工厂方法模式扩展单个产品不同抽象工厂扩展的是整个产品族。4.2 产品族的“配套约束”是核心价值抽象工厂模式真正厉害的地方不在于它能创建多个对象而在于它用接口约束保证了“配套一致性”。如果不用抽象工厂你在业务层可能这样写IButton button new WindowsButton(); ITextBox textBox new MacTextBox();这在编译期是完全合法的但在运行期会出现风格不匹配的问题。抽象工厂通过把同一产品族的创建方法放在同一个接口中让“配套”关系在类型层面变得可见从根本上杜绝了随意混搭的可能。这种思路在我们实际项目中非常有用。我之前参与过一个报表导出功能的开发需要支持 PDF、Excel、CSV 三种格式的导出。每种格式不仅需要导出器还需要对应的样式配置、页眉页脚生成器。如果不做约束很容易出现“导出器是 PDF 但页眉生成器用了 Excel 的”这种低级错误。用抽象工厂把每个格式的完整导出链绑定起来问题就迎刃而解。4.3 抽象工厂的平衡艺术抽象工厂也不是银弹。它最大的问题是如果产品族中要新增一个产品比如在 UI 主题里增加一个下拉框IDropdown那么所有具体的 ThemeFactory 都要新增对应方法这意味着所有现有实现都要修改。这明显违反了开闭原则。所以抽象工厂更适合产品等级结构相对稳定的场景一旦产品族的成员基本确定抽象工厂就是非常好的选择。还有一点抽象的层次要根据实际需求来定。不一定要每个 UI 组件都抽象成接口如果某些组件在所有主题下实现一致那就不需要放进抽象工厂。抽象工厂只应该管理那些确实存在“族”差异的产品这个概念很多人初学时会搞混觉得抽象工厂生成的每个对象都必须有独立接口其实完全没必要过度抽象反而会增加系统复杂度。让我用一个实际例子说明这种“适度抽象”的做法。某跨平台报表系统的 UI 部分我只把 Button 和 TextBox 抽象成接口因为这两个组件在 Windows 和 macOS 下差异最大而 Label 只是显示文本各平台实现几乎相同我就直接使用基础类不参与抽象工厂。这样既保证了风格一致性又避免了无谓的复杂。5. 三种模式的区别对照与应用选型指南5.1 一张表看懂三者的核心差异经常有人问我“面试的时候怎么把这三个模式的区别说清楚”我会建议他们在脑子里先建立一张对照表而不是死背概念。下面这张表是我日常给团队培训时用的版本对比维度简单工厂工厂方法抽象工厂抽象层级一个静态方法一个抽象工厂类一组工厂接口关注对象单个产品单个产品产品族创建方式通过参数判断子类工厂决定族内工厂决定是否属于 GoF 23否是是扩展单个产品的代价修改工厂方法新增产品类和工厂子类新增产品类和族内所有工厂实现扩展产品族的代价修改工厂方法不适用需修改所有工厂接口实现开闭原则支持差好产品等级结构稳定时好适用场景产品少、扩展少产品多、需灵活扩展产品族配套使用这张表的信息量比较大但我想强调一个关键理解简单工厂和工厂方法解决的是同一个维度的“产品数量问题”而抽象工厂解决的是另一个维度的“产品族配套问题”。你可以把工厂方法看成“横向扩展”把抽象工厂看成“纵向扩展”。5.2 一个决策清单帮你快速选型在实际项目中我一般会按照下面这个思路来选型你也可以把这个流程当作一个决策清单来用第一步先问自己当前需要创建的是一类对象还是多个配套对象如果是一类对象看看产品数量是否可能频繁增加。如果是多套配套对象直接考虑抽象工厂。第二步如果是一类对象再问产品种类是否稳定如果一年到头也就三五种而且新增频率很低用简单工厂就够了。引入工厂方法反而会让类数量暴增不划算。第三步如果预见到产品种类会持续增加或者当前代码已经有多个散落的 new 调用点优先选择工厂方法模式。它可以让你在新增产品时完全不需要改动已有工厂类。第四步如果是多个配套对象还要确认产品族的成员是否稳定比如 UI 主题Button、TextBox 这些组件基本不会变。如果稳定抽象工厂是非常好的设计如果产品族还在快速演进比如今天要加一个组件、明天要换一个组件抽象工厂在扩展时就要修改所有工厂实现此时可能需要结合依赖注入或策略注册表等方式来缓解。这里有一个我踩过的坑在某支付网关项目中一开始我用简单工厂管理所有支付渠道后来支付渠道从三四种增长到十多种简单工厂的 switch 越来越大维护成本飙升。后来我用工厂方法模式做了重构类数量一下子增加了不少但是每新增一种支付渠道只需要新增两个文件其他代码完全不动重构之后系统扩展变得极其顺畅。这个经验告诉我选型的核心不是“用哪个模式显得高级”而是“哪种模式最匹配当前的扩展预期”。5.3 工厂模式与 IoC 容器的关系讨论工厂模式时有一个绕不开的话题控制反转容器IoC 容器和工厂模式有什么关系其实像 ASP.NET Core 内置的依赖注入容器、第三方框架里的容器本质上就是一个超级工厂它集中管理着各种对象的生命周期和创建方式。但我们日常使用的依赖注入往往是通过向构造函数声明接口来完成对象获取与手写工厂类相比IoC 容器更灵活、更自动。那是不是有了 IoC 容器工厂模式就没用了并不是。容器本身只是解决了“如何创建和注入对象”的问题但它并不能很好地表达“根据动态参数选择具体产品”的业务语义。比如根据支付方式选择不同的支付渠道这个选择逻辑必须由业务代码显式控制容器没法自动帮你判断。所以在实际项目中工厂模式和 IoC 容器经常是搭配使用的工厂负责“按需创建”容器负责“全局装配”。我提倡的设计方式是工厂类本身注册到容器中业务层从容器获取工厂然后工厂根据业务参数创建具体产品。这样既享受了容器的生命周期管理又保留了工厂模式的“按需路由”能力。如果你正在用 ASP.NET Core这种组合方式非常常见。6. 实战踩坑与排查经验从“跑通”到“跑对”6.1 我遇到过的三类典型问题工厂模式在运行期最常见的坑概括起来有三类第一类是工厂返回了错误的产品类型但没有在编译期暴露第二类是产品对象的生命周期问题导致资源泄漏第三类是过度抽象让简单的场景变得难以理解。先说说第一类问题。很多人会在简单工厂里用字符串做参数比如Create(wechat)。这种写法从类型安全角度看很脆弱一旦传入了拼写错误的字符串运行期才会抛出异常。更隐蔽的是如果有人把参数大小写搞混或者在不同模块中使用了不同的命名规范就可能出现“创建不出来产品”或者“创建了错误产品”的问题。我的建议是尽量使用枚举或常量类来约束参数别让魔法字符串满天飞。第二类问题更隐蔽。工厂创建的产品对象往往需要在一段业务逻辑结束后释放资源比如数据库连接、文件流、网络连接。很多人在工厂模式里忽略了 IDisposable 接口的处理导致每次创建都增加一个未释放的句柄运行时间一长系统性能就明显下降。一个简单的经验法则是如果产品对象实现了 IDisposable工厂创建后应该由调用方负责释放或者工厂内部做好统一管理。第三类问题是我最想提醒的不要为了用模式而用模式。我见过一个只有三种产品的团队硬是用工厂方法模式拆出了六个类加上接口、抽象类、泛型约束最后代码的可读性反而大幅下降。设计模式的目的是让结构更清晰而不是更复杂。在你没有“多个调用点、频繁扩展、产品族配套”这些真实需求之前简单工厂甚至直接 new 都是合理的。6.2 一个工厂模式重构的实战案例为了让你更直观地理解工厂模式的实战价值我讲一个之前做订单系统的重构案例。系统最初只有支付宝支付和微信支付两种支付方式代码直接在订单服务里写死通过 if-else 判断。后来每接入一个新的支付渠道都要在订单服务、退款服务、对账服务三个地方修改 if-else。第三次接入新渠道时我终于受不了了决定做一次工厂模式重构。我先定义一个IPayment接口里面统一了支付、退款、查询等方法。然后为每个支付渠道创建一个具体实现类例如WechatPayment、AlipayPayment。接着创建一个支付渠道工厂在初始化时通过配置注册各渠道的策略。最后把订单、退款、对账三个服务里的 if-else 全部替换为通过工厂获取支付对象。这个重构最直观的收益是后续新增支付渠道只需要写一个实现类和一个注册配置三个服务代码完全不用动。与此同时测试也变得更容易了因为我们可以针对每个渠道单独编写测试而不会让业务逻辑散落在三个服务里。从这次重构中我总结出一个规律工厂模式最适合用在“创建逻辑有变化趋势”的位置。判断一个位置是否需要引入工厂最有效的方法是统计创建逻辑在项目中出现的次数。如果一个具体类的 new 出现在十个地方那么一旦要替换实现你就得改十个地方。如果你把这十处 new 统一收拢到一个工厂里替换实现时只需要改一个地方。这就是工厂模式降低维护成本的核心原理。6.3 排查问题的顺序与工具实际开发中如果遇到工厂模式相关的问题我的排查顺序一般是这样的先确认产品类本身是否实现正确再确认工厂类是否返回了正确的类型然后检查调用方拿到的对象是否执行了正确的初始化步骤最后看生命周期和释放逻辑有没有问题。有一个非常实用的小技巧在开发阶段给工厂类加上日志输出把工厂收到的参数和返回的产品类型记录下来。很多同伴觉得生产环境加日志会拖慢性能其实针对工厂类的日志开销极小而且它只发生在对象创建时不是高频操作。有了这些日志排查问题时会节省大量时间。另外在单元测试中工厂模式是特别好测的目标。你可以通过依赖注入或者直接调用工厂方法验证不同类型的参数是否返回了预期的产品类型。如果测试中需要模拟某些难以创建的产品可以使用 mock 库直接代替工厂让测试只关注业务逻辑。6.4 结合依赖注入进一步简化工厂最后分享一个我个人很喜欢的组合实践把工厂模式与依赖注入结合起来。以 ASP.NET Core 项目为例我们可以把不同产品类注册为带名称的服务或者注册为 IEnumerable然后工厂通过构造函数获取所有实现根据业务参数选择其中一个返回。这种方式的好处是新增产品时完全不需要动工厂代码只需要新增一个实现类并完成注册工厂就能自动发现并处理。这比手写 switch 的简单工厂更加彻底地支持开闭原则。不过这种方式对团队的抽象能力要求更高如果团队中有人不太熟悉 DI 容器的用法可能会觉得难以理解。我从实践中得到的经验是在项目初期先使用最容易理解的简单工厂等团队成员对模式有了统一认识之后再逐步演进到工厂方法或抽象工厂甚至引入 DI 容器方案。工厂模式的选型不是一个“越高级越好”的问题而是一个“成本与收益匹配”的问题。理解每个模式背后的权衡比背下每个模式的定义更加重要。这些年在不同项目里反复切换三种工厂模式我的体会是真正的高手不是能写出最复杂的设计而是能在正确的场景选择最合适的设计并且能够讲清楚自己为什么这样选择。
阅读完成 · 觉得有帮助?