
領域驅動設計(Domain-Driven Design, DDD)已經成為現代軟體開發的重要方法論,但在實際專案中,我們經常看到團隊雖然採用了 DDD 的架構,卻沒有真正遵循其核心原則。本文將提供一份完整的 DDD Code Review 檢查清單,幫助你的團隊寫出真正符合 DDD 精神的程式碼。
為什麼需要 DDD Code Review 檢查清單?
在我多年的軟體開發經驗中,我發現最常見的問題不是團隊不了解 DDD 理論,而是在實作時容易偏離正軌。一個結構看起來像 DDD 的專案,可能實際上只是把傳統的 MVC 架構重新包裝,業務邏輯仍然散落在各個地方,Domain 層變成了貧血模型的集合。
這就是為什麼我們需要一個系統性的檢查清單,來確保每一行程式碼都符合 DDD 的設計原則。專案結構:DDD 的基礎骨架
四層架構檢查
首先,我們需要確保專案具有完整的四層結構:
project/
├── interfaces/ # 介面層(Controllers, DTOs)
├── application/ # 應用層(Application Services, Commands)
├── domain/ # 領域層(Entities, Value Objects, Domain Services)
└── infrastructure/ # 基礎設施層(Repositories, External Services)
依賴方向檢查重點:
- interfaces → application → domain ← infrastructure
- Domain 層不能依賴任何其他層(依賴反轉原則)
- 絕對不允許跨層直接依賴
我曾經參與過一個專案,看起來有 DDD 的目錄結構,但是 Controller 直接注入 Repository,完全違反了 DDD 的分層原則。這種違規會導致業務邏輯散落,難以維護。
Domain 層結構完整性
Domain 層是 DDD 的核心,必須包含完整的領域概念:
domain/
├── entity/ # 領域實體
├── aggregate/ # 聚合根
├── valueobject/ # 值物件
├── repository/ # 倉儲介面(僅介面,不含實作)
├── service/ # 領域服務
├── event/ # 領域事件
└── exception/ # 領域異常
Domain 層:DDD 的靈魂所在
告別貧血模型:富含業務邏輯的實體
最常見的 DDD 反模式就是貧血模型。讓我們看看正確和錯誤的實作:
✅ 正確示例:富有業務邏輯的實體
public class InsurancePolicy {
private PolicyId id;
private PolicyStatus status;
private Money premium;
// ✅ 包含業務邏輯的方法
public ClaimResult processClaim(ClaimRequest request) {
this.validateCanClaim();
Money amount = this.calculateClaimAmount(request);
this.recordClaimHistory(request);
return ClaimResult.of(amount);
}
// ✅ 業務規則封裝在實體內部
private void validateCanClaim() {
if (!this.status.canProcessClaim()) {
throw new PolicyCannotClaimException("保單狀態不允許申請理賠");
}
}
// ✅ 使用領域語言的方法名
public void activate() {
if (this.status.isExpired()) {
throw new CannotActivateExpiredPolicyException();
}
this.status = PolicyStatus.ACTIVE;
}
}
❌ 錯誤示例:貧血模型
// ❌ 只有資料,沒有行為
public class InsurancePolicy {
private String id;
private String status;
private BigDecimal premium;
// 只有 getter/setter,業務邏輯在別的地方
public String getId() { return id; }
public void setStatus(String status) { this.status = status; }
public BigDecimal getPremium() { return premium; }
}
Code Review 檢查要點
實體檢查清單:
- [ ] 是否包含業務邏輯方法,而不只是 getter/setter?
- [ ] 方法名是否使用領域專家的語言?
- [ ] 是否正確封裝狀態,透過方法控制變更?
- [ ] 是否避免了基礎設施相關的註解(如 @Entity, @Table)?
值物件:不可變的領域概念
值物件是 DDD 中容易被忽略但非常重要的概念:
// ✅ 正確的值物件實作
public class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
this.validateAmount(amount); // ✅ 建立時驗證
this.amount = amount;
this.currency = currency;
}
// ✅ 不可變性:返回新的物件
public Money add(Money other) {
this.validateSameCurrency(other);
return new Money(this.amount.add(other.amount), this.currency);
}
// ✅ 透過屬性值判斷相等性
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Money)) return false;
Money other = (Money) obj;
return Objects.equals(amount, other.amount) &&
Objects.equals(currency, other.currency);
}
}
值物件檢查清單:
- [ ] 是否不可變(final 欄位,無 setter)?
- [ ] 是否在建立時執行驗證?
- [ ] equals/hashCode 是否基於屬性值?
- [ ] 是否包含有意義的業務方法?
Application 層:流程編排者
Application 層常常是業務邏輝洩露的重災區。讓我們看看正確的實作:
// ✅ 正確的應用服務
@ApplicationService
@Transactional
public class PolicyApplicationService {
public PolicyId createPolicy(CreatePolicyCommand command) {
// 1. 載入必要的領域物件
Customer customer = customerRepo.findById(command.getCustomerId());
// 2. 委託領域層處理業務邏輯
Policy policy = Policy.create(customer, command.toPolicyRequest());
// 3. 協調基礎設施操作
policyRepo.save(policy);
eventPublisher.publish(new PolicyCreatedEvent(policy.getId()));
return policy.getId();
}
}
❌ 常見錯誤:業務邏輯寫在 Application 層
@ApplicationService
public class PolicyApplicationService {
public void createPolicy(CreatePolicyCommand command) {
// ❌ 業務規則不應該寫在這裡
if (command.getAge() < 18) {
throw new UnderageException();
}
// ❌ 複雜計算不應該在這裡
BigDecimal premium = calculateBasePremium()
.multiply(getAgeRisk(command.getAge()))
.multiply(getOccupationRisk(command.getOccupation()));
}
}
Application 層檢查清單:
- [ ] 是否只做流程編排,不包含業務邏輯?
- [ ] 是否正確管理事務邊界?
- [ ] 是否委託給 Domain 層處理複雜邏輯?
- [ ] 是否避免複雜的 if-else 判斷?
常見違規:Domain 層的禁區
在 Code Review 中,以下任何一項出現在 Domain 層都應該被標記為違規:
🚫 絕對不能出現在 Domain 層的東西
- 資料庫註解:
@Entity,@Table,@Column,@JoinColumn - Spring 框架:
@Service,@Repository,@Autowired,@Component - HTTP 相關:
HttpServletRequest,@RequestBody,@PathVariable - 外部服務:
RestTemplate,WebClient,HttpClient - 持久化技術:SQL 語句、JDBC、JPA Criteria
- 序列化註解:
@JsonProperty,@XmlElement - 快取操作:
@Cacheable, Redis 操作 - 檔案操作:
File,InputStream,OutputStream
記住:Domain 層必須保持純淨,只關注業務邏輯!
Infrastructure 層:技術實作的歸宿
Infrastructure 層是所有技術細節的家,但也要遵循一定的原則:
// ✅ 正確的倉儲實作
@Repository
public class JpaPolicyRepository implements PolicyRepository {
@Override
public Optional<Policy> findByPolicyNumber(PolicyNumber policyNumber) {
Optional<PolicyEntity> entity = jpaRepository.findByNumber(policyNumber.getValue());
return entity.map(this::toDomainObject);
}
@Override
public void save(Policy policy) {
PolicyEntity entity = this.toEntity(policy);
jpaRepository.save(entity);
}
// ✅ 正確處理 Domain Object ↔ Database Entity 轉換
private Policy toDomainObject(PolicyEntity entity) {
// 轉換邏輯
}
}
Code Review 最佳實踐
檢查流程
- 結構檢查:先看目錄結構和分層是否正確
- 依賴檢查:用工具檢查依賴方向
- Domain 檢查:重點審查業務邏輯放置
- 命名檢查:確保使用一致的領域語言
- 測試檢查:驗證業務規則測試覆蓋
實用工具推薦
- ArchUnit:自動化架構檢查
- SonarQube:程式碼品質分析
- Checkstyle:命名規範檢查
測試:DDD 專案的品質保證
好的 DDD 專案應該有分層的測試策略:
// ✅ Domain 層單元測試
public class InsurancePolicyTest {
@Test
public void should_throw_exception_when_claim_on_expired_policy() {
// Given
Policy expiredPolicy = PolicyBuilder.anExpiredPolicy().build();
ClaimRequest request = new ClaimRequest(Money.of(1000));
// When & Then
assertThatThrownBy(() -> expiredPolicy.processClaim(request))
.isInstanceOf(PolicyCannotClaimException.class);
}
}
測試檢查要點:
- [ ] Domain 層有豐富的單元測試?
- [ ] 測試是否使用領域語言?
- [ ] 是否測試了重要的業務規則?
- [ ] Domain 測試是否不依賴外部資源?
結論:保持 DDD 的初心
DDD 不只是一種架構模式,更是一種思維方式。它要求我們:
- 以業務為中心:所有設計決策都要回到業務價值
- 保持領域純淨:Domain 層只關注業務邏輯
- 使用通用語言:程式碼要能被領域專家理解
- 持續演進:隨著業務理解的深入而重構
記住,優秀的 DDD 專案不是一蹴而就的,需要團隊持續的努力和嚴格的 Code Review。使用這份檢查清單,可以幫助你的團隊寫出真正體現 DDD 精神的程式碼。
最後提醒:Domain 層是整個 DDD 的核心,必須保持純淨!
每一次 Code Review 都是學習和改進的機會。讓我們一起寫出更好的 DDD 程式碼!






















