ARTICLE DETAIL

资讯详情

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

抽象工厂模式详解:从餐厅类比到跨平台UI与多数据库实战

抽象工厂模式详解:从餐厅类比到跨平台UI与多数据库实战 1. 从一个餐厅订餐的日常场景说起先讲个我经常拿来类比的生活故事。假设你今天晚上想请朋友吃饭走进一家西餐厅你不会对服务员说给我来一份牛排加一份筷子和蘸料你会很自然地认为这家餐厅提供的餐具、饮品、菜品风格是配套的——刀叉、红酒杯、餐前面包、主菜所有东西都带着同样的西式调性。要是换成一家人均二十块的路边小面馆呢你也不会幻想它给你上牛排配红酒他们端出来的是筷子、瓷碗、辣椒油和一碟小菜。这个进了一家店就能自动拿到一整套风格统一的配套产品的直觉放到程序世界里就是抽象工厂模式Abstract Factory Pattern要解决的核心问题。它是一种创建型设计模式属于GoF 23种经典设计模式中的一员。它的作用是**提供一个创建一系列相关或相互依赖对象的接口而无需指定它们具体的类。**用大白话翻译就是——你只要说我要一家西餐厅程序就自动帮你把刀叉、红酒、牛排全部备齐你说我要一家面馆就自动给你配上筷子、瓷碗、辣椒油。你从头到尾都只知道自己在跟餐厅打交道。这篇内容适合谁我觉得至少有三类人需要认真看学了一段时间面向对象但一直搞不清楚工厂方法、抽象工厂、简单工厂三兄弟区别的入门同学做业务系统时发现if else 越写越臭、想找到一种优雅的按类型创建一整套对象方案的开发人员准备面试需要把设计模式讲到能落地、能举一反三的求职者。我不打算讲那种满屏UML箭头的学院派教程咱们就站在一个写业务代码的人的角度把抽象工厂模式拆开揉碎从类比到代码从代码到实战再把容易踩的坑一个个躺一遍给你看。2. 抽象工厂模式的设计思路到底在抽什么2.1 为什么不能直接用 new 一把梭很多人学设计模式之前写代码的习惯是需要什么对象就直接new一个。比如程序中要弹出一个弹窗那就new WindowsDialog()要画一个按钮那就new WindowsButton()。这样做在项目小、平台单一的时候确实问题不大但一旦业务复杂起来问题马上就会出现。想象一家公司要做一套跨平台的桌面软件同时支持 Windows 和 macOS。如果代码里到处是new WindowsButton()、new WindowsDialog()等 macOS 考过来的时候你只能全局搜索所有new WindowsXxx一个个替换成new MacXxx。替换本身不可怕可怕的是你根本无法确认是不是所有地方都替换干净了有些对象创建逻辑嵌套在深层业务代码里牵一发动全身测试还没覆盖完产品又说我们还要支持 Linux。所以直接 new 表面上是省事实际上是把用什么具体类的决策分散到了程序的各个角落。一旦决策点发生变化比如平台切换所有调用点就跟着遭殃。抽象工厂的思路恰恰相反它把创建对象的代码集中收集到一个独立的工厂对象里业务层只认工厂接口不认具体产品。你告诉工厂我现在要 Windows 风格的按钮和弹窗工厂保证给你一整套 Windows 风格的产品你说切换成 macOS 风格工厂整体换一套业务代码几乎不用动。2.2 工厂方法 vs 抽象工厂一句话划清界限这里必须先区分一个非常容易被混淆的概念——工厂方法模式。面试的时候十个有八个会被问到这两个有什么区别。我用一个特别生活化的例子来分清楚工厂方法模式是一个工厂只做一种产品。比如你有一个饮料工厂这个工厂能根据参数产出可乐、雪碧、橙汁但不管是哪种它本质上都还是饮料这一条产品线上的东西。你要新增一种薯片对不起这个饮料工厂管不了。抽象工厂模式是一个工厂能发出一整套配套产品。比如你有一个快餐套餐工厂它一次给你配齐汉堡 薯条 可乐还有一个中餐套餐工厂一次给你配齐米饭 时蔬 汤。汉堡和可乐是不同类别的产品但它们之间有着同属一个套餐风格的强关联。一句话总结工厂方法解决的是单个产品族的单个对象怎么按需创建抽象工厂解决的是一整套相关产品对象怎么保证风格统一、整体替换。所以抽象工厂天然适用于强调产品族概念的场景跨平台UI组件、数据库连接层的多数据库适配、主题换肤系统、云服务厂商的SDK封装……这些都是典型的一整套对象必须同进同退的业务形态。2.3 抽象工厂里的四张关键牌抽象工厂模式一共有四个核心角色理解这四个角色代码怎么写就水到渠成了抽象产品AbstractProduct为一类产品定义统一接口。比如按钮接口声明渲染方法。具体产品ConcreteProduct同一类接口下的不同实现。比如WindowsButton、MacButton。抽象工厂AbstractFactory声明一组创建产品的方法。每个方法对应一类产品比如创建按钮创建弹窗。具体工厂ConcreteFactory实现抽象工厂产出某一套具体的产品组合。比如WindowsFactory只产 Windows 风格的按钮和弹窗MacFactory只产 Mac 风格的按钮和弹窗。这里有一个很容易被忽略的设计要点**在抽象工厂模式里客户端代码依赖的是抽象工厂 抽象产品这两层接口它永远不直接依赖具体产品类。**一旦某天你想让程序从Windows 皮肤切换到Mac 皮肤只需要在最顶层换掉具体工厂的实现底下的业务逻辑全部不用动。这是抽象工厂最值钱的地方——它把变化拦截在创建环节让系统的其他部分稳定运行。3. 用代码把抽象工厂真正跑起来3.1 一个最直观的跨平台UI案例光说不练假把式。我们写一个最简单的跨平台 UI 生成器只涉及两类产品按钮Button和弹窗Dialog。平台只模拟 Windows 和 Mac 两种。先定义抽象产品接口// 按钮接口所有平台的按钮都必须实现这个接口 public interface Button { void render(); }// 弹窗接口所有平台的弹窗都必须实现这个接口 public interface Dialog { void show(); }然后写具体产品Windows 风格的按钮和弹窗。public class WindowsButton implements Button { Override public void render() { System.out.println(渲染一个 Windows 风格的按钮方方正正带直角边框); } }public class WindowsDialog implements Dialog { Override public void show() { System.out.println(弹出 Windows 风格对话框标题栏是经典蓝色渐变); } }Mac 风格的按钮和弹窗public class MacButton implements Button { Override public void render() { System.out.println(渲染一个 macOS 风格按钮圆角平滑带毛玻璃阴影); } }public class MacDialog implements Dialog { Override public void show() { System.out.println(弹出 macOS 风格对话框视觉风格简约偏扁平); } }接着定义抽象工厂接口声明两类产品的生产方法public interface UIFactory { Button createButton(); Dialog createDialog(); }再实现两个具体工厂分别负责产出一整套配套风格的产品public class WindowsFactory implements UIFactory { Override public Button createButton() { return new WindowsButton(); } Override public Dialog createDialog() { return new WindowsDialog(); } }public class MacFactory implements UIFactory { Override public Button createButton() { return new MacButton(); } Override public Dialog createDialog() { return new MacDialog(); } }最后写一段客户端代码验证这套设计public class Application { private Button button; private Dialog dialog; public Application(UIFactory factory) { // 客户端的核心逻辑只知道在跟 UIFactory 打交道 // 至于具体是 Windows 还是 Mac那是工厂内部的事。 button factory.createButton(); dialog factory.createDialog(); } public void run() { button.render(); dialog.show(); } public static void main(String[] args) { // 模拟运行时按平台选择工厂 String osName System.getProperty(os.name).toLowerCase(); UIFactory factory; if (osName.contains(windows)) { factory new WindowsFactory(); } else { factory new MacFactory(); } Application app new Application(factory); app.run(); } }运行结果会很清爽如果当前是 Windows 系统输出 Windows 风格按钮 Windows 弹窗如果是 Mac则输出 Mac 风格按钮 Mac 弹窗。注意Application这个类从头到尾没有出现任何一个WindowsButton或是MacButton的字眼它只是拿到一个UIFactory接口然后调用方法。真正决定整套产品风格的是外部传入的那个具体工厂对象。这就是抽象工厂模式想达到的效果业务代码与具体产品实现彻底解耦横向扩展新平台时老的业务代码一个字都不用改。3.2 再进一步怎么让它更好用上面的代码已经能工作但还有两个常见的优化点我在实际项目中几乎每次都这么做第一个优化用配置文件替换 if else 的工厂选择逻辑。上面main方法里的 if else 只判断了两个平台如果产品线扩展到10个平台这个条件判断会越来越难看。一种常见的做法是引入配置中心public class FactoryProducer { public static UIFactory getFactory(String configKey) { String className ConfigLoader.getProperty(configKey); try { Class? clazz Class.forName(className); return (UIFactory) clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException(工厂类加载失败: configKey, e); } } }这样新增平台只需要新增一个配置项连代码都不需要重新编译部署就能切换整套皮肤。第二个优化工厂对象做成单例。具体工厂类往往是无状态的每次new一个实例完全没意义。我一般会顺手加个单例或者用枚举实现保证全局只有一个 WindowsFactory、一个 MacFactory节省那点微不足道的对象创建开销也让代码更规整。3.3 这套模式里最容易被忽略的一致性约束抽象工厂名字里带抽象两个字很多人会忽略它隐含的一条约束同一套工厂生产出来的所有产品必须保证风格一致、逻辑配套。这个一致性有时候比能创建对象更重要。举一个我真实踩过的坑之前做一个数据上报系统需要同时支持把数据写入 MySQL 和 ClickHouse。我一开始按抽象工厂设计了两个工厂类MySQL 工厂创建写入器和查询器ClickHouse 工厂也创建写入器和查询器。一切看起来很正常。直到有一天同事为了赶进度直接在某段业务代码里new了一个 MySQL 的查询器硬塞给 ClickHouse 的写入链路用。结果数据写入正常查询的时候字段类型对不上线上排查了整整一个下午。后来我加了代码评审规范**凡是使用抽象工厂的地方不允许在业务层直接创建具体产品类。**这才从根本上防住了这种混搭事故。所以如果你决定用抽象工厂就要把产品族内部必须成套使用这条规则刻在团队规范里否则这套模式的保护力就大打折扣。4. 抽象工厂模式的一个完整实战多数据库支持4.1 需求场景与选型分析UI 组件是理解抽象工厂最容易的例子但很多后端开发觉得 UI 离自己太远。所以我们再来看一个更常见的后端场景一个小型 SaaS 系统最开始只支持 MySQL后来客户要求必须还能接 PostgreSQL再后来又有客户希望跑在低成本的 SQLite 上用于内网单机部署。这个需求如果不用设计模式典型实现是这样public class UserDao { // 每个方法里都去判断当前用的是什么数据库 public void save(User user) { switch (currentDbType) { case MYSQL: // mysql-specific insert break; case POSTGRES: // postgres-specific insert break; case SQLITE: // sqlite-specific insert break; } } }这种写法现在看着还能忍等业务表一多、查询逻辑复杂起来每个方法都是一坨按类型分支的数据访问代码新加一种数据库所有 DAO 方法全都要跟着改漏改一个就是线上事故。用抽象工厂来重新设计的话思路完全不一样抽象产品数据库连接器 —— 负责创建连接抽象产品用户操作对象 —— 负责针对用户表的增删改查抽象工厂数据库工厂 —— 负责创建上面这一整套数据库访问组件具体工厂MySQL 工厂、PostgreSQL 工厂、SQLite 工厂各出一整套实现。4.2 用 Spring Boot 配置抽象工厂如果在 Spring Boot 项目里用抽象工厂实践上会简化很多因为我们不需要自己管理工厂实例全部交给 Spring 容器。假设我用 MyBatis 作为 ORM 层那么抽象产品就直接抽象成 Mapper 接口和SqlSessionFactory代码实现大概这样public interface DatabaseFactory { SqlSessionFactory createSqlSessionFactory(); UserMapper createUserMapper(); }MySQL 工厂Component ConditionalOnProperty(name app.database.type, havingValue mysql) public class MySqlDatabaseFactory implements DatabaseFactory { Autowired private DataSource mysqlDataSource; Override public SqlSessionFactory createSqlSessionFactory() { // 基于 mysqlDataSource 构建 SqlSessionFactory return new SqlSessionFactoryBuilder().build(config); } Override public UserMapper createUserMapper() { return createSqlSessionFactory().openSession().getMapper(UserMapper.class); } }PostgreSQL 工厂和 SQLite 工厂基本是同样写法只是注入不同的DataSource构建SqlSessionFactory时传入的方言配置不同。客户端业务代码只需要拿到一个DatabaseFactory然后统一调用createUserMapper()它根本不需要知道到底连的是 MySQL 还是 PostgreSQL。这比在业务代码里写一堆switch(dbType)要舒服太多了。4.3 这个实战里抽象工厂的真实收益这个数据库工厂设计让我最直观地感受到了三个好处第一添加新数据库像插U盘一样简单。后续客户说我们还得支持达梦或者人大金仓我只需要新增对应的DataSource配置和对应的XxxDatabaseFactory实现业务层代码零改动这就是开闭原则的典型体现——对扩展开放对修改关闭。第二团队分工更清晰。熟悉 PostgreSQL 的同事负责 PostgreSQL 工厂那部分代码熟悉 MySQL 的同事负责 MySQL 工厂两个人在互不干扰的模块里并行开发几乎不会产生代码冲突。第三测试变得简单。单元测试阶段我可以直接注入一个内存数据库工厂或Mock 工厂替换掉真实的数据库工厂从而非常方便地跑业务逻辑测试完全不依赖外部基础设施。不过这里也得说句公道话如果项目只是单一数据库、没有明确的多平台或多套产品配套需求硬上抽象工厂只会把简单问题复杂化。这个模式真正的价值在于应对变化如果你的产品线未来完全没有横向扩展的可能那就别用它保持简单才是第一位。5. 抽象工厂 vs 工厂方法一张表看清差异前面提到了这两个模式的本质区别这里再放一张对照表方便你复习或者直接抄到笔记里。对比维度工厂方法模式抽象工厂模式生产对象类型单一种类的产品一整套相关多个种类的产品关键词一个工厂 → 一种产品一个工厂 → 一个产品族对象数量一次创建一个对象一次创建一组对象扩展维度扩展新产品类型扩展新平台/新产品族典型例子日志记录器工厂只产Logger跨平台UI工厂按钮弹窗菜单一起产复杂度相对较低相对较高通常配合工厂方法一起使用适用场景产品种类单一但实现多产品种类多且产品之间存在配套关系这里补充一个很实用的理解方式抽象工厂里往往内含多个工厂方法。你看上面UIFactory里的createButton()和createDialog()拆开看每一个都是工厂方法把它们打包到一个接口里、保证成套产出就成了抽象工厂。很多初学者看到各种设计模式图表就头晕其实没必要。把这两个模式的关系理清楚了你就能在面试和实际设计中快速做出取舍你只是要根据参数创建某一种对象的不同实现 → 用工厂方法你需要的是一整套互相搭配的产品而且产品线会整体替换 → 用抽象工厂。6. 实战中的常见问题与排查技巧6.1 扩展系统时类爆炸问题如何缓解这是很多人用抽象工厂后抱怨的第一件事一个平台来三五个产品三个平台就是十几二十个类再加上工厂接口、抽象产品类数量直接翻倍项目结构一下子臃肿起来。我的经验是不要机械地一上来就设计很多抽象产品。正确做法是先摸清需求边界再决定产品维度的粗细。比如我刚才的 UI 案例里如果当前只需要按钮和弹窗就没必要提前把菜单输入框标签页全部都抽象出来。等实际需求真的来了再添加对应方法你会发现新增产品的成本并没有想象中那么高。毕竟抽象工厂的可扩展性是它的优点不是逼你一次性把所有潜在产品都设计出来的理由。从维护角度合理拆包也能减少类爆炸带来的认知负担。每个产品族放进单独的包或者模块比如factory.windows、factory.mac各管各的代码导航起来就很舒服。6.2 产品族内部新增一种产品时所有工厂都要跟着改抽象工厂有一个著名的先天痛点——往抽象工厂接口里增加一个方法会迫使所有具体工厂都实现这个新方法。如果这时候有十个具体工厂你要改十个地方还是挺烦的。我常用的缓解策略有两个默认实现兜底。在抽象工厂里给新增的产品方法写一个返回空实现或者抛『暂不支持』异常的默认实现这样新方法加进去不会影响旧工厂类只有那些真正需要支持新产品的工厂才去覆盖。控制抽象粒度。不要把接口设计得过于庞大保持工厂只负责确实强关联的产品组合。如果一个产品跟其他产品之间没有配套关系就不应该放进同一个抽象工厂里。这个问题的本质是设计权衡你不可能既想要抽象工厂的成套一致性又想要完全零成本的横向扩展。只要提前想清楚怎么兜底这个痛点完全可以接受。6.3 从大 if else重构到抽象工厂时最容易踩的坑经常有朋友看了我这套思路后回手就想把项目里所有的 if else 全部改成抽象工厂。我得拦一下重构有风险操作需谨慎。我见过最惨的一次重构是有人把一段根据设备类型创建不同播放器的 switch 硬改成了抽象工厂结果每个播放器类之间本来就没有多少共性抽象产品接口设计得很牵强最后代码比原来还难读。正确姿势是这样先确认产品族概念真实存在。如果产品之间根本没有成套配套的语义只是不同实现用工厂方法甚至简单工厂就够了。砍掉产品选择逻辑中的潜规则。比如某个场景只有 Windows 才有特殊弹窗Mac 弹窗不需要实现这类不对称需求会让抽象工厂的一致性优势变成束缚。小步迁移别一步到位。先让新工厂和旧逻辑并存用开关配置项灰度切换等验证无问题后再删除旧代码。一把梭的重构大概率会搞出一周都修不完的回归 bug。最后再强调一个我非常看重的点设计模式是命名成熟的套路是为了沟通效率和应对变化不是为了炫技。抽象工厂模式具有很强的封装性和扩展性但也自带一定的复杂度。结合实际项目判断值不值得用比能不能用重要一百倍。7. 我个人的一点实操体会写到现在我回忆了一下自己真正用抽象工厂最爽的一次不是什么高并发系统反而是给公司内部做了一个主题换肤工具。设计师出了三套视觉方案暗黑、浅色、护眼每个主题下都有按钮、卡片、图表配色、字体等一系列套件。我用抽象工厂做了三个具体工厂切换主题只需要换一行配置。那次之后我特别认可一句话抽象工厂模式最闪光的地方不是帮你少写了多少代码而是让整体替换一整套东西变成了一件让人放心的事。如果你正处在学习设计模式的阶段我的建议是别背定义拿着这篇文章里的两个案例自己动手敲一遍再把系统加一个新的平台或者主题亲身体验一下对现有代码的改动范围有多小。真正让你理解抽象工厂的永远是改了一个类整套风格跟着变的那个瞬间。这篇分享里如果有一个知识点让你觉得原来如此我就很高兴了。后面我还会陆续整理其他创建型模式的实际运用心得包括构建者模式、原型模式这些被很多人低估的内容咱们下一篇见。
返回列表