尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

DDD分层架构实战:依赖倒置、防腐层与领域事件解耦指南

DDD分层架构实战:依赖倒置、防腐层与领域事件解耦指南 1. 从“面条式代码”到分层一个架构师的觉醒我见过太多项目初期为了赶进度代码写得那叫一个“随心所欲”。业务逻辑、数据库访问、页面渲染所有东西都搅和在一起像一碗意大利面你扯出一根能带出整个系统。这种代码刚开始跑起来可能挺快但三个月后当产品经理提出第一个稍微复杂点的需求变更时整个团队就开始陷入泥潭。改一个地方莫名其妙地崩了三个看似不相关的功能加一个字段需要从数据库一直改到前端页面牵一发而动全身。这就是典型的层与层之间强耦合带来的恶果。后来接触了DDD领域驱动设计其倡导的分层架构像一剂良药让我看到了清晰解耦的希望。但说实话刚开始实践时我也踩了不少坑。最常见的就是虽然我们把代码分成了“用户接口层”、“应用层”、“领域层”、“基础设施层”这几个包目录但层与层之间的调用关系依然混乱不堪。领域对象里直接调用了数据库接口应用服务里充斥着大量的数据转换和业务判断这不过是把“大泥球”切成了几个“小泥球”依赖关系依然是一团乱麻分层形同虚设。所以今天我想聊的不是DDD分层架构那几个层叫什么名字——这个随便一搜就有。我想深入聊聊的是如何真正地、有效地降低层与层之间的依赖。这不仅仅是画个架构图而是要通过一系列具体的设计原则、编码规范和依赖管理手段让每一层都保持纯洁让依赖关系变得清晰、稳定且可测试。这是我们构建一个能够应对业务快速变化、便于团队协作的复杂系统的基石。2. 依赖倒置打破层间耦合的“铁律”要降低依赖首先要理解依赖的方向。在传统的分层架构里我们很容易陷入一种“自上而下”的天然依赖用户接口层依赖应用层应用层依赖领域层领域层依赖基础设施层比如数据库、消息队列。这种依赖关系是单向的、稳定的但问题在于高层模块如领域层直接依赖了低层模块如基础设施层的具体实现。想象一下你的Order领域对象里直接调用了一个OrderRepositoryImpl的save方法。这意味着你的核心业务逻辑订单规则和具体的数据库技术MySQL还是MongoDB绑死了。一旦你想换数据库或者想为订单增加一个缓存机制你就不得不去修改Order这个本该最稳定的核心领域对象。这违反了“开闭原则”。DDD分层架构的精髓之一就是引入依赖倒置原则来破解这个困局。这个原则简单说就是高层模块不应该依赖低层模块二者都应该依赖其抽象。在分层架构的语境下具体表现为领域层和基础设施层都依赖一个抽象的接口而这个接口定义在领域层。让我们看一个具体的代码对比。这是错误的强依赖示例// 领域层 - Order 领域实体 public class Order { private OrderId id; private Money totalAmount; // ... 其他属性 // 问题领域实体直接依赖了具体的基础设施实现 private OrderRepositoryImpl repository new OrderRepositoryImpl(); public void save() { // 业务逻辑... repository.save(this); // 直接调用具体实现 } }下面是遵循依赖倒置原则的正确做法// 1. 首先在领域层定义一个仓储接口抽象 // 位于 domain.repository 包 public interface OrderRepository { Order findById(OrderId id); void save(Order order); // ... 其他领域相关的查询方法 } // 2. 领域实体或领域服务只依赖这个接口 public class Order { private OrderId id; private Money totalAmount; // 业务方法接收抽象接口作为参数 public void confirm(OrderRepository repository) { if (this.canBeConfirmed()) { this.status OrderStatus.CONFIRMED; repository.save(this); // 通过接口调用 this.domainEvents.add(new OrderConfirmedEvent(this.id)); } } } // 3. 在基础设施层实现这个领域接口 // 位于 infrastructure.persistence.jpa 包 Repository public class JpaOrderRepository implements OrderRepository { PersistenceContext private EntityManager entityManager; Override public Order findById(OrderId id) { // 使用JPA具体实现 return entityManager.find(Order.class, id.getValue()); } Override public void save(Order order) { // 使用JPA具体实现 entityManager.persist(order); } }通过这种方式依赖关系发生了根本性逆转。领域层Order定义了自己需要什么OrderRepository接口而基础设施层JpaOrderRepository则去适配和满足这个需求。领域层完全不知道也不关心数据是用JPA、MyBatis还是直接JDBC存的它只关心“保存订单”这个领域能力。当未来需要将订单数据同步到 Elasticsearch 做搜索时你只需要再实现一个EsOrderRepository领域代码一行都不用改。实操心得在项目初期团队最容易犯的错误就是把Repository的实现类如JpaOrderRepository放在domain包下。务必在物理包结构上就进行隔离比如com.xxx.domain.repository接口和com.xxx.infrastructure.persistence实现。这能从物理层面提醒开发者依赖的方向。3. 防腐层与适配器抵御外部依赖的“入侵”依赖倒置解决了领域与基础设施之间的耦合但系统不可能孤立存在。我们总要调用外部的REST API、消息中间件、文件存储服务或者遗留系统。这些外部系统有着自己的数据模型、变更节奏和故障模式如果让它们的数据结构和逻辑直接渗透到我们的应用层甚至领域层那将是一场灾难。比如你的支付模块需要调用一个外部支付网关。如果直接在应用服务里使用支付网关的SDK并让其返回的ExternalPaymentResponse对象在整个系统中流转那么一旦支付网关升级API修改了响应格式你的核心业务代码将不得不跟着大面积修改。这时就需要引入防腐层的概念。防腐层本质上是一个隔离层它将外部系统的不稳定因素和复杂模型转换为我们内部系统定义的、稳定的领域模型。通常我们使用适配器模式来实现防腐层。让我们来看一个为外部支付服务构建防腐层的例子// 1. 首先在领域层定义我们自己的支付领域模型和端口接口 // 位于 domain.service 包 public interface PaymentService { PaymentResult pay(Order order, PaymentCommand command); } // 领域值对象 - 支付结果 public class PaymentResult { private boolean success; private String transactionId; private String failureReason; // ... 领域逻辑如判断是否成功等 } // 2. 在应用层或一个独立的“适配器”模块实现防腐层 // 位于 application.external 或 infrastructure.client 包 Component public class ExternalPaymentServiceAdapter implements PaymentService { Autowired private ExternalPaymentGatewayClient gatewayClient; // 外部SDK的客户端 Override public PaymentResult pay(Order order, PaymentCommand command) { // 将内部领域模型转换为外部API所需的DTO ExternalPaymentRequest externalRequest convertToExternalRequest(order, command); try { // 调用不稳定的外部服务 ExternalPaymentResponse externalResponse gatewayClient.createPayment(externalRequest); // **关键步骤**将外部响应转换回内部领域模型 // 在这里处理外部系统的各种“怪异”情况 return convertToDomainResult(externalResponse); } catch (ExternalServiceTimeoutException e) { // 外部系统超时转换为领域可理解的失败结果 return PaymentResult.failed(支付网关响应超时); } catch (ExternalServiceException e) { // 处理其他外部异常进行降级或转换 log.error(调用支付网关失败, e); return PaymentResult.failed(支付网关暂时不可用); } } private ExternalPaymentRequest convertToExternalRequest(Order order, PaymentCommand cmd) { // 转换逻辑隔离外部模型变化 ExternalPaymentRequest req new ExternalPaymentRequest(); req.setOrderNo(order.getOrderNumber().toString()); req.setAmount(order.getTotalAmount().getValue()); req.setCurrency(order.getTotalAmount().getCurrency()); // ... 其他映射 return req; } private PaymentResult convertToDomainResult(ExternalPaymentResponse resp) { // **防腐核心**外部系统的“success”字段可能是字符串“Y”而我们是布尔值 // 外部系统的错误码需要翻译成我们领域的错误原因 boolean success Y.equals(resp.getStatus()); String reason success ? null : translateErrorCode(resp.getErrorCode()); return new PaymentResult(success, resp.getGatewayTransactionId(), reason); } private String translateErrorCode(String externalCode) { // 一个简单的映射表将外部错误码转换为业务语言 MapString, String errorMap Map.of( INSUFFICIENT_BALANCE, 余额不足, CARD_DECLINED, 卡片被拒绝, NETWORK_ERROR, 网络错误请重试 ); return errorMap.getOrDefault(externalCode, 支付失败请联系客服); } }通过这个ExternalPaymentServiceAdapter我们做到了以下几点隔离变化外部支付网关的API变更、SDK升级只需要修改这个适配器类领域核心逻辑PaymentService的接口和使用方完全不受影响。统一异常处理将外部系统的各种技术异常超时、网络错误、解析失败转换为我们领域内统一的、有业务含义的失败结果。模型转换将外部“丑陋”的数据模型如下划线字段、奇怪的枚举值转换为我们整洁的领域模型如值对象PaymentResult。语义翻译将外部系统的技术状态码如“99”翻译成业务语言如“余额不足”使得上层业务逻辑可以基于业务语义做判断而不是技术细节。踩坑记录我曾在一个项目中没有为短信服务构建防腐层。后来短信服务商从A换到B两个服务商的发送状态回执格式完全不同。结果我们不得不在三个不同的应用服务注册、登录、支付中修改状态解析逻辑散落各处的if-else让人抓狂。重构时引入一个SmsService适配器后再次更换服务商只需改一个地方清爽至极。4. 领域事件以事件驱动替代过程调用实现层间解耦很多时候层与层之间、模块与模块之间的依赖源于一个模块需要主动调用另一个模块的方法来完成一个连贯的业务流程。比如订单确认后需要更新库存、发送短信通知、给用户增加积分。如果在OrderService的confirmOrder方法里依次调用InventoryService.reduceStock()、SmsService.sendNotification()、UserService.addPoints()那么OrderService就对这三个服务产生了直接的、编译期的强依赖。这会导致事务复杂多个外部调用混在同一个事务里容易导致长事务和分布式事务问题。性能瓶颈同步调用必须等所有步骤完成才能返回响应时间变长。耦合度高任何下游服务的接口变更或故障都会直接影响上游订单确认的主流程。DDD提倡使用领域事件来解耦这种流程性的依赖。核心思想是一个聚合根完成一个重要的状态变更后它并不关心后续谁会来响应这个变更它只需要发布一个事件说“我发生了某件事”。其他感兴趣的组件可以是同一层的其他聚合也可以是应用层的处理器可以订阅这个事件并异步地执行自己的逻辑。让我们用领域事件重构上面的订单确认流程// 1. 在领域层定义领域事件 // 位于 domain.event 包 public class OrderConfirmedEvent extends DomainEvent { private final OrderId orderId; private final CustomerId customerId; private final Money paidAmount; // ... 事件发生时间等元数据 public OrderConfirmedEvent(OrderId orderId, CustomerId customerId, Money paidAmount) { this.orderId orderId; this.customerId customerId; this.paidAmount paidAmount; } // getters... } // 2. 在Order聚合根中在确认操作后发布事件 public class Order { // ... 其他属性和方法 private ListDomainEvent domainEvents new ArrayList(); public void confirm() { // 核心业务逻辑校验 if (!this.canBeConfirmed()) { throw new IllegalOrderStateException(订单无法确认); } this.status OrderStatus.CONFIRMED; this.confirmedAt LocalDateTime.now(); // **发布领域事件而不是调用具体服务** this.domainEvents.add(new OrderConfirmedEvent(this.id, this.customerId, this.totalAmount)); } // 提供一个方法让上层通常是Repository获取并清空事件列表 public ListDomainEvent getDomainEvents() { return new ArrayList(domainEvents); } public void clearDomainEvents() { domainEvents.clear(); } } // 3. 在基础设施层如Repository实现中发布事件到消息总线 Repository public class JpaOrderRepository implements OrderRepository { PersistenceContext private EntityManager entityManager; Autowired private DomainEventPublisher eventPublisher; // 事件发布器接口 Override Transactional public void save(Order order) { entityManager.persist(order); // 或merge // 保存后发布该订单产生的所有领域事件 order.getDomainEvents().forEach(eventPublisher::publish); order.clearDomainEvents(); } } // 4. 在应用层或单独的“事件处理”模块编写事件处理器 // 位于 application.eventhandler 包 Component public class OrderConfirmedEventHandler { Autowired private InventoryService inventoryService; Autowired private NotificationService notificationService; Autowired private LoyaltyService loyaltyService; // 订阅OrderConfirmedEvent事件 EventListener Async // 可以异步执行不阻塞主流程 Transactional(propagation Propagation.REQUIRES_NEW) // 使用新事务 public void handle(OrderConfirmedEvent event) { // 处理库存扣减 inventoryService.reduceStock(event.getOrderId()); // 发送通知邮件/短信 notificationService.sendOrderConfirmedMsg(event.getCustomerId(), event.getOrderId()); // 增加用户积分 loyaltyService.addPoints(event.getCustomerId(), event.getPaidAmount()); } }通过引入领域事件Order聚合根和OrderService应用服务变得非常“干净”和“专注”。它们只负责完成订单确认的核心业务规则和状态变更至于后续要做什么它们完全不知道也不关心。库存、通知、积分这些逻辑被移到了独立的事件处理器中。这种模式带来了巨大的好处彻底解耦订单模块不再依赖库存、通知、积分模块。它们之间唯一的联系就是事件对象这个“契约”。只要事件结构不变各个模块可以独立演化、独立部署、独立伸缩。提升性能与可靠性主流程订单确认可以快速同步返回。后续处理可以异步执行即使某个处理器如积分服务暂时挂掉也可以通过事件的重试机制保证最终一致性不会阻塞主订单流程。增强可扩展性未来如果需要增加一个新动作比如订单确认后触发一个数据分析只需要新增一个事件处理器并订阅OrderConfirmedEvent即可完全不用修改现有的订单相关代码。注意事项领域事件虽然强大但引入了最终一致性的复杂度。需要仔细设计事件的结构包含足够的信息供处理器使用并处理好事件丢失、重复消费、顺序消费等问题。通常需要结合可靠的消息中间件如Kafka, RabbitMQ和幂等性设计来保证系统的健壮性。5. 依赖管理与构建工具从代码约束到物理隔离前面我们讨论的都是代码设计层面的解耦。但在实际项目中仅仅依靠设计和约定是不够的尤其是当团队规模扩大、新人加入时不经意的错误依赖导入很快就会让清晰的架构腐化。因此我们需要借助依赖管理工具和模块化手段在物理层面强制隔离将架构约束固化下来。以最常用的Java项目构建工具Maven为例我们可以通过精心设计的pom.xml依赖关系来确保分层原则不被破坏。一个理想的DDD多模块项目结构可能如下所示my-ddd-project ├── pom.xml (父POM管理公共依赖和插件) ├── domain-core (领域核心模块) │ ├── pom.xml │ └── src/... (包含领域模型、领域服务接口、仓储接口、领域事件定义等) ├── application-service (应用服务模块) │ ├── pom.xml │ └── src/... (包含应用服务实现、DTO、事件处理器等) ├── infrastructure-persistence (基础设施-持久化模块) │ ├── pom.xml │ └── src/... (包含JPA实体、Repository实现、数据库迁移脚本等) ├── infrastructure-external (基础设施-外部服务模块) │ ├── pom.xml │ └── src/... (包含外部API客户端、防腐层适配器等) ├── interfaces-web (用户接口层-Web模块) │ ├── pom.xml │ └── src/... (包含Controller、Web配置、DTO转换器等) └── interfaces-job (用户接口层-定时任务模块) ├── pom.xml └── src/... (包含定时任务入口)关键在于各模块pom.xml中的依赖声明。依赖必须单向流动严禁循环依赖。domain-core模块的pom.xml这是最纯净的模块。它不应该依赖任何其他模块也不应该依赖任何具体的技术框架如Spring, JPA, MyBatis。它只包含JDK和必要的工具类如Apache Commons Lang, Guava依赖。它是整个系统的核心稳定性最高。dependencies !-- 纯JDK和通用工具无框架依赖 -- dependency groupIdorg.apache.commons/groupId artifactIdcommons-lang3/artifactId /dependency /dependenciesapplication-service模块的pom.xml它依赖domain-core因为它需要调用领域模型和领域服务接口。它可以引入一些应用层框架如Spring的Transactional,Async注解支持但依然不应该依赖具体的数据访问或Web框架。dependencies !-- 依赖领域核心 -- dependency groupIdcom.example/groupId artifactIddomain-core/artifactId version${project.version}/version /dependency !-- 应用层框架支持 -- dependency groupIdorg.springframework/groupId artifactIdspring-tx/artifactId /dependency dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId /dependency /dependenciesinfrastructure-persistence模块的pom.xml它依赖domain-core因为它要实现domain-core中定义的Repository接口。它会引入具体的持久化框架如Spring Data JPA。dependencies !-- 依赖领域核心以实现其接口 -- dependency groupIdcom.example/groupId artifactIddomain-core/artifactId version${project.version}/version /dependency !-- 具体的技术栈依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependenciesinterfaces-web模块的pom.xml它依赖application-service调用应用服务和infrastructure-*模块Spring运行时需要扫描到具体的Bean实现。它会引入Spring MVC等Web框架。dependencies !-- 依赖应用服务层 -- dependency groupIdcom.example/groupId artifactIdapplication-service/artifactId version${project.version}/version /dependency !-- 依赖基础设施因为需要注入Repository等具体实现 -- dependency groupIdcom.example/groupId artifactIdinfrastructure-persistence/artifactId version${project.version}/version /dependency dependency groupIdcom.example/groupId artifactIdinfrastructure-external/artifactId version${project.version}/version /dependency !-- Web框架 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies通过这样的模块划分和依赖管理我们在编译期就杜绝了错误的依赖。如果domain-core里的代码不小心导入了spring-boot-starter-data-jpaMaven构建会直接失败。这比任何文档和代码评审都更有效。构建工具实战技巧对于使用Gradle的项目同样可以利用api和implementation依赖配置来严格控制暴露范围。将domain-core的依赖全部声明为api因为接口需要被实现而将具体基础设施模块的内部工具依赖声明为implementation可以避免依赖泄露。同时定期使用gradle dependencies或mvn dependency:tree命令分析依赖树检查是否有违反架构分层的“偷偷潜入”的依赖。6. 测试策略依赖隔离是高质量测试的基石清晰的层间依赖和接口隔离不仅让生产代码更健壮也让测试变得异常简单和高效。每一层都可以在隔离的环境中用最适合的方式被测试。6.1 领域层测试纯单元测试速度极快领域层是业务核心应该用最纯粹的单元测试来验证其正确性。由于它不依赖任何外部框架数据库、网络、Spring容器测试可以跑得飞快。// 测试Order领域实体的confirm方法 class OrderTest { Test void should_confirm_order_when_all_conditions_met() { // 准备 Order order new Order(...); order.place(); // 假设订单已下单 // 执行 order.confirm(); // 断言 assertThat(order.getStatus()).isEqualTo(OrderStatus.CONFIRMED); assertThat(order.getConfirmedAt()).isNotNull(); // 断言领域事件被发布 assertThat(order.getDomainEvents()) .hasSize(1) .first() .isInstanceOf(OrderConfirmedEvent.class); } Test void should_throw_exception_when_confirming_a_cancelled_order() { Order order new Order(...); order.cancel(); // 先取消订单 // 执行并断言异常 assertThatThrownBy(order::confirm) .isInstanceOf(IllegalOrderStateException.class) .hasMessageContaining(无法确认); } }这些测试不启动Spring不连接数据库运行速度在毫秒级可以在开发人员保存代码后立即执行提供快速反馈。6.2 应用层测试集成测试与Mock应用服务协调领域对象和基础设施测试时需要模拟外部依赖。我们可以使用Mock框架如Mockito来隔离测试。ExtendWith(MockitoExtension.class) // 使用JUnit 5 Mockito class OrderApplicationServiceTest { Mock private OrderRepository orderRepository; Mock private PaymentService paymentService; // 防腐层接口 InjectMocks private OrderApplicationService orderService; // 被测试的应用服务 Test void should_confirm_order_successfully() { // 准备Mock数据和行为 OrderId orderId new OrderId(order-123); Order mockOrder mock(Order.class); when(orderRepository.findById(orderId)).thenReturn(Optional.of(mockOrder)); when(paymentService.pay(any(), any())).thenReturn(PaymentResult.success(tx-456)); ConfirmOrderCommand command new ConfirmOrderCommand(orderId, ...); // 执行 orderService.confirmOrder(command); // 验证交互应用服务是否以正确的参数调用了领域对象和基础设施 verify(mockOrder).confirm(any(PaymentService.class)); // 验证调用了领域方法 verify(orderRepository).save(mockOrder); // 验证保存了订单 // 注意我们并不验证Order内部状态那是领域层单元测试的责任 } }6.3 基础设施层与用户接口层测试组件测试与端到端测试对于基础设施层如JpaOrderRepository我们需要启动一个真实的数据库可以是内存数据库H2进行测试验证ORM映射和SQL是否正确。对于用户接口层如Controller可以使用Spring的WebMvcTest切片测试只启动Web层相关的组件Mock掉后面的应用服务。// 基础设施层 Repository 测试 DataJpaTest // Spring Boot测试切片只初始化JPA相关组件 AutoConfigureTestDatabase(replace AutoConfigureTestDatabase.Replace.NONE) class JpaOrderRepositoryTest { Autowired private TestEntityManager entityManager; Autowired private JpaOrderRepository repository; Test void should_save_and_find_order() { // 使用内存数据库H2进行真实持久化操作测试 Order order new Order(...); entityManager.persist(order); Order found repository.findById(order.getId()); assertThat(found).isNotNull(); assertThat(found.getOrderNumber()).isEqualTo(order.getOrderNumber()); } } // 用户接口层 Controller 测试 WebMvcTest(OrderController.class) // 只启动Web MVC层 class OrderControllerTest { Autowired private MockMvc mockMvc; MockBean private OrderApplicationService orderService; // Mock掉应用服务 Test void should_return_ok_when_confirming_order() throws Exception { ConfirmOrderRequest request new ConfirmOrderRequest(...); when(orderService.confirmOrder(any())).thenReturn(ConfirmOrderResult.success()); mockMvc.perform(post(/api/orders/confirm) .contentType(MediaType.APPLICATION_JSON) .content(asJsonString(request))) .andExpect(status().isOk()) .andExpect(jsonPath($.success).value(true)); } }清晰的依赖关系使得我们可以为每一层选择最合适、最快速的测试策略。领域层用快速的单元测试保证核心逻辑应用层用Mock测试验证协调逻辑基础设施和接口层用集成测试验证技术集成点。这种测试金字塔结构是构建高可靠性系统的关键。7. 常见陷阱与最佳实践让分层真正落地即使理解了所有原则在实际项目中维护一个清晰的分层架构依然充满挑战。下面是一些我总结的常见陷阱和应对的最佳实践。7.1 陷阱一贫血模型与“DTO大爆炸”这是最普遍的陷阱。领域对象Entity只剩下一堆getter/setter所有业务逻辑都散落在应用服务Application Service中变成了“事务脚本”模式。同时为了在各层之间传递数据创造了大量的、几乎一模一样的XXXRequest,XXXResponse,XXXDTO,XXXVO造成映射代码泛滥和认知负担。最佳实践富领域模型坚决将属于该聚合的业务逻辑如订单的confirm,cancel,calculateTotal放到领域实体或值对象内部。应用服务只负责协调、事务和防腐层调用。谨慎使用DTO并非所有层间传递都需要DTO。Controller - Application Service可以使用Command或Query对象它们本身就是应用层API的一部分。Application Service - Controller对于简单返回可以直接返回领域对象需注意序列化问题。对于复杂聚合可以定义一个Response对象但应避免为每个接口都创建一对DTO。考虑使用Projection投影技术按需从领域模型中选择字段返回。领域层内部严禁使用DTO应直接传递领域对象。使用MapStruct等映射工具如果确实需要映射使用专业的映射工具避免手写冗长且易错的setter代码。7.2 陷阱二基础设施细节泄露到领域层在领域实体中使用了JPA的Entity,Table注解或者在字段上使用了Column(name “xxx”)。这虽然方便但让领域模型沾染了持久化框架的细节。最佳实践领域模型保持纯净领域层定义的Order类应该是纯Java对象POJO只有业务属性和方法。持久化实体作为“数据模型”在基础设施层如infrastructure.persistence.jpa包定义另一个OrderJpaEntity类它使用JPA注解。在JpaOrderRepository的实现中负责将领域Order对象与OrderJpaEntity对象进行转换。这增加了些许转换代码但换来了领域层的绝对纯净和持久化技术的可替换性。7.3 陷阱三过度分层与“流水账”式应用服务有时为了分层而分层把简单的CRUD操作也套上DDD的帽子导致每个操作都要经过Controller - Application Service - Domain Service - Repository的漫长链条应用服务里全是repository.save(entity)这样的流水账代码。最佳实践识别核心子域与通用子域DDD适用于业务逻辑复杂的核心子域。对于简单的通用子域如数据字典管理、文件上传或支撑子域完全可以使用简单的CRUD架构不必强行套用DDD的所有规则。在同一个项目中可以并存多种架构风格。应用服务应体现“用例”应用服务的方法应对应一个完整的用户用例User Case如confirmOrder,shipOrder而不是简单的getOrder,updateOrder。它的主要职责是获取聚合、调用其业务方法、持久化、发布事件。如果只是简单的数据存取直接调用Repository也未尝不可但要控制其范围。7.4 陷阱四循环依赖与上帝服务由于不合理的职责划分导致OrderService依赖UserServiceUserService又反过来依赖OrderService形成编译期循环依赖。或者出现一个庞大的CommonService或Manager什么都往里塞成了上帝类。最佳实践依赖注入解决编译期循环如果确实是双向业务依赖考虑引入第三个组件如一个事件处理器来解耦或者重新审视聚合边界是否划分正确。使用领域事件解耦运行时依赖如第4节所述用事件代替直接的方法调用是解耦服务间依赖的利器。遵循单一职责原则如果一个服务过于庞大就按职责将其拆分为多个更小、更专注的服务。例如将OrderService拆分为OrderCreationService,OrderPaymentService,OrderFulfillmentService等。分层架构不是银弹而是一种需要持续维护和权衡的设计纪律。它初期会带来一定的复杂度更多的类、接口、模块但换来的长期收益是系统在面对变化时的韧性、团队并行开发的效率以及代码的可测试性。关键在于团队要对这些原则有共同的理解并通过代码规范、依赖管理工具和定期的架构评审来守护它。当你在修改一个功能时发现影响范围被清晰地限制在某一层或某一个模块内那种“一切尽在掌握”的感觉就是对坚持清晰分层最好的回报。
返回列表