用 Spring 思维快速上手 DDD——以 Kratos 为例的分层解读

发布于:2025-08-14 ⋅ 阅读:(17) ⋅ 点赞:(0)

用 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;

  @Transactional
  public 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);
  }
}
  • 业务逻辑在领域对象:例如ensureActivereservefreeze方法,他们只关心业务实现,不关心数据是怎么获取的
  • 数据访问集中在仓储实现: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 接口定义在领域层,实现放在基础设施层;

领域模型对外暴露的是业务语义,数据访问实现细节被封装在仓储里,上层业务不感知底层变化。


网站公告

今日签到

点亮在社区的每一天
去签到