DDD分层的例子-优惠券的发放
DDD项目分层结构
// Controller:参数校验、路由、返回包装,不写业务逻辑
// Application Service:事务边界、业务编排,Dubbo 调用远端服务
// Domain:纯业务逻辑,无技术框架依赖,无 Dubbo 引用
// Repository / Infrastructure:数据持久化、Redis、MQ、RPC 通信
下面用一个"优惠券发放"业务场景,把 Dubbo Provider / Consumer 在四层中的位置都演示清楚。
场景说明
Consumer(订单服务)调用 Provider(优惠券服务)查询用户可用券,再由订单服务自己完成扣减逻辑。
一、Provider 侧——优惠券服务
1. Domain 层(纯业务,零框架依赖)
java
复制
// domain/model/Coupon.java
public class Coupon {
private Long id;
private Long userId;
private String code;
private BigDecimal discountAmount;
private CouponStatus status; // AVAILABLE / USED / EXPIRED
/** 核心业务规则:只有可用状态才能被使用 */
public void markUsed() {
if (this.status != CouponStatus.AVAILABLE) {
throw new DomainException("券 [" + code + "] 当前不可用,状态=" + status);
}
this.status = CouponStatus.USED;
}
}
// domain/service/CouponDomainService.java
// 纯业务计算,无任何框架注解
public class CouponDomainService {
public List<Coupon> filterAvailable(List<Coupon> coupons) {
return coupons.stream()
.filter(c -> c.getStatus() == CouponStatus.AVAILABLE)
.collect(Collectors.toList());
}
}
这一层没有
@Service、没有@DubboService,可以单独单元测试。
2. Repository / Infrastructure 层(持久化 + RPC 通信)
java
复制
// infrastructure/repository/CouponRepositoryImpl.java
@Repository // ← Spring 的,不是 Dubbo 的
public class CouponRepositoryImpl implements CouponRepository {
@Autowired
private CouponMapper couponMapper; // MyBatis Mapper
@Override
public List<Coupon> findAvailableByUserId(Long userId) {
return couponMapper.selectAvailableByUserId(userId)
.stream()
.map(CouponConverter::toDomain)
.collect(Collectors.toList());
}
@Override
public void save(Coupon coupon) {
couponMapper.updateStatus(coupon.getId(), coupon.getStatus().name());
}
}
3. Application Service 层(事务边界 + 业务编排)
java
复制
// application/CouponAppService.java
@Service // Spring Bean,不对外暴露 Dubbo
public class CouponAppService {
@Autowired
private CouponRepository couponRepository;
@Autowired
private CouponDomainService couponDomainService;
/** 查询:无副作用,无事务 */
public List<Coupon> queryAvailable(Long userId) {
List<Coupon> all = couponRepository.findAvailableByUserId(userId);
return couponDomainService.filterAvailable(all);
}
/** 使用券:有状态变更,加事务 */
@Transactional
public void useCoupon(Long userId, String couponCode) {
Coupon coupon = couponRepository.findByUserIdAndCode(userId, couponCode)
.orElseThrow(() -> new BizException("券不存在"));
coupon.markUsed(); // Domain 规则校验
couponRepository.save(coupon);
}
}
4. Controller / Dubbo Provider 层(对外暴露接口)
java
复制
// api/dto/CouponDTO.java(放在公共 api 模块,Consumer 依赖)
public class CouponDTO implements Serializable {
private Long id;
private String code;
private BigDecimal discountAmount;
}
// api/CouponFacade.java(接口定义,放公共 api 模块)
public interface CouponFacade {
List<CouponDTO> queryAvailable(Long userId);
void useCoupon(Long userId, String couponCode);
}
// interfaces/dubbo/CouponFacadeImpl.java ← Dubbo Provider 实现
@DubboService(version = "1.0.0", group = "coupon") // ← 只在这里出现 Dubbo 注解
public class CouponFacadeImpl implements CouponFacade {
@Autowired
private CouponAppService couponAppService; // 调 AppService,不碰 Domain
@Override
public List<CouponDTO> queryAvailable(Long userId) {
// 1. 参数校验
Assert.notNull(userId, "userId 不能为空");
// 2. 调用 AppService
List<Coupon> coupons = couponAppService.queryAvailable(userId);
// 3. 转 DTO 返回(不把领域对象透传出去)
return CouponConverter.toDTOList(coupons);
}
@Override
public void useCoupon(Long userId, String couponCode) {
Assert.notNull(userId, "userId 不能为空");
Assert.hasText(couponCode, "couponCode 不能为空");
couponAppService.useCoupon(userId, couponCode);
}
}
二、Consumer 侧——订单服务
1. Infrastructure 层(RPC 通信,引用远端服务)
java
复制
// infrastructure/rpc/CouponRpcClient.java
@Component
public class CouponRpcClient {
// @DubboReference 只在 Infrastructure 层出现
@DubboReference(version = "1.0.0", group = "coupon",
timeout = 3000, retries = 2,
check = false)
private CouponFacade couponFacade; // 引用公共 api 模块的接口
public List<CouponDTO> getAvailableCoupons(Long userId) {
try {
return couponFacade.queryAvailable(userId);
} catch (RpcException e) {
// 降级:返回空券列表,不阻断主流程
log.warn("查询优惠券 RPC 失败,降级返回空列表 userId={}", userId, e);
return Collections.emptyList();
}
}
public void consumeCoupon(Long userId, String couponCode) {
couponFacade.useCoupon(userId, couponCode);
}
}
2. Application Service 层(业务编排,调 RPC Client)
java
复制
// application/OrderAppService.java
@Service
public class OrderAppService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private CouponRpcClient couponRpcClient; // 通过 Infrastructure 层调用,不直接用 Facade
@Transactional
public OrderResult createOrder(CreateOrderCmd cmd) {
// 1. 查询可用券(RPC 调用在 Infrastructure 层)
List<CouponDTO> coupons = couponRpcClient.getAvailableCoupons(cmd.getUserId());
// 2. 匹配用户指定的券
CouponDTO chosen = coupons.stream()
.filter(c -> c.getCode().equals(cmd.getCouponCode()))
.findFirst()
.orElseThrow(() -> new BizException("券不可用"));
// 3. 计算最终金额(Domain 层计算)
Order order = OrderFactory.create(cmd, chosen);
// 4. 持久化
orderRepository.save(order);
// 5. 核销券(RPC,跨服务写操作)
couponRpcClient.consumeCoupon(cmd.getUserId(), cmd.getCouponCode());
return OrderConverter.toResult(order);
}
}
3. Controller 层(HTTP 入口,不含业务逻辑)
java
复制
// interfaces/http/OrderController.java
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderAppService orderAppService;
@PostMapping
public Result<OrderResult> create(@RequestBody @Validated CreateOrderRequest req) {
CreateOrderCmd cmd = OrderConverter.toCmd(req); // DTO → Command
OrderResult result = orderAppService.createOrder(cmd);
return Result.ok(result);
}
}
三、依赖关系图
┌─────────────────────────────────────────┐
│ 公共 api 模块(coupon-api) │
│ CouponFacade 接口 + CouponDTO │
└───────────────┬─────────────────────────┘
│ maven dependency
┌───────────┴──────────┐
│ │
┌───▼──────────┐ ┌──────▼────────────┐
│ 优惠券服务 │ │ 订单服务 │
│ (Provider) │ │ (Consumer) │
│ │ │ │
│ @DubboService│ │ @DubboReference │
│ 只在 interfaces│ │ 只在 infrastructure│
│ /dubbo/ 层 │ │ /rpc/ 层 │
└──────────────┘ └────────────────────┘
四、关键原则总结
| 层次 | Dubbo 注解 | 职责 |
|---|---|---|
Controller / @DubboService |
Provider 唯一出口 | 参数校验、DTO 转换、调 AppService |
| Application Service | ❌ 无 Dubbo | 事务边界、业务编排、调 RpcClient |
| Domain | ❌ 无任何框架 | 纯业务规则、聚合根、领域服务 |
Infrastructure / @DubboReference |
Consumer 唯一入口 | RPC 调用 + 降级兜底 |
一句话原则:
@DubboService只在 Provider 的interfaces层;@DubboReference只在 Consumer 的infrastructure层——Domain 和 AppService 永远不感知 Dubbo 的存在。
Application Service 层不直接调用facade,要封装一层RpcClient的四层用意:
一、最核心:依赖倒置,Application Service 不感知 RPC 框架
java
复制
// 如果 AppService 直接用 @DubboReference:
@Service
public class OrderAppService {
@DubboReference(version="1.0.0", group="coupon", timeout=3000)
private CouponFacade couponFacade; // ← AppService 直接依赖 Dubbo
// 问题:换成 gRPC/Feign/HTTP 时,AppService 代码必须改
}
// 封装 RpcClient 后:
@Service
public class OrderAppService {
@Autowired
private CouponRpcClient couponRpcClient; // ← 只依赖自己定义的接口
// 换框架只改 CouponRpcClient,AppService 零改动
}
这就是依赖倒置原则(DIP):AppService 依赖"自己定义的抽象",而不是"Dubbo 的具体实现"。
二、降级兜底逻辑的归宿
降级属于"技术容错",不是业务逻辑,不该放在 AppService:
java
复制
// ❌ 错误做法:降级逻辑污染 AppService
public OrderResult createOrder(CreateOrderCmd cmd) {
List<CouponDTO> coupons;
try {
coupons = couponFacade.queryAvailable(cmd.getUserId()); // Dubbo 调用
} catch (RpcException e) {
coupons = Collections.emptyList(); // 这行不该在 AppService 里
}
// 继续业务编排...
}
// ✅ 正确做法:降级封在 RpcClient,AppService 看不见
public OrderResult createOrder(CreateOrderCmd cmd) {
// 直接用,不管 RPC 是否失败——失败已在 RpcClient 里降级了
List<CouponDTO> coupons = couponRpcClient.getAvailableCoupons(cmd.getUserId());
// 继续业务编排...
}
三、参数拼装、超时重试等技术细节的收口
RPC 调用往往有很多技术参数:
java
复制
// CouponRpcClient 统一管理这些技术关注点
@DubboReference(version = "1.0.0", group = "coupon",
timeout = 3000, retries = 2, // ← 超时、重试
loadbalance = "roundrobin", // ← 负载均衡
check = false) // ← 启动不检查
private CouponFacade couponFacade;
public List<CouponDTO> getAvailableCoupons(Long userId) {
// 参数转换、日志打点、链路追踪 tag 也在这里统一做
RpcContext.getContext().setAttachment("traceId", MDC.get("traceId"));
return couponFacade.queryAvailable(userId);
}
这些如果散落在 AppService 里,每个调用点都要重复写,且改动时要找所有散落点。
四、单元测试的可 Mock 性
java
复制
// AppService 的单元测试,直接 Mock RpcClient
@ExtendWith(MockitoExtension.class)
class OrderAppServiceTest {
@Mock
private CouponRpcClient couponRpcClient; // ← 轻松 Mock,无需启动 Dubbo
@InjectMocks
private OrderAppService orderAppService;
@Test
void createOrder_withValidCoupon_success() {
when(couponRpcClient.getAvailableCoupons(anyLong()))
.thenReturn(List.of(mockCoupon()));
// 直接测业务逻辑,不依赖真实 RPC 环境
}
}
// 如果 AppService 直接用 @DubboReference,
// 单元测试就必须启动 Dubbo 容器,变成集成测试,极慢
总结
| 用意 | 封装 RpcClient 解决了什么 |
|---|---|
| 依赖倒置 | AppService 不绑定 Dubbo,换框架零改动 |
| 降级归宿 | try/catch/fallback 收口在 Infrastructure,不污染业务层 |
| 技术参数收口 | timeout/retry/traceId 统一管理,不散落各处 |
| 可测试性 | AppService 单测只需 Mock 接口,无需启动 RPC 容器 |
一句话:RpcClient 是 Infrastructure 层的"防腐层"——它把 Dubbo 的技术细节全部挡在外面,让 AppService 只看见"我需要一个查券的能力",而不是"我在用 Dubbo 调用一个版本号 1.0.0 的远端服务"。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)