设计模式 04 · 抽象工厂模式

📅 2026/8/4 4:21:54 👁️ 阅读次数 📝 编程学习
设计模式 04 · 抽象工厂模式

上一篇的工厂方法,解决的是"一个产品有多种实现,该造哪一个"。但现实里有一类更麻烦的情况:你要造的不是一个产品,而是一整套互相搭配、必须配套使用的产品。比如做一笔线上订单,你需要的不只是一个Order,还有配套的电子发票Invoice、以及虚拟发货单Shipment;而换成门店订单,同样是这三样,但每一样都得换成门店专用的版本——纸质发票、到店自提单。这三样东西必须成套出现,不能混搭:你不能给一笔线上订单配一张需要邮寄的纸质发票。

这种"成套、配套、不能混搭"的产品组合,就是抽象工厂模式(Abstract Factory)的主场。它是工厂三兄弟里最"重"、也最容易被误用的一个。这一篇我们把它讲清楚,尤其要讲透它一个非常独特、也最需要权衡的特性——开闭原则在它身上是"倾斜"的:某个方向的扩展轻松无比,另一个方向的扩展却要伤筋动骨。理解了这个倾斜性,你才算真懂了抽象工厂,也才知道它到底适不适合你的场景。

这篇文章按这条线索展开:先说清楚"产品族"这个核心概念,它和上一篇的"产品等级"有什么区别;再看如果硬用工厂方法去解决产品族问题会碰到什么麻烦,从而引出抽象工厂;然后讲清它的角色、骨架和那张关键的"网格图";接着重点剖析它的开闭倾斜性——为什么加一个新渠道很爽、加一个新单据却很痛;最后给出选型建议、现实身影,以及工厂三兄弟的总对比。全程用"线上/门店两种渠道 × 订单/发票/发货单三种单据"这个例子。

目录

  1. 两个维度:产品族与产品等级
  2. 硬用工厂方法会怎样:产品族的困境
  3. 抽象工厂登场:一个工厂造一整族
  4. 角色、骨架与那张网格图
  5. 最关键的取舍:开闭原则的"倾斜性"
  6. 什么时候该用,什么时候别碰
  7. 现实身影,与工厂三兄弟总对比

一、两个维度:产品族与产品等级

要理解抽象工厂,必须先建立一个"二维"的视角。上一篇的工厂方法,本质上只有一个维度:一堆同类产品(各种Payment)的不同实现。而抽象工厂面对的是两个维度交织的情况。

用我们的例子来说。一笔订单业务涉及三种不同类型的单据:

  • 订单Order
  • 发票Invoice
  • 发货单Shipment

同时,整个系统又有两个不同的渠道:

  • 线上渠道:线上订单、电子发票、虚拟发货单
  • 门店渠道:门店订单、纸质发票、到店自提单

把这两个维度画成一张表,一切就清楚了:

订单 Order发票 Invoice发货单 Shipment
线上渠道OnlineOrderElectronicInvoiceVirtualShipment
门店渠道StoreOrderPaperInvoicePickupShipment

这里有两个关键术语,务必分清:

  • 产品等级结构(Product Hierarchy):表格的每一列。比如"发票"这一列,ElectronicInvoicePaperInvoice都是发票,是同一种产品的不同实现——这正是上一篇工厂方法管的那个维度
  • 产品族(Product Family):表格的每一行。比如"线上渠道"这一行,OnlineOrder+ElectronicInvoice+VirtualShipment这三样,虽然类型不同,但同属线上渠道、必须搭配使用,它们组成一个"产品族"。

一句话记住这个区别:产品等级是"同一种东西的不同牌子",产品族是"同一个品牌下的整套不同东西"。工厂方法关心的是"列"(一种产品选哪个实现),抽象工厂关心的是"行"(一整族产品成套地造出来)。这就是两者最本质的分界。

二、硬用工厂方法会怎样:产品族的困境

假设我们不用抽象工厂,而是用上一篇的工厂方法来应付。那意味着:订单要一个OrderFactory,发票要一个InvoiceFactory,发货单要一个ShipmentFactory,每个还各自有线上、门店两个实现。业务代码里要造一套线上单据,得这么写:

Orderorder=newOnlineOrderFactory().create();Invoiceinvoice=newElectronicInvoiceFactory().create();// 得记得选"电子"Shipmentshipment=newVirtualShipmentFactory().create();// 得记得选"虚拟"

问题来了:"必须搭配"这个约束,现在全靠程序员自觉。这三行代码里,你必须自己保证选的都是"线上"系列——一旦手滑,给线上订单配了个PaperInvoiceFactory(纸质发票),编译器不会报错,代码也能跑,直到线上用户莫名其妙收到一张要邮寄的纸质发票,才发现出了事。

而且切换渠道更痛苦。如果某段逻辑要根据渠道整体切换,你得同时改这三行、把三个工厂都从"线上系列"换成"门店系列",一处漏改就出混搭 bug。产品族的核心诉求是"成套、一致",而工厂方法各管一列、彼此独立,天然就没法保证跨列的一致性。

这就是产品族的困境:我们需要的不是"分别造三个产品",而是"一次性造出保证配套的一整族产品"。谁能提供这种"打包保证"?抽象工厂。

三、抽象工厂登场:一个工厂造一整族

抽象工厂的思路很直接:不再为每种产品单独设工厂,而是为每个"产品族"设一个工厂;这个工厂一口气负责生产该族里的所有产品。

先定义各产品的抽象接口(这部分和以前一样):

publicinterfaceOrder{voidsubmit();}publicinterfaceInvoice{voidissue();}publicinterfaceShipment{voidship();}

关键在工厂接口——它不再只有一个create(),而是每种产品对应一个创建方法,一个工厂声明了造出"一整族"的能力:

// 抽象工厂:声明"造这一整族产品"的能力publicinterfaceOrderChannelFactory{OrdercreateOrder();InvoicecreateInvoice();ShipmentcreateShipment();}

然后,每个渠道(每个产品族)提供一个具体工厂,它造出来的三样东西天然就是配套的:

// 线上渠道工厂:造出来的必然是"线上全家桶"publicclassOnlineChannelFactoryimplementsOrderChannelFactory{publicOrdercreateOrder(){returnnewOnlineOrder();}publicInvoicecreateInvoice(){returnnewElectronicInvoice();}publicShipmentcreateShipment(){returnnewVirtualShipment();}}// 门店渠道工厂:造出来的必然是"门店全家桶"publicclassStoreChannelFactoryimplementsOrderChannelFactory{publicOrdercreateOrder(){returnnewStoreOrder();}publicInvoicecreateInvoice(){returnnewPaperInvoice();}publicShipmentcreateShipment(){returnnewPickupShipment();}}

现在业务代码变成了这样,配套一致性由工厂从结构上保证了:

publicclassOrderService{privatefinalOrderChannelFactoryfactory;// 只认一个族工厂publicOrderService(OrderChannelFactoryfactory){this.factory=factory;}publicvoidplaceOrder(){Orderorder=factory.createOrder();Invoiceinvoice=factory.createInvoice();// 不用操心选哪个牌子Shipmentshipment=factory.createShipment();// 一定和上面配套// ... 三者天然同属一个渠道,不可能混搭}}// 决定用哪个渠道,只需换一个工厂newOrderService(newOnlineChannelFactory());// 全套线上newOrderService(newStoreChannelFactory());// 全套门店

对比第二节的困境,升级点非常清晰:“必须搭配"这个约束,从"靠程序员自觉”,变成了"由具体工厂在代码结构上强制保证"。你只要选定了OnlineChannelFactory,它吐出来的三样东西不可能出现门店的版本——混搭在结构上就被杜绝了。而且切换渠道,现在只需要在最外层换一个工厂,业务代码一个字都不用动。这就是抽象工厂的核心价值:保证一族产品的配套一致性,并把整族的切换收敛到一个点。

四、角色、骨架与那张网格图

抽象工厂的角色和工厂方法类似,但"工厂"这一侧的职责变重了:

角色本例中是谁职责
抽象工厂(Abstract Factory)OrderChannelFactory接口声明创建一族产品的一组方法
具体工厂(Concrete Factory)OnlineChannelFactory一个族一个,负责造出本族的全套产品
抽象产品(Abstract Product)Order/Invoice/Shipment每种产品的抽象接口
具体产品(Concrete Product)OnlineOrder落在网格某个交叉格里的具体实现

理解抽象工厂,最好的方式就是回到第一节那张二维表,把它当成一张"网格"来看:

这张网格图是理解抽象工厂的钥匙,盯住它记三件事:

  • 纵向的每一列,是一个"产品等级结构"(同一种单据的不同实现),由抽象产品接口统领;
  • 横向的每一行,是一个"产品族"(同一渠道的整套单据),由一个具体工厂负责生产一整行;
  • 一个具体工厂 = 网格里的一整行。你选了哪个工厂,就锁定了哪一行,这一行里的产品自动配套。

把这张网格刻在脑子里,下一节那个最关键的"倾斜性",你一看就懂。

五、最关键的取舍:开闭原则的"倾斜性"

这是抽象工厂最重要、也最常被考察的一点,务必吃透。抽象工厂对开闭原则的支持是倾斜的——沿着网格的两个方向扩展,难度天差地别。

方向一:增加一个新的"产品族"(加一行)——非常容易,完美符合开闭。

假设现在要新增一个"直播带货"渠道。你只需要:新建一族具体产品(LiveOrderLiveInvoiceLiveShipment),再新建一个具体工厂LiveChannelFactory implements OrderChannelFactory,实现那三个创建方法。就完了。抽象工厂接口不用动,已有的线上、门店工厂不用动,业务代码不用动。这个方向,抽象工厂扩展起来行云流水。

方向二:增加一个新的"产品等级"(加一列)——非常痛苦,严重违反开闭。

假设现在每笔订单除了订单、发票、发货单,还要多一样"优惠券Coupon"。你得怎么改?

  1. 先在抽象工厂接口OrderChannelFactory里,加一个方法Coupon createCoupon();
  2. 而一旦接口加了方法,所有已经存在的具体工厂——OnlineChannelFactoryStoreChannelFactoryLiveChannelFactory——全部都得跟着实现这个新方法,一个都跑不掉。

你被迫回去修改了每一个现存的工厂类,这是彻头彻尾的违反开闭原则。加一列,牵动全身。

用一句话总结这个倾斜性:抽象工厂对"新增产品族"友好(加行容易),对"新增产品等级"敌视(加列极难)。这不是抽象工厂的 bug,而是它的固有特性——它是为"产品族维度频繁变、产品种类维度稳定"这种场景量身定做的。

这个特性直接决定了它的适用边界:只有当你确信"产品的种类(列)基本固定,而产品族(行)会不断增加"时,抽象工厂才是绝配。如果你的产品种类本身还在剧烈变动、经常要加新单据,那抽象工厂会让你每加一样东西都痛不欲生,这时候它就是错误的选择。

六、什么时候该用,什么时候别碰

抽象工厂是三兄弟里最重的,滥用的代价也最大——一上来就是一大堆接口和类。所以它的适用场景很挑,记住这几个前提,同时满足才考虑用:

适合用的信号:

  • 系统里确实存在成套、配套、不能混搭的产品组合(产品族的概念真实存在),比如换肤(一套皮肤里的按钮、边框、背景必须一致)、跨数据库(一套 MySQL 的连接、语句、结果集,或一套 Oracle 的,不能混用)、跨渠道单据。
  • 这些产品族会不断新增,而每族里的产品种类相对固定——正好卡在抽象工厂"加行容易"的甜区。
  • 你希望整族切换能一键完成,且从结构上杜绝混搭。

别碰的信号:

  • 产品之间根本没有"必须配套"的关系,只是单纯的多实现——那用工厂方法就够了,别硬套抽象工厂,凭空多出一堆类。
  • 产品的种类经常变动(经常要加新的产品等级)——抽象工厂的倾斜性会让你每次都改遍所有工厂,得不偿失。
  • 就一个产品族,未来也看不到第二个——那连工厂都未必需要,直接创建即可。

还是那句贯穿全系列的话:先确认"产品族"这个东西在你的业务里真实存在、且会沿着正确的方向(加行)增长,抽象工厂才配得上它的复杂度。为了不存在的"配套需求"预先搭一套抽象工厂,是这个模式最典型的过度设计——毕竟它一上来就要你写一堆接口,沉没成本很高。

七、现实身影,与工厂三兄弟总对比

抽象工厂在标准库里有几个非常经典的例子:

  • java.sql.Connection:它就是一个抽象工厂。ConnectioncreateStatement()prepareStatement()createBlob()……造出一整族互相配套的数据库操作对象。你连的是 MySQL,拿到的就是 MySQL 那一族实现;连的是 Oracle,就是 Oracle 那一族——族内配套,不会混。这正是"跨数据库产品族"的教科书案例。
  • javax.xml.parsers.DocumentBuilderFactoryjavax.xml.transform.TransformerFactory:名字里带 Factory,通过newInstance()拿到具体工厂,再由它生产配套的解析组件。
  • 各类 UI/换肤框架:一套主题工厂负责生产该主题下配套的按钮、文本框、滚动条,保证整个界面风格统一,是抽象工厂最直观的应用。

最后,把工厂三兄弟放在一起做个总收束,这也是这三篇的落点:

模式解决的问题一句话扩展代价
简单工厂创建逻辑散落在业务里一个工厂 + switch,收拢创建新增产品要改 switch(违反开闭)
工厂方法一种产品的多实现,要频繁扩展一个产品配一个工厂,下放决策新增产品 = 加一对类(符合开闭)
抽象工厂一整族配套产品,要成套创建/切换一个工厂造一整族,保证配套加族容易、加产品种类极难(倾斜)

一条清晰的升级链:简单工厂把创建收拢到一处;工厂方法把"造哪个"下放给子类工厂、解决单一产品的扩展;抽象工厂再升一维,把"造哪一族"打包给族工厂、解决配套产品的一致性。三者不是三选一,而是随着"变化压力"从一维走向二维的层层递进。选型时倒过来问自己就行:有没有配套的产品族?有 → 抽象工厂;没有,只是单产品多实现且要频繁扩展?→ 工厂方法;实现稳定、只想收个口?→ 简单工厂。


小结。抽象工厂是工厂家族的"二维版本",专治"一整族产品必须配套使用"的问题。它的核心是产品族这个概念——用一个具体工厂负责生产网格里的一整行,从结构上保证族内配套、杜绝混搭,并把整族切换收敛到换一个工厂。而它最需要记住的特质,是开闭原则的倾斜性:新增产品族(加行)轻松无比,新增产品种类(加列)牵一发而动全身。这个倾斜性既是它的适用边界,也是它的选型开关——只有"族频繁增、种类稳定"的场景才适合它,否则宁可退回工厂方法。到这里,工厂三兄弟就集齐了。下一篇我们离开"造哪个/哪族"的话题,转向另一个创建难题:当一个对象本身特别复杂、要一步步组装时,该怎么优雅地把它造出来——这就是建造者模式