尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
工厂方法模式详解:从“到处new对象”到优雅解耦
1. 为什么写这篇文章从“到处new对象”的痛感说起1.1 一种很常见的代码气味先聊一个场景。假设你维护着一个不算小的后端服务里面有一个消息推送模块用户下单后要发短信、发邮件、发站内信。业务方今天说“短信渠道加一个”明天说“邮件模板要改成国际版”后天又说“顺便接入一下钉钉机器人”。你打开代码一看发现推送入口处全是new SmsSender()、new EmailSender()、new DingTalkSender()。每加一个渠道就得改一遍入口方法加一个else if改一次测试用例再重新部署一次。这种代码并不算“写错”但它有一个很别扭的地方调用方本意是“你给我发个通知”结果它却被迫知道了所有具体渠道类的名字、构造函数参数、依赖的配置。这就是耦合——创建对象的逻辑和业务逻辑搅在一起所有类之间互相认识互相依赖。我最早接触设计模式里的工厂方法时就是被这种“到处new对象”的痛感逼着去学的。1.2 工厂方法是什么一句话讲清楚工厂方法Factory Method的核心思想并不复杂把“创建对象”这件事从调用方手里拿过来交给一个专门的工厂逻辑去处理。但和很多人理解的“一个工具类里写個switch”不同工厂方法强调的是“让子类去决定具体实例化哪一个类”。父类定义一个创建产品的接口子类各自实现这个接口返回不同类型的产品对象。一句话版本定义一个创建对象的接口让子类决定实例化哪个类。工厂方法把对象的创建延迟到子类中完成。这句话听起来很绕我拆开讲。假设有一个Logger接口里面有log(String message)方法。父类LoggerFactory声明了一个createLogger()方法但父类自己不知道返回什么子类FileLoggerFactory返回FileLogger子类ConsoleLoggerFactory返回ConsoleLogger。调用方只依赖LoggerFactory和Logger这两个抽象具体是文件日志还是控制台日志由运行时到底用了哪个子类工厂决定。1.3 适用人群与阅读收益这篇文章适合几类人刚学完面向对象三大特性、想进一步理解多态怎么落地的初学者写了一段时间业务代码、发现if-else和new越来越多、想引入设计模式改善结构的开发者以及准备面试、需要把几种工厂的关系讲清楚的人。我会从标准结构讲起给出一段可以运行的 Java 示例再对比简单工厂、工厂方法、抽象工厂三者的边界然后聊实际业务里的用法和框架身影最后把我踩过的坑一并列出来。看完之后你应该能判断“什么时候值得用工厂方法”“什么时候用了反而画蛇添足”。2. 工厂方法的标准结构拆解四个角色到底在干什么2.1 Product 与 ConcreteProduct产品和具体产品工厂方法模式里有两个基础角色Product和ConcreteProduct。Product是产品对象的抽象可以是一个接口也可以是一个抽象类。调用方能够接触到的只有Product不应该看到任何具体产品类。ConcreteProduct则是Product的具体实现也就是真正被创建出来的对象。举一个例子。假设你在做一个报表导出功能产品抽象是ReportExporter它声明了export(ReportData data)方法。具体产品包括PdfExporter、ExcelExporter、CsvExporter。对调用方来说它只需要知道“我拿到的是一个ReportExporter我能调用export方法”至于底层是 PDF 还是 Excel调用方不关心也不应该关心。2.2 Creator 与 ConcreteCreator工厂和被实现的工厂另两个角色是Creator和ConcreteCreator。Creator是工厂的抽象它声明了一个工厂方法通常签名长这样public abstract ReportExporter createExporter();Creator内部往往还有一些业务逻辑这些业务逻辑会调用这个工厂方法。ConcreteCreator则负责实现createExporter()返回具体的产品对象。这里有一个关键点Creator不一定要是抽象类它也可以是一个普通类里面的工厂方法默认抛异常或返回 null真正干活的是子类。但为了类型安全和语义清晰绝大多数场景下我会把Creator定义为抽象类或者定义成一个接口让实现类各自返回产品。2.3 工厂方法的运行时协作流程一个典型的调用流程是这样的调用方持有某个Creator类型的引用但它不知道这个引用背后到底是哪个ConcreteCreator。调用方调用 Creator 上的业务方法业务方法内部执行到某个地方需要产品对象于是调用createExporter()。由于 Java 的多态机制实际执行的是ConcreteCreator.createExporter()返回一个具体的ConcreteProduct。业务方法拿到Product类型的对象继续执行自己的逻辑。这个流程的核心收益是新增产品类型时不需要改动既有代码只需要新增一个ConcreteProduct和一个ConcreteCreator。这符合开闭原则——对扩展开放对修改关闭。3. 用一段能跑的代码落地模拟日志框架的演进3.1 第一版硬编码的日志器为了把模式讲透我模拟一个日志模块的演进过程。先看一个最原始的版本所有日志都输出到控制台调用方直接newpublic class ConsoleLogger { public void log(String message) { System.out.println([console] message); } } public class NotificationService { private ConsoleLogger logger new ConsoleLogger(); public void send(String message) { logger.log(准备发送通知: message); // 业务逻辑... } }这个版本能用但只要需求一变问题立刻出现。有一天产品经理说“日志不能只打到控制台要存到文件里出问题后能查记录。”于是你新建了一个FileLogger然后把NotificationService里的ConsoleLogger改成FileLogger。再过两天又要支持数据库日志、云日志。每次改动都是同一个套路新建具体类然后去所有使用的地方改构造函数。这种改动的痛苦在于NotificationService的职责是“发通知”它不该关心日志是写文件还是写控制台。日志的实现细节被强行塞进了业务类里导致任何日志层面的改动都会引发业务代码的连锁修改。3.2 重构引入 LoggerFactory我用工厂方法重构这段代码。先定义产品的抽象public interface Logger { void log(String message); } public class ConsoleLogger implements Logger { Override public void log(String message) { System.out.println([console] message); } } public class FileLogger implements Logger { private final String filePath; public FileLogger(String filePath) { this.filePath filePath; } Override public void log(String message) { // 实际场景这里会写文件这里简化为打印 System.out.println([file: filePath ] message); } }然后定义工厂的抽象public abstract class LoggerFactory { // 模板逻辑所有子类共用 public void log(String message) { Logger logger createLogger(); logger.log(message); } // 工厂方法由子类决定创建哪种 Logger protected abstract Logger createLogger(); }注意这个LoggerFactory不只是有createLogger()它还带了一个默认的log流程。这就是工厂方法最常见的形态父类定义骨架子类决定细节。接着实现两个具体工厂public class ConsoleLoggerFactory extends LoggerFactory { Override protected Logger createLogger() { return new ConsoleLogger(); } } public class FileLoggerFactory extends LoggerFactory { private final String filePath; public FileLoggerFactory(String filePath) { this.filePath filePath; } Override protected Logger createLogger() { return new FileLogger(filePath); } }改造后的NotificationService只依赖抽象public class NotificationService { private final LoggerFactory loggerFactory; public NotificationService(LoggerFactory loggerFactory) { this.loggerFactory loggerFactory; } public void send(String message) { loggerFactory.log(准备发送通知: message); // 业务逻辑... } }使用的时候根据环境配置选择工厂即可// 生产环境 NotificationService service new NotificationService(new FileLoggerFactory(/var/log/app.log)); // 本地开发 NotificationService localService new NotificationService(new ConsoleLoggerFactory());NotificationService不再认识ConsoleLogger、FileLogger任何一个具体类。它只知道LoggerFactory能帮它写日志至于日志写到哪是创建工厂时决定的。后续想加一个DatabaseLogger我只需要新增DatabaseLogger和DatabaseLoggerFactory然后在使用处更换工厂不用碰NotificationService内部任何代码。3.3 扩展新日志类型时发生了什么假设现在要新增一个 JSON 格式日志输出到 Kafka。扩展步骤很清晰写一个KafkaJsonLogger implements Logger。写一个KafkaJsonLoggerFactory extends LoggerFactory在createLogger()里返回KafkaJsonLogger。在系统启动配置的地方把LoggerFactory实例换成KafkaJsonLoggerFactory。全程没有改动LoggerFactory也没有改动NotificationService。这种扩展方式对新需求非常友好尤其是当系统里有十几个服务都在使用同一个日志模块时改动被限制在“新增类 切换装配点”两步回归测试范围大大缩小。提示工厂方法并不是银弹。如果产品类型极其稳定、几年都不会变那么引入这套结构反而增加类数量。是否值得取决于扩展频率。4. 别把工厂方法用成简单工厂三种模式的边界选择4.1 简单工厂集中式switch的便利与代价很多人在学工厂方法之前先接触过“简单工厂”的概念一个工厂类里面写一堆if或switch根据参数返回不同产品。public class LoggerFactory { public Logger createLogger(String type) { if (console.equals(type)) { return new ConsoleLogger(); } else if (file.equals(type)) { return new FileLogger(app.log); } throw new IllegalArgumentException(unknown logger type: type); } }这确实能解决“调用方直接new”的问题但它有一个明显的隐患每新增一种产品都要修改这个工厂类的判断逻辑。换句话说简单工厂的“开关”是集中式的修改不可避免。如果产品只有两三种这样写完全没问题代码也简单。但如果产品类型越来越多这个工厂类会膨胀测试用例也会围绕这一个类不断增加。严格来说简单工厂不是 GoF 二十三种设计模式之一它更像一种编程习惯。工厂方法则把它升级为“每个具体产品对应一个具体工厂”用“新增类”代替“修改判断分支”。4.2 工厂方法让子类决定产品工厂方法的核心理念是父类不知道也不关心具体产品是什么创建逻辑被推迟到子类。正因为如此新增产品类型时不需要改既有工厂只需要新增具体工厂去实现那个创建方法。代价也很明显每个产品都要配对创建一个工厂类类数量增加。如果产品本身只有两三个而且变化频率很低为此引入工厂方法反而会让项目结构显得臃肿。我在实际项目中见过有人为两个不会变的产品类型建了四个类结果后续三年一行代码都没改过这种设计就属于过度投资。4.3 抽象工厂产品族的组装再往上走一层抽象工厂Abstract Factory解决的是“产品族”的创建问题。工厂方法关注的是“一个工厂创建一个产品”抽象工厂关注的是“一个工厂创建一组相互关联的产品”。举个例子。一个跨平台 UI 组件库需要创建按钮、输入框、弹窗。Windows 风格的按钮和 Linux 风格的按钮是不同产品同时它们隶属于同一个风格族。用抽象工厂你会有一个UIFactory接口里面有createButton()、createInput()、createDialog()三个方法WindowsUIFactory返回一套 Windows 风格组件LinuxUIFactory返回一套 Linux 风格组件。抽象工厂往往基于工厂方法实现每个createXxx()方法就是一个工厂方法只是它们被组合在同一个接口里。所以这三者不是互斥关系而是层层递进的关系。4.4 一张表帮你做选型我在项目里做选型时通常会画这样一张对照表维度简单工厂工厂方法抽象工厂核心意图集中创建减少调用方的直接依赖让子类决定创建哪个产品创建一组相关的产品创建逻辑位置一个工厂类多个具体工厂子类多个具体工厂子类新增产品时修改工厂类判断分支新增具体产品 新增具体工厂新增具体产品族 新增具体工厂扩展代价低但集中修改中类数量增加中高涉及产品族一致性适用场景产品少、稳定、不易变化产品变化频繁、按环境区分产品族成套出现、需要保持一致选型建议其实很简单产品类型很少且几乎不变直接用简单工厂就够了产品类型多、扩展频繁、希望符合开闭原则用工厂方法存在多套产品族、需要保证配套关系用抽象工厂。别一上来就追求最复杂的方案设计模式是解决问题的手段不是 KPI。5. 实际业务里工厂方法的高频用法与框架身影5.1 配置驱动对象创建工厂方法在生产代码里最常见的用法之一就是和配置文件配合。系统启动时读配置根据配置项决定装配哪个具体工厂。比如日志、缓存、消息队列、存储引擎这些基础组件往往都会通过工厂方法来支持不同的实现。以缓存为例。接口Cache实现有RedisCache、LocalCache、ClusterCache。工厂CacheFactory是一个抽象类子类分别对应不同缓存实现。系统根据部署环境选择CacheFactory的实现类然后注入到业务代码里。业务代码全程只跟Cache打交道不会出现“如果用 Redis 就走 Redis 客户端如果用本地缓存就换成本地实现”这类判断。这种做法的好处是当你准备从单机缓存切换到集群缓存时业务代码一行都不改只需要在配置层替换工厂实现。替换后业务代码内部所有依赖Cache的调用自动生效。5.2 框架基类与模板方法配合工厂方法经常与模板方法模式一起出现。模板方法定义了业务步骤的骨架工厂方法则为某些步骤提供对象创建逻辑。我前面写的LoggerFactory就是这种组合log()方法是一个模板固定了“先创建 Logger再写日志”的流程createLogger()是一个工厂方法让子类自定义 Logger。在实际框架中这种写法很常见。比如一个消息处理框架父类定义processMessage(Message msg)的整体流程解析消息 → 创建处理器 → 处理 → 记录结果。其中“创建处理器”就是工厂方法每种消息类型对应一个子类子类返回不同的处理器。这样新增消息类型时只需要新增消息处理器和对应的子类框架主干不动。5.3 测试注入与mock协作工厂方法对单元测试也很友好。因为调用方依赖的是抽象工厂和抽象产品测试时可以注入一个 fake 或 mock 工厂让产品返回预置的对象。比如NotificationService依赖LoggerFactory测试里我可以写public class MockLoggerFactory extends LoggerFactory { private final ListString messages new ArrayList(); Override protected Logger createLogger() { return message - messages.add(message); } public ListString getMessages() { return messages; } }这样测试就能验证NotificationService.send()确实写入了预期日志而不需要真的去操作文件系统或控制台。这在接口测试里有很强的实用价值测试不需要依赖底层具体实现也能覆盖业务逻辑。5.4 工厂方法与依赖注入的配合关系有人会问既然有了依赖注入容器工厂方法是不是过时了我的看法是两者解决的问题不在同一层。依赖注入容器负责组装对象图工厂方法负责封装某个对象的创建逻辑。二者完全可以共存。实际中常见的组合是容器里注册LoggerFactory的具体实现业务类通过构造器注入LoggerFactory。这时候工厂方法的价值依然存在——它把“怎么创建一个具体的 Logger”的规则封装在工厂内部容器不关心Logger怎么来的。尤其是当 Logger 的构造过程需要读取配置、检查连接池、做参数校验时把这些逻辑放在工厂里比放在容器配置里更清晰也更容易单测。6. 踩坑手记使用工厂方法最容易翻车的六个场景6.1 产品类型判断写死在工厂里我把工厂方法写成了“半工厂方法”抽象工厂定义好了createLogger()但子类实现时又回到了if-else的怀抱。比如某个HybridLoggerFactory里根据配置项if (file.equals(config)) return new FileLogger(...)。这样一来新增类型时又要改这个子类开闭原则在子类内部被破坏了。正确做法是如果HybridLoggerFactory自己都判断不过来说明它内部实际上应该拆成多个独立工厂或者在更高层根据配置选择对应的工厂实现。工厂方法的价值在于“每个子类绑定一种产品”不要让一个工厂内部继续做产品路由。6.2 过度设计只有一个产品也要建工厂有次我要封装一个简单的配置读取器当时只有一种实现我却照着工厂方法搞了一套接口、抽象类、具体工厂的完整结构。事后三个月里这个配置读取器从未出现过第二个实现。那套额外增加的类全是负担——每次编译都要多编译几个文件阅读代码的人还要花时间搞清楚这些类之间的关系。这是我至今印象最深的一条教训设计模式的选择依据是“变化点”不是“未来的想象”。如果只有一个产品且没有任何迹象表明会有第二个直接返回具体对象就够了等真正出现第二个实现时再重构成本并不高。6.3 返回具体类导致调用方依赖泄露有些朋友写的工厂方法签名直接返回具体类型public ConsoleLogger createLogger() { ... }这会让调用方拿到ConsoleLogger随后自然而然地调用ConsoleLogger特有的方法。一旦你决定把实现替换成FileLogger调用方就会报错。工厂方法的签名应该返回抽象类型Logger哪怕当前只有一个具体产品也要保持返回类型的抽象层次。这就相当于对外承诺“我给你一个 Logger”而不是“我给你一个 ConsoleLogger”。承诺越具体以后越难变。6.4 忽略对象的生命周期管理工厂方法只管创建不管销毁。对于普通对象这没问题但如果产品是重量级对象比如数据库连接、HTTP 客户端、线程池创建成本很高就需要考虑缓存和复用策略。你可以让工厂在创建时做缓存也可以把生命周期管理交给容器。我遇到过一个真实问题一个工厂方法每次被调用时都新建一个数据库连接业务高峰期连接数飙升数据库被打挂。问题不在工厂方法本身而在于我没用心思考产品的生命周期。工厂方法返回一个对象之前你要清楚这个对象是短暂的还是有状态的要不要复用由谁负责释放。6.5 工厂类做成静态方法后不可扩展有人觉得工厂方法没必要实例化直接把创建函数写成static于是写成了public static Logger create(String type)。一旦用了静态方法你就没办法通过多态去覆盖它也没办法在测试中替换它。这实际上就退化成了简单工厂。如果你确实只有一个静态工厂那也没问题但不要称它为工厂方法。工厂方法需要“实例方法 继承”才能发挥作用。如果你需要在不同环境返回不同产品请放弃静态方法改用实例化工厂。6.6 产品构造参数差异过大时硬套工厂方法工厂方法适合“产品结构一致、只是类型不同”的场景。但如果不同产品需要的构造参数天差地别——比如一个需要读数据库配置、一个需要连接外部服务、一个干脆不需要任何参数——强行让所有产品走同一个工厂方法会导致参数传递非常别扭。这种情况下我往往会在工厂的子类构造时传入需要的依赖。比如FileLoggerFactory的构造器接收filePathKafkaJsonLoggerFactory的构造器接收 Kafka 连接地址。抽象工厂方法的签名保持精简具体差异在子类构造阶段消化。如果连“创建什么产品”都依赖运行时的强上下文那你更可能需要的是策略模式或建造者模式而不是工厂方法。7. 最后聊点个人体会写代码这几年我越来越觉得工厂方法不是一个“会用”就够了的模式它背后其实是一种思维方式把变化的创建逻辑和稳定的业务逻辑分开让调用方依赖抽象而不是依赖具体。这种思维方式比类图本身重要得多。我自己的使用习惯是先写出最朴素的版本冒着“以后要改”的风险写硬编码当我发现同一个创建流程第三次出现时再开始抽象工厂方法。这个节奏避免了过度设计也不会错过真正的扩展点。同时我会把工厂的选型决定尽量放到系统启动装配阶段让业务代码对“今天用哪个实现”完全无感。如果你正在读这篇博客我建议你自己动手把日志框架的例子写一遍然后试着加一种新日志类型体会一下“不改老代码、只加新类”的过程。然后反过来再试一下用简单工厂实现同样需求感受一下两种方式的差别。只有亲手对比过你才知道什么时候该用工厂方法什么时候简单粗暴更好。最后分享一个小技巧工厂方法的命名尽量统一比如都用createXxx()或buildXxx()。这样团队里的人看到方法名就能猜到这是一处创建逻辑代码的可读性会明显提升。模式的名字没那么重要重要的是大家都能快速理解这段代码在做什么。
RELATED

相关推荐

上网家长控制软件工程拆解:DNS过滤、SNI嗅探与eBPF高性能识别

上网家长控制软件工程拆解:DNS过滤、SNI嗅探与eBPF高性能识别

简介:这是一份面向家长与家庭教育场景的上网控制软件源码资源,用于帮助父母管理孩子的网络访问行为,解决网址过滤、上网时长限制与设备管控等实际问题,适合具备一定C#基础的开发者学习或二次开发。压缩包共40个文件,约…

📅 2026/10/15 7:48:02
从研究驱动到API工厂:AI实验室商业化的代价与启示

从研究驱动到API工厂:AI实验室商业化的代价与启示

这是一篇关于AI实验室商业化的评论文章。我注意到,按照合规要求,需要将真实公司名称替换为虚构代称,以下是基于此思路创作的内容。1. 从理想主义到API工厂:一场酝酿已久的变轨那封内部邮件在几个核心圈子里传开的时候,…

📅 2026/10/15 7:48:02
Golang哈希表(map)原理与性能优化实践

Golang哈希表(map)原理与性能优化实践

1. Golang中的哈希表基础概念哈希表作为计算机科学中最经典的数据结构之一,在Golang中通过map类型得到了优雅的实现。我第一次接触Go的map是在处理一个需要快速查找的用户数据缓存系统时,当时就被其简洁的语法和高效的性能所吸引。哈希表的本质是键值对的…

📅 2026/10/15 7:48:02
MORE NEWS

更多资讯

📰

R语言绘制Nature级科研图表的实战指南

1. 为什么需要绘制Nature级别的图表在科研论文写作中,图表质量直接影响研究成果的呈现效果。Nature作为国际顶级期刊,对图表有着极高的审美和规范要求。很多科研人员在投稿时常常因为图表质量不达标而被拒稿或要求返修。R语言作为统计分析和数据可视化的…

📰

具身智能机器人入门:从感知决策执行闭环到产业落地的完整指南

我去年第一次在真实产线上看到人形机器人干活,说实话,第一反应不是“哇未来已来”,而是“这玩意儿碰上意外停下要多久才能恢复”。真正让我把观念彻底掰过来的,是跟着咕泡科技 J4这个项目体系,从概念拆解、仿真环境搭建…

📰

Dart空安全机制解析与迁移实践指南

1. Dart空安全概述空安全(Null Safety)是Dart 2.12版本引入的核心语言特性,它从根本上改变了开发者处理null值的方式。我在实际项目迁移过程中发现,这项特性能够消除约70%的运行时null引用异常(NullPointerException&a…

📰

多智能体编排实战:用状态机、DAG与事件总线设计Agent调度中枢

接手第一个 Multi-Agent 项目的时候,我犯过一个特别典型的错误:把两个智能体的输出直接拼在一起当最终结果。表面上两个模型都能跑,但 A 需要的上下文在 B 执行完之后才生成,B 在等 A 的数据,A 又在等 B 的确认&#x…

📰

贝叶斯公式从推导到解题模板:期末复习与高频题型实战

几乎每学期的概率论期末复习课上,我都会被同一个问题拦住:“老师,贝叶斯公式我背下来了,但一到做题就不知道哪个是A、哪个是B。”这个问题问得特别实在。贝叶斯公式在概率论与数理统计期末考试里几乎稳定占据一道大题的位置&#…

📰

HTML表格开发全指南:从基础到高级应用

1. 表格基础概念与使用场景表格(Table)作为数据展示的基础元素,几乎出现在所有需要结构化呈现信息的场景中。从最简单的学生成绩单到复杂的企业财务报表,表格都能以清晰的二维结构将数据关系可视化。在网页开发领域,表…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬