Day32-11|13.4.2 工厂模式在数据访问层的应用:隔离创建逻辑与数据源变化

对应教材:《系统架构设计师教程(第2版)》第13章“层次式架构设计理论与实践”13.4“数据访问层设计”中的 13.4.2“工厂模式在数据访问层应用”。本文面向软考高级系统架构设计师,重点学习工厂模式、抽象工厂、DAO创建、数据源切换、连接与事务管理、测试替换和企业应用实践。

一、教材定位与学习目标

数据访问层经常面临对象创建和实现选择问题:系统可能同时支持不同数据库,读写使用不同数据源,测试环境使用内存存储,生产环境使用关系数据库;不同租户可能采用不同分库;同一业务的查询和更新可能需要不同访问实现。若业务代码直接 new 具体 DAO、数据库连接或 ORM 对象,数据源和技术实现就会扩散到整个系统,切换和测试都会变得困难。

工厂模式通过封装对象创建过程,将“使用什么接口”与“创建什么具体实现”分离。在数据访问层中,它可以根据配置、环境、租户、业务场景或版本创建合适的 DAO、仓储、连接、事务和映射器。工厂不是简单的工具类,也不是把所有 if/else 都藏起来;合理的工厂应体现稳定的创建抽象、明确的产品族和可治理的选择策略。

今天的学习目标:

  1. 理解简单工厂、工厂方法和抽象工厂的基本思想。
  2. 掌握工厂模式在 DAO、仓储、连接和数据源选择中的应用。
  3. 区分工厂、依赖注入容器、服务定位器和策略模式。
  4. 设计多数据库、多租户、读写分离和测试环境切换机制。
  5. 能够结合案例题和论文说明工厂模式的价值、风险和治理。

二、为什么数据访问层需要工厂

1. 隔离具体实现

业务服务只需要依赖订单仓储、客户查询服务或报销数据访问接口,不应知道其具体使用 MySQL、PostgreSQL、文档数据库还是远程服务。工厂负责选择并创建实现,业务层通过抽象接口完成操作。

2. 集中创建约束

DAO创建可能需要连接池、事务管理器、映射器、缓存、审计和租户上下文。如果每个业务组件自行创建,初始化规则和资源释放容易不一致。工厂可以集中装配相关依赖,保证创建出的对象满足运行约束。

3. 支持环境和场景变化

测试环境可能使用内存数据库或模拟DAO,生产环境使用真实数据源;读请求使用只读副本,写请求使用主库;不同租户根据隔离策略路由到不同数据库。工厂可以把这些选择隐藏在创建边界内。

4. 控制生命周期

数据访问对象可能是无状态单例、请求作用域或事务作用域。工厂可以统一处理缓存、连接借用、事务绑定和销毁,避免把连接对象错误地保存到长生命周期组件中。

三、工厂模式的基本类型

1. 简单工厂

简单工厂根据类型或配置返回某个接口的具体实现。它可以集中创建逻辑,使用简单,适合实现数量少且变化有限的场景。缺点是工厂内部需要了解所有产品类,新增类型可能需要修改工厂,容易形成大型分支。

2. 工厂方法

工厂方法将创建责任交给子类或具体工厂,每种产品实现对应一个工厂。新增实现可以增加新的工厂,降低对已有工厂的修改。代价是类和配置数量增加,适合产品族或实现变化较多的系统。

3. 抽象工厂

抽象工厂创建一组相互匹配的对象,例如某数据库的数据访问工厂同时创建连接、事务管理器、订单DAO、客户DAO和映射器,保证它们使用同一数据源和兼容版本。它适合存在多个产品族且产品之间需要一致的场景。

4. 参数化工厂

参数化工厂根据租户、环境、读写类型、业务版本或数据区域返回实现。参数必须来自可信上下文并经过白名单校验,不能让客户端直接传入任意类名、连接字符串或 SQL 方言。

类型 创建方式 适用场景 主要风险
简单工厂 一个工厂集中分支 实现少、选择简单 工厂膨胀
工厂方法 子工厂负责具体产品 实现可扩展 类和配置增多
抽象工厂 创建兼容产品族 多数据源、多组件配套 产品族管理复杂
参数化工厂 按上下文选择 多租户、读写、环境 参数篡改和组合爆炸

四、工厂在数据访问层的应用位置

1. DAO 工厂

DAO工厂根据业务能力或访问场景创建 DAO。例如订单写DAO、订单查询DAO和订单审计DAO可以拥有不同实现。业务层只依赖接口,工厂负责装配连接、映射和缓存。

2. 仓储工厂

仓储工厂创建面向聚合的仓储。复杂领域中,仓储需要知道领域映射、事务和聚合加载边界,工厂可以注入这些依赖。仓储不应被设计为任意表访问的通用入口。

3. 数据源工厂

数据源工厂创建主库、只读副本、租户数据库或区域数据库的访问对象。路由依据应由服务端根据身份、业务和配置决定,不能信任客户端自由指定数据源。

4. 连接和事务工厂

连接工厂负责从连接池借用合适连接,事务工厂负责创建或加入事务。连接不应跨线程和长时间保存;事务工厂要处理传播、隔离、超时和回滚规则。

5. 映射器和查询对象工厂

不同领域模型、数据库版本和查询场景可能需要不同映射器。工厂可以选择读模型、领域映射器和批处理映射器,但应让选择规则清晰、可测试和可观测。

五、工厂与依赖注入、服务定位器的比较

1. 工厂与依赖注入

依赖注入容器负责通用对象装配和生命周期管理;工厂负责根据业务条件或运行上下文选择具体产品。工厂可以作为容器中的一个组件,容器再注入工厂到应用服务。两者并不冲突。

2. 工厂与服务定位器

服务定位器由调用方主动查找依赖,容易隐藏依赖关系、难以测试和产生运行时错误。工厂如果被设计成全局服务定位器,同样会产生问题。更好的做法是显式注入抽象工厂,并限制工厂职责。

3. 工厂与策略模式

策略模式关注选择并执行一种算法,例如不同价格计算或数据路由策略;工厂关注创建对象。两者可以组合:工厂根据配置创建路由策略,再由策略决定数据源。不要用工厂替代业务策略,也不要把业务判断全堆到创建方法中。

六、多数据库与读写分离设计

1. 多数据库实现

如果系统需要支持不同数据库,不能只在工厂里切换驱动,还要处理 SQL 方言、分页、事务隔离、索引、类型映射、锁和特性差异。抽象接口应隐藏共同能力,对数据库特有能力通过受控扩展表达。

2. 主库与只读副本

读写分离工厂可以创建写DAO和读DAO。刚写入的数据若立即查询,需要考虑复制延迟;关键流程可以从主库读取或携带版本,普通列表允许短暂最终一致。路由不能仅依据“查询就是读库”,还要考虑数据新鲜度和事务上下文。

3. 多租户路由

租户路由应从可信身份和服务端配置确定,工厂只接受经过验证的租户上下文。连接池、缓存、事务和日志必须保持租户隔离,防止连接或对象复用造成串租户。

4. 数据源故障切换

工厂可以配合健康检查和路由策略切换数据源,但故障切换可能带来数据丢失、重复写入和读旧数据。必须明确哪些操作允许切换,写操作是否需要幂等和对账,切换过程如何审计和回退。

七、工厂设计中的事务、缓存和资源管理

1. 事务边界

DAO工厂不应偷偷开启或提交业务事务,除非它明确属于独立的访问用例。通常由应用服务确定事务,工厂提供参与事务的DAO和连接。跨多个DAO的操作必须使用同一事务上下文,不能每个DAO各自创建连接并提交。

2. 连接生命周期

连接工厂返回的连接应有明确借用和释放机制,优先使用作用域或上下文管理。不要把连接放入单例DAO字段中,也不要在异步任务中复用已关闭连接。连接池参数要按并发、事务时间和数据库能力调优。

3. 缓存对象

工厂可以创建带缓存能力的查询DAO,也可以将缓存作为装饰器注入。缓存键必须包含租户、参数、权限、版本和数据范围;更新后的失效和降级策略要由业务场景决定。不要因为工厂统一创建就给所有数据默认加缓存。

4. 审计和可观测性

工厂应记录选择了哪个实现、数据源区域、读写类型、版本和失败原因,但不能记录密码、连接字符串和敏感数据。业务请求ID、租户和用例应通过上下文传递到DAO和数据库监控,便于定位路由错误和连接泄漏。

八、案例分析:集团订单系统的多数据源访问工厂

某集团订单系统有总部订单库和多个区域订单库。新系统希望支持区域隔离,普通列表从只读副本查询,创建和支付必须写主库;测试环境需要使用模拟数据访问。原系统在控制器中直接创建数据库连接和 DAO,区域路由写在多个方法中。

1. 目标架构

定义订单仓储、订单查询、支付记录和审计访问接口;建立订单数据访问抽象工厂,负责创建同一租户和区域下的写DAO、读DAO、事务管理器、映射器和审计组件。具体产品族包括生产关系数据库工厂、测试模拟工厂和报表只读工厂。

2. 路由规则

应用服务从认证上下文获取租户和区域,读取服务端路由配置后选择工厂。创建订单和支付使用写产品族;普通列表可使用只读产品族;支付后立即查询应在事务上下文或带有足够版本的主库读取。客户端不能传入任意区域或数据库连接信息。

3. 事务和幂等

订单创建在应用服务事务中使用写DAO,支付调用使用支付编号作为幂等键。读DAO不参与写事务,复制延迟造成的状态滞后通过状态查询、版本和提示处理。故障切换前先判断写操作是否已提交,避免重复创建和重复支付。

4. 测试和迁移

单元测试注入模拟工厂,验证业务服务与具体数据库无关;契约测试验证不同产品族接口行为一致;集成测试验证主库、只读副本和区域路由;迁移中同时运行旧路由和新工厂,通过日志和对账比较结果,逐步切换。

5. 指标

通过新增数据源接入周期、控制器直连数量、区域路由错误率、只读副本命中率、事务连接泄漏数、支付重复数、测试替换成本和故障切换恢复时间评价效果。

九、综合知识练习与详细答案

练习一:单项选择

工厂模式在数据访问层的主要作用是( )。

A. 让业务代码依赖具体数据库类 B. 封装数据访问对象的创建和实现选择 C. 自动保证所有跨系统事务强一致 D. 将所有业务规则集中到工厂

答案:B。工厂隔离对象创建和实现选择,不能自动解决分布式一致性,也不应承载全部业务规则。

练习二:多项选择

抽象工厂适合用于( )。

A. 创建相互匹配的DAO和映射器 B. 支持多数据库产品族 C. 支持生产和测试实现切换 D. 让客户端任意指定连接字符串 E. 保证同一事务上下文中的数据访问组件一致

答案:A、B、C、E。客户端任意指定连接字符串会带来安全和治理风险。

练习三:案例题

某系统用一个全局工厂,根据请求参数中的 className 反射创建任意 DAO,并允许调用方传入数据库地址。请分析问题并提出改进方案。

参考答案:任意类名反射可能导致未授权代码创建、依赖注入和执行风险;客户端传入数据库地址会造成越权访问、数据泄露和资源失控;工厂职责不受控,无法保证DAO、事务、映射器和数据源的兼容。改进时应采用接口和白名单注册具体产品,禁止任意反射和任意连接信息;数据源由服务端配置、租户上下文和安全策略决定;使用抽象工厂创建兼容的DAO、映射器、连接和事务产品族;将业务路由规则交给受控策略或路由服务;对创建、路由、租户、版本和失败进行审计;通过依赖注入、契约测试和安全测试验证;生产配置支持版本、审批、灰度和回滚。

十、案例题答题模板

遇到工厂、DAO创建、多数据源和数据库切换类题目,可以按以下顺序作答:

  1. 说明业务代码直接创建具体DAO或连接造成的耦合和重复。
  2. 定义访问抽象和产品族,明确工厂负责创建,业务层负责使用。
  3. 根据环境、租户、读写、区域或版本选择简单工厂、工厂方法或抽象工厂。
  4. 设计依赖注入、生命周期、连接、事务、缓存和异常处理。
  5. 限制路由参数,采用白名单、服务端配置、租户隔离和审计。
  6. 处理读写延迟、故障切换、幂等、对账和回退。
  7. 用接入周期、路由错误、资源泄漏、性能和测试成本验证效果。

十一、论文写作素材

开头素材

“原有订单系统在多个控制器中直接创建数据库连接和数据访问对象,区域路由、读写分离和测试实现分散在业务代码中,导致技术切换困难、事务资源管理不一致。为隔离数据访问实现变化,我参与设计了基于工厂模式的数据访问层,通过抽象工厂统一创建不同区域、读写类型和运行环境下的DAO、映射器、连接和事务组件。”

正文素材

可以重点写四点:第一,如何根据多数据源和产品族选择抽象工厂,保证DAO、映射器和事务实现相互兼容;第二,如何结合依赖注入和工厂隔离业务层与具体存储;第三,如何处理租户路由、主从读写、复制延迟、故障切换、幂等和对账;第四,如何治理工厂的生命周期、安全白名单、配置版本、审计、测试和性能。要写出工厂与服务定位器、策略的区别,避免把工厂变成全局万能入口。

结尾素材

“实践表明,工厂模式的价值不是隐藏所有 new 操作,而是把数据访问对象的创建约束、实现选择和资源生命周期集中到可治理的边界内。通过显式抽象、受控路由、兼容产品族和完善测试,系统可以在不修改业务逻辑的情况下适应数据库、租户和运行环境变化。”

十二、今日复盘

本节核心结论是:工厂封装数据访问对象的创建和实现选择,抽象工厂保证一组访问组件相互匹配;路由必须受控,事务和连接生命周期必须明确,工厂不能演变为万能服务定位器。

请回答:

  1. 简单工厂、工厂方法和抽象工厂有什么区别?
  2. 工厂与依赖注入容器、服务定位器和策略模式有什么关系?
  3. 多租户和读写分离场景下,工厂的选择依据是什么?
  4. 为什么客户端不能自由指定 className 和数据库连接地址?
  5. 工厂如何处理连接、事务、缓存、幂等和故障切换?
  6. 如何通过测试和指标证明工厂模式降低了数据访问耦合?

请以订单、合同或报销系统为例,画出“应用服务—抽象工厂—DAO/仓储产品族—连接/事务/映射器—数据源”的关系,并标注租户路由、读写分离、权限、幂等、审计和回滚位置。完成后,你就掌握了 13.4.2 的核心内容。