用 Spring 思維理解 DDD —— 以 Kratos 為參照
? 在此前的學習工作中,使用的開發框架一直都是 SpringBoot,對 MVC 架構幾乎是肌肉記憶:Controller 接請求,Service 寫業務邏輯,Mapper 操作數據庫,這套套路早已深入骨髓。
? 最近開始學習 Go,為了寫微服務,接觸到了 Kratos 框架,也順帶深入了解了 DDD(領域驅動設計)。一開始,我的第一反應是——“這不就是 MVC 換個說法嗎?”
但越學越發現,雖然 DDD 和我們熟悉的 Spring MVC 分層在形狀上很像,但它對“業務邏輯該放哪、數據訪問該放哪”劃分得更嚴格,還用工程化的方式強制執行這些規則。
? 這篇文章,我就用自己熟悉的 Java Spring 思維,把 DDD 的分層思想翻譯成 Spring 語言,再對照 Kratos 在 Go 里的實現,幫你快速搞懂它們的異同。
往期博客
Go語言新手村:輕松理解變量、常量和枚舉用法
Go 語言中的結構體、切片與映射:構建高效數據模型的基石
1. Spring 的常見分層
在 Java 項目中,最典型的分層是:
Controller → Service → Mapper → DB
- Controller:接收請求、參數校驗、調用 Service
- Service:執行業務邏輯(有時混合數據訪問)
- Mapper:訪問數據庫(MyBatis Mapper / JPA Repository)
這種分層沒錯,而且配合團隊自律,也能寫出很干凈的項目結構。但現實是:
- Service 往往既寫業務規則,又寫 SQL 條件拼接;
- 業務規則分散在 Service、Mapper 甚至 Controller 中;
- 一旦底層數據訪問方式變化(換數據庫、換 RPC),改動會影響大量上層代碼。
2. DDD 的目標:邊界清晰、職責單一
DDD 要解決的,就是讓業務邏輯與技術實現徹底解耦,做到:
- 業務規則集中在領域模型中,貼著數據維護不變式;
- 數據訪問細節被隔離在倉儲實現中,隨時可替換;
- 跨聚合的編排邏輯集中在應用服務(Usecase)中,清晰可測。
DDD 的典型分層:
接口層(Controller / API)→ 應用層(Usecase / Application Service)→ 領域層(Entity / Aggregate / Domain Service / Repository 接口)→ 基礎設施層(Repository 實現 / 外部服務實現)
3. 用 Spring 語言對照 DDD 分層
DDD 層次 | Kratos 對應 | Spring 對應 | 職責 |
---|---|---|---|
接口層(API) | service | Controller | 參數校驗、鑒權、DTO ? Domain 轉換 |
應用層(Usecase) | biz | Service(理想狀態) | 編排業務流程、事務控制、調用多個領域對象或外部服務 |
領域層 | repo | 實體類、領域服務接口 | 維護業務不變式、暴露行為方法、定義倉儲接口 |
基礎設施層 | data | Mapper / Feign 實現層 | 數據持久化、調用遠程服務、模型映射(PO ? Domain) |
4. 核心理念對比
4.1 Repository
- Spring 常見寫法:Mapper/Repository 接口直接返回 PO(數據庫模型),業務層可能直接用它判斷。
- DDD 寫法:倉儲接口定義在領域層,返回的是領域對象(封裝了業務行為的方法),由基礎設施層實現。
4.2 業務規則的位置
- 常見誤區:在 Service 里寫
if (user.getEnabled() == 0) throw ...
。 - DDD 方式:在領域對象中提供
ensureActive()
,領域對象自己決定什么是可用。
4.3 應用服務(Usecase)
- 職責:一次完整業務流程的編排、事務邊界、調用多個倉儲接口、發布領域事件。
- 不做的事:不直接寫 SQL,不去判斷
user.getEnabled()
,不實現底層細節。
5. 案例:鎖單流程
下面的鎖單方法只是為了演示 DDD 分層思路,并不具備生產可用性。在真實系統中,鎖單流程往往要面對并發控制、一致性等復雜問題。
5.1 常見 Spring 寫法(簡化版)
@Service
@AllArgsConstructor
public class OrderService {private final OrderMapper orderMapper;private final UserMapper userMapper;private final StockMapper stockMapper;private final WalletMapper walletMapper;@Transactionalpublic String lockOrder(String userId, String sku, int count) {// 校驗賬戶是否可用if (!userMapper.ensureActive(userId)) {throw new BizException("用戶不可用");}// 校驗庫存if (!stockMapper.hasStock(sku, count)){throw new BizException("商品庫存不足");}// 檢查余額if (!walletMapper.hasBalance(userId, count * price)){ throw new BizException("余額不足");}// 預減庫存stockMapper.reserveStock(...);// 凍結余額walletMapper.freezeBalance(...);// 添加一條訂單記錄orderMapper.insertOrder(...);return orderId;}
}
缺點:業務判斷和數據訪問混雜
5.2 DDD 寫法(Spring 風格)
領域層(實體類 + 倉儲接口)
// 聚合根:用戶
public class User {public void ensureActive() { /* 校驗用戶有效性 */ }
}// 聚合根:庫存
public class Stock {public void reserve(int count) { /* 校驗庫存并預留 */ }
}// 聚合根:錢包
public class Wallet {public void freeze(double amount) { /* 校驗余額并凍結 */ }
}// 倉儲接口
public interface UserRepo { User load(String id); void save(User u); }
public interface StockRepo { Stock load(String sku); void save(Stock s); }
public interface WalletRepo { Wallet load(String uid); void save(Wallet w); }
public interface OrderRepo { void save(Order o); }
應用層(用例編排)
@Service
@AllArgsConstructor
public class LockOrderUsecase {private final UserRepo userRepo;private final StockRepo stockRepo;private final WalletRepo walletRepo;private final OrderRepo orderRepo;public void lock(String userId, String sku, int qty, double price) {// 調用倉儲接口,獲取領域對象User user = userRepo.load(userId);Stock stock = stockRepo.load(sku);Wallet wallet = walletRepo.load(userId);// 業務編排user.ensureActive();stock.reserve(qty);wallet.freeze(qty * price);// 持久化數據orderRepo.save(new Order());stockRepo.save(stock);walletRepo.save(wallet);}
}
- 業務邏輯在領域對象:例如
ensureActive
、reserve
、freeze
方法,他們只關心業務實現,不關心數據是怎么獲取的 - 數據訪問集中在倉儲實現:Repo 不做業務判斷,取出來交給實體自己判斷
- 應用層清晰編排流程:用 Repo 加載實體 → 調用實體方法做判斷/修改 → 再通過 Repo 保存變更
6. Kratos 如何落地
Kratos 在 Go 里用目錄結構 + wire 靜態注入強制執行這種依賴方向:
service(接口層) → biz(用例+倉儲接口) → data(倉儲實現) → DB/遠程服務
biz
中不能 import ORM/HTTP 客戶端等具體庫;data
中實現所有倉儲接口,負責 PO ? Domain 映射;service
只負責接收請求、調用 Usecase。
這跟 Spring 在理想狀態下的分層幾乎一致,但 Kratos 用工程手段物理防止越層,減少團隊自律成本。
7. 總結
用 Spring 開發者的眼光看:
- DDD 并不是要你放棄 Controller/Service/Mapper,而是讓 Service 變成 應用服務,專注業務編排;
- 業務判斷應該寫在領域對象中,不應該在 Mapper 或 Service 里直接寫;
- Repository 接口定義在領域層,實現放在基礎設施層;
領域模型對外暴露的是業務語義,數據訪問實現細節被封裝在倉儲里,上層業務不感知底層變化。