ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

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

设计模式 04 · 抽象工厂模式 上一篇的工厂方法,解决的是一个产品有多种实现,该造哪一个。但现实里有一类更麻烦的情况:你要造的不是一个产品,而是一整套互相搭配、必须配套使用的产品。比如做一笔线上订单,你需要的不只是一个Order,还有配套的电子发票Invoice、以及虚拟发货单Shipment;而换成门店订单,同样是这三样,但每一样都得换成门店专用的版本——纸质发票、到店自提单。这三样东西必须成套出现,不能混搭:你不能给一笔线上订单配一张需要邮寄的纸质发票。这种成套、配套、不能混搭的产品组合,就是抽象工厂模式(Abstract Factory)的主场。它是工厂三兄弟里最重、也最容易被误用的一个。这一篇我们把它讲清楚,尤其要讲透它一个非常独特、也最需要权衡的特性——开闭原则在它身上是倾斜的:某个方向的扩展轻松无比,另一个方向的扩展却要伤筋动骨。理解了这个倾斜性,你才算真懂了抽象工厂,也才知道它到底适不适合你的场景。这篇文章按这条线索展开:先说清楚产品族这个核心概念,它和上一篇的产品等级有什么区别;再看如果硬用工厂方法去解决产品族问题会碰到什么麻烦,从而引出抽象工厂;然后讲清它的角色、骨架和那张关键的网格图;接着重点剖析它的开闭倾斜性——为什么加一个新渠道很爽、加一个新单据却很痛;最后给出选型建议、现实身影,以及工厂三兄弟的总对比。全程用线上/门店两种渠道 × 订单/发票/发货单三种单据这个例子。目录两个维度:产品族与产品等级硬用工厂方法会怎样:产品族的困境抽象工厂登场:一个工厂造一整族角色、骨架与那张网格图最关键的取舍:开闭原则的倾斜性什么时候该用,什么时候别碰现实身影,与工厂三兄弟总对比一、两个维度:产品族与产品等级要理解抽象工厂,必须先建立一个二维的视角。上一篇的工厂方法,本质上只有一个维度:一堆同类产品(各种Payment)的不同实现。而抽象工厂面对的是两个维度交织的情况。用我们的例子来说。一笔订单业务涉及三种不同类型的单据:订单Order发票Invoice发货单Shipment同时,整个系统又有两个不同的渠道:线上渠道:线上订单、电子发票、虚拟发货单门店渠道:门店订单、纸质发票、到店自提单把这两个维度画成一张表,一切就清楚了:订单 Order发票 Invoice发货单 Shipment线上渠道OnlineOrderElectronicInvoiceVirtualShipment门店渠道StoreOrderPaperInvoicePickupShipment这里有两个关键术语,务必分清:产品等级结构(Product Hierarchy):表格的每一列。比如发票这一列,ElectronicInvoice和PaperInvoice都是发票,是同一种产品的不同实现——这正是上一篇工厂方法管的那个维度。产品族(Product Family):表格的每一行。比如线上渠道这一行,OnlineOrderElectronicInvoiceVirtualShipment这三样,虽然类型不同,但同属线上渠道、必须搭配使用,它们组成一个产品族。一句话记住这个区别:产品等级是同一种东西的不同牌子,产品族是同一个品牌下的整套不同东西。工厂方法关心的是列(一种产品选哪个实现),抽象工厂关心的是行(一整族产品成套地造出来)。这就是两者最本质的分界。二、硬用工厂方法会怎样:产品族的困境假设我们不用抽象工厂,而是用上一篇的工厂方法来应付。那意味着:订单要一个OrderFactory,发票要一个InvoiceFactory,发货单要一个ShipmentFactory,每个还各自有线上、门店两个实现。业务代码里要造一套线上单据,得这么写:OrderordernewOnlineOrderFactory().create();InvoiceinvoicenewElectronicInvoiceFactory().create();// 得记得选电子ShipmentshipmentnewVirtualShipmentFactory().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.factoryfactory;}publicvoidplaceOrder(){Orderorderfactory.createOrder();Invoiceinvoicefactory.createInvoice();// 不用操心选哪个牌子Shipmentshipmentfactory.createShipment();// 一定和上面配套// ... 三者天然同属一个渠道,不可能混搭}}// 决定用哪个渠道,只需换一个工厂newOrderService(newOnlineChannelFactory());// 全套线上newOrderService(newStoreChannelFactory());// 全套门店对比第二节的困境,升级点非常清晰:“必须搭配这个约束,从靠程序员自觉”,变成了由具体工厂在代码结构上强制保证。你只要选定了OnlineChannelFactory,它吐出来的三样东西不可能出现门店的版本——混搭在结构上就被杜绝了。而且切换渠道,现在只需要在最外层换一个工厂,业务代码一个字都不用动。这就是抽象工厂的核心价值:保证一族产品的配套一致性,并把整族的切换收敛到一个点。四、角色、骨架与那张网格图抽象工厂的角色和工厂方法类似,但工厂这一侧的职责变重了:角色本例中是谁职责抽象工厂(Abstract Factory)OrderChannelFactory接口声明创建一族产品的一组方法具体工厂(Concrete Factory)OnlineChannelFactory等一个族一个,负责造出本族的全套产品抽象产品(Abstract Product)Order/Invoice/Shipment每种产品的抽象接口具体产品(Concrete Product)OnlineOrder等落在网格某个交叉格里的具体实现理解抽象工厂,最好的方式就是回到第一节那张二维表,把它当成一张网格来看:这张网格图是理解抽象工厂的钥匙,盯住它记三件事:纵向的每一列,是一个产品等级结构(同一种单据的不同实现),由抽象产品接口统领;横向的每一行,是一个产品族(同一渠道的整套单据),由一个具体工厂负责生产一整行;一个具体工厂 网格里的一整行。你选了哪个工厂,就锁定了哪一行,这一行里的产品自动配套。把这张网格刻在脑子里,下一节那个最关键的倾斜性,你一看就懂。五、最关键的取舍:开闭原则的倾斜性这是抽象工厂最重要、也最常被考察的一点,务必吃透。抽象工厂对开闭原则的支持是倾斜的——沿着网格的两个方向扩展,难度天差地别。方向一:增加一个新的产品族(加一行)——非常容易,完美符合开闭。假设现在要新增一个直播带货渠道。你只需要:新建一族具体产品(LiveOrder、LiveInvoice、LiveShipment),再新建一个具体工厂LiveChannelFactory implements OrderChannelFactory,实现那三个创建方法。就完了。抽象工厂接口不用动,已有的线上、门店工厂不用动,业务代码不用动。这个方向,抽象工厂扩展起来行云流水。方向二:增加一个新的产品等级(加一列)——非常痛苦,严重违反开闭。假设现在每笔订单除了订单、发票、发货单,还要多一样优惠券Coupon。你得怎么改?先在抽象工厂接口OrderChannelFactory里,加一个方法Coupon createCoupon();而一旦接口加了方法,所有已经存在的具体工厂——OnlineChannelFactory、StoreChannelFactory、LiveChannelFactory——全部都得跟着实现这个新方法,一个都跑不掉。你被迫回去修改了每一个现存的工厂类,这是彻头彻尾的违反开闭原则。加一列,牵动全身。用一句话总结这个倾斜性:抽象工厂对新增产品族友好(加行容易),对新增产品等级敌视(加列极难)。这不是抽象工厂的 bug,而是它的固有特性——它是为产品族维度频繁变、产品种类维度稳定这种场景量身定做的。这个特性直接决定了它的适用边界:只有当你确信产品的种类(列)基本固定,而产品族(行)会不断增加时,抽象工厂才是绝配。如果你的产品种类本身还在剧烈变动、经常要加新单据,那抽象工厂会让你每加一样东西都痛不欲生,这时候它就是错误的选择。六、什么时候该用,什么时候别碰抽象工厂是三兄弟里最重的,滥用的代价也最大——一上来就是一大堆接口和类。所以它的适用场景很挑,记住这几个前提,同时满足才考虑用:适合用的信号:系统里确实存在成套、配套、不能混搭的产品组合(产品族的概念真实存在),比如换肤(一套皮肤里的按钮、边框、背景必须一致)、跨数据库(一套 MySQL 的连接、语句、结果集,或一套 Oracle 的,不能混用)、跨渠道单据。这些产品族会不断新增,而每族里的产品种类相对固定——正好卡在抽象工厂加行容易的甜区。你希望整族切换能一键完成,且从结构上杜绝混搭。别碰的信号:产品之间根本没有必须配套的关系,只是单纯的多实现——那用工厂方法就够了,别硬套抽象工厂,凭空多出一堆类。产品的种类经常变动(经常要加新的产品等级)——抽象工厂的倾斜性会让你每次都改遍所有工厂,得不偿失。就一个产品族,未来也看不到第二个——那连工厂都未必需要,直接创建即可。还是那句贯穿全系列的话:先确认产品族这个东西在你的业务里真实存在、且会沿着正确的方向(加行)增长,抽象工厂才配得上它的复杂度。为了不存在的配套需求预先搭一套抽象工厂,是这个模式最典型的过度设计——毕竟它一上来就要你写一堆接口,沉没成本很高。七、现实身影,与工厂三兄弟总对比抽象工厂在标准库里有几个非常经典的例子:java.sql.Connection:它就是一个抽象工厂。Connection能createStatement()、prepareStatement()、createBlob()……造出一整族互相配套的数据库操作对象。你连的是 MySQL,拿到的就是 MySQL 那一族实现;连的是 Oracle,就是 Oracle 那一族——族内配套,不会混。这正是跨数据库产品族的教科书案例。javax.xml.parsers.DocumentBuilderFactory、javax.xml.transform.TransformerFactory:名字里带 Factory,通过newInstance()拿到具体工厂,再由它生产配套的解析组件。各类 UI/换肤框架:一套主题工厂负责生产该主题下配套的按钮、文本框、滚动条,保证整个界面风格统一,是抽象工厂最直观的应用。最后,把工厂三兄弟放在一起做个总收束,这也是这三篇的落点:模式解决的问题一句话扩展代价简单工厂创建逻辑散落在业务里一个工厂 switch,收拢创建新增产品要改 switch(违反开闭)工厂方法一种产品的多实现,要频繁扩展一个产品配一个工厂,下放决策新增产品 加一对类(符合开闭)抽象工厂一整族配套产品,要成套创建/切换一个工厂造一整族,保证配套加族容易、加产品种类极难(倾斜)一条清晰的升级链:简单工厂把创建收拢到一处;工厂方法把造哪个下放给子类工厂、解决单一产品的扩展;抽象工厂再升一维,把造哪一族打包给族工厂、解决配套产品的一致性。三者不是三选一,而是随着变化压力从一维走向二维的层层递进。选型时倒过来问自己就行:有没有配套的产品族?有 → 抽象工厂;没有,只是单产品多实现且要频繁扩展?→ 工厂方法;实现稳定、只想收个口?→ 简单工厂。小结。抽象工厂是工厂家族的二维版本,专治一整族产品必须配套使用的问题。它的核心是产品族这个概念——用一个具体工厂负责生产网格里的一整行,从结构上保证族内配套、杜绝混搭,并把整族切换收敛到换一个工厂。而它最需要记住的特质,是开闭原则的倾斜性:新增产品族(加行)轻松无比,新增产品种类(加列)牵一发而动全身。这个倾斜性既是它的适用边界,也是它的选型开关——只有族频繁增、种类稳定的场景才适合它,否则宁可退回工厂方法。到这里,工厂三兄弟就集齐了。下一篇我们离开造哪个/哪族的话题,转向另一个创建难题:当一个对象本身特别复杂、要一步步组装时,该怎么优雅地把它造出来——这就是建造者模式。
返回列表