从‘大泥球’到清晰边界:用DDD六边形架构改造你的在线图书馆管理系统
当你面对一个已经运行多年的在线图书馆系统时,是否经常遇到这样的困境:每次新增一个功能都要修改十几处代码,业务逻辑散落在各个角落,测试覆盖率低得可怜,团队成员都不敢轻易改动那些"祖传代码"?这正是典型的"大泥球"架构症状。本文将带你用领域驱动设计(DDD)和六边形架构,像外科手术般精准解耦这个系统。
1. 诊断系统现状:识别架构痛点
在我们开始重构之前,先来看看这个典型的"大泥球"系统有哪些症状:
- 业务逻辑四散:借阅规则可能出现在Controller、Service甚至DAO层
- 高度耦合:修改用户模块可能意外破坏罚款计算功能
- 测试困难:没有清晰的领域边界,单元测试需要启动整个Spring容器
- 技术入侵业务:数据库表结构直接决定了领域模型设计
以一个常见的借阅场景为例,原始代码可能是这样的:
@RestController public class LibraryController { @Autowired private BookRepository bookRepo; @Autowired private UserRepository userRepo; @PostMapping("/borrow") public String borrowBook(@RequestParam Long bookId, @RequestParam Long userId) { Book book = bookRepo.findById(bookId).orElseThrow(); User user = userRepo.findById(userId).orElseThrow(); if(!book.isAvailable()) { return "Book not available"; } if(user.getBorrowedBooks().size() >= 5) { return "Exceed max borrowing limit"; } book.setAvailable(false); bookRepo.save(book); BorrowRecord record = new BorrowRecord(book, user, LocalDate.now()); borrowRecordRepo.save(record); return "Success"; } }这段代码暴露了典型的问题:业务规则(最大借阅量)与持久化逻辑混杂在一起,没有清晰的领域边界。
2. 战略设计:划分限界上下文
DDD的战略设计帮助我们识别系统的核心子域,并划分清晰的限界上下文。对于图书馆系统,我们可以识别出以下几个核心上下文:
| 限界上下文 | 核心职责 | 关键领域概念 |
|---|---|---|
| 借阅管理 | 处理图书借还业务 | 借阅记录、借阅规则 |
| 馆藏管理 | 管理图书元数据和库存 | 图书、副本、分类 |
| 用户管理 | 管理读者账户和权限 | 读者、账户、权限组 |
| 罚款管理 | 计算和处理逾期罚款 | 罚款规则、支付记录 |
每个上下文都应该有明确的边界,并通过定义良好的接口进行通信。例如,借阅上下文需要知道图书是否可借,但不需要了解图书的分类细节。
3. 战术设计:构建纯净领域模型
在确定了限界上下文后,我们需要在每个上下文中应用DDD的战术模式。以借阅上下文为例:
实体:BorrowRecord需要唯一标识来跟踪每笔借阅
public class BorrowRecord { private BorrowRecordId id; private BookId bookId; private UserId userId; private LocalDate borrowDate; private LocalDate dueDate; // 领域行为 public boolean isOverdue() { return LocalDate.now().isAfter(dueDate); } }值对象:BorrowPolicy封装借阅规则
public class BorrowPolicy { private final int maxBorrowDays; private final int maxBorrowCount; public boolean canBorrow(UserBorrowStatus status) { return status.currentBorrowCount() < maxBorrowCount; } public LocalDate calculateDueDate(LocalDate from) { return from.plusDays(maxBorrowDays); } }聚合根:Borrowing作为入口点保护不变条件
public class Borrowing { private List<BorrowRecord> activeRecords; private BorrowPolicy policy; public BorrowResult borrowBook(BookId bookId, UserId userId) { if(!policy.canBorrow(getUserStatus(userId))) { return BorrowResult.rejected("Exceed max borrow limit"); } BorrowRecord record = new BorrowRecord( BorrowRecordId.generate(), bookId, userId, LocalDate.now(), policy.calculateDueDate(LocalDate.now()) ); activeRecords.add(record); return BorrowResult.success(record); } }4. 六边形架构实现
六边形架构(端口与适配器)帮助我们隔离领域核心与外部依赖。以下是关键实现步骤:
4.1 定义领域端口
首先定义领域需要与外界交互的端口(接口):
// 左侧端口:领域服务需要的外部功能 public interface BookCatalog { BookDetail getDetail(BookId id); boolean isAvailable(BookId id); } public interface UserRepository { UserInfo getUserInfo(UserId id); } // 右侧端口:持久化接口 public interface BorrowRecordRepository { void save(BorrowRecord record); List<BorrowRecord> findActiveByUser(UserId userId); }4.2 实现适配器
然后为这些端口提供具体实现适配器:
// 适配外部图书查询服务 @Adapter public class RestBookCatalog implements BookCatalog { private final RestTemplate restTemplate; @Override public BookDetail getDetail(BookId id) { return restTemplate.getForObject( "/books/{id}", BookDetail.class, id.getValue() ); } } // 数据库持久化适配器 @Repository public class JpaBorrowRecordRepository implements BorrowRecordRepository { private final BorrowRecordJpaRepository jpaRepo; @Override public void save(BorrowRecord record) { BorrowRecordEntity entity = toEntity(record); jpaRepo.save(entity); } }4.3 组装应用
使用依赖注入将适配器注入领域核心:
@Configuration public class LibraryConfig { @Bean public BorrowingService borrowingService( BookCatalog bookCatalog, UserRepository userRepository, BorrowRecordRepository recordRepo ) { return new BorrowingService( new Borrowing(new BorrowPolicy(30, 5)), bookCatalog, userRepository, recordRepo ); } }5. 测试策略
六边形架构的一个巨大优势是便于测试。我们可以轻松地为领域核心编写单元测试:
class BorrowingTest { private Borrowing borrowing; private BorrowPolicy policy; @BeforeEach void setUp() { policy = new BorrowPolicy(30, 5); borrowing = new Borrowing(policy); } @Test void should_reject_when_exceed_max_limit() { // 准备测试替身 UserRepository userRepo = mock(UserRepository.class); when(userRepo.getUserInfo(any())) .thenReturn(new UserInfo(5)); // 已借5本 BorrowResult result = borrowing.borrowBook( new BookId("1"), new UserId("1"), userRepo ); assertTrue(result.isRejected()); } }对于适配器,我们可以编写集成测试:
@SpringBootTest class JpaBorrowRecordRepositoryTest { @Autowired private BorrowRecordRepository repository; @Test void should_save_and_retrieve_record() { BorrowRecord record = new BorrowRecord(...); repository.save(record); List<BorrowRecord> found = repository .findActiveByUser(record.getUserId()); assertFalse(found.isEmpty()); } }6. 演进式重构策略
对于已有系统,我们推荐采用渐进式重构:
- 识别热点:从变更最频繁的模块开始
- 提取子域:将相关功能提取到独立模块
- 定义防腐层:在新旧系统间建立转换层
- 逐步替换:功能点逐个迁移到新架构
- 最终切换:当新架构覆盖所有场景后移除旧代码
例如,可以先从借阅功能开始重构:
原始结构: library/ ├── controller/ ├── service/ ├── repository/ └── model/ 重构后结构: library/ ├── borrowing/ │ ├── domain/ │ ├── application/ │ └── infrastructure/ ├── legacy/ └── shared/7. 领域事件解耦
使用领域事件进一步解耦系统组件。当借阅发生时,发布领域事件:
public class Borrowing { private final List<DomainEvent> events = new ArrayList<>(); public BorrowResult borrowBook(...) { // ...借阅逻辑 events.add(new BookBorrowed( record.getId(), record.getBookId(), record.getUserId(), record.getDueDate() )); return result; } public List<DomainEvent> getEvents() { return Collections.unmodifiableList(events); } }其他上下文可以订阅这些事件:
// 罚款上下文处理逾期事件 @Service @RequiredArgsConstructor public class FineEventHandler { private final FineCalculator calculator; @EventListener public void handle(BookBorrowed event) { // 设置定时器在到期日检查 scheduleDueDateCheck(event.dueDate(), event.recordId()); } }这种设计使得系统各部分的耦合降到最低,每个上下文只需要关心自己感兴趣的事件。