
代码的本质,是对现实世界的模拟(或许,现实世界也是代码虚拟出来的)。人们对现实世界的认知越成熟、越深刻,代码面对的复杂性就越高,就需要更加宽广、健壮的软件架构去囊括、解释这复杂的世界。
软件的架构是开发人员对现实业务认知的集中体现。一个好的架构,能够合理划分业务逻辑,保证内部代码的整洁、清晰,降低开发、维护成本。
近几年,越来越多的公司开始拆服务、搭中台,沉淀自己的核心业务模型,打薄前台应用,以求更快应对市场变化。然而,传统的集中式架构思想无法精准提炼核心业务逻辑、识别业务领域边界,无法指导业务模型的建立。在这种大环境下,04年由著名建模专家 eric evans 提出的 “领域驱动设计” (Domain Driven Design)思想开始重新进入人们的视野,对业务领域识别、微服务拆分、软件架构起到了极大的指导作用。
DDD 思想包含很多概念,实体、值对象、聚合根、领域边界、限界上下文等等,单纯讲这些概念非常抽象,难以在具体项目中进行实践,造成了很多开发人员听说过 DDD,但对其仍是一知半解。
本文直接避开这些抽象概念,从具体案例出发,比较传统软件架构模式与 DDD 模式的区别,同时讲解 DDD 思想指导下的具体业务的实现方式,让各位看官对 DDD 思想有一个更加直观的感受。




现在,我们举一个简单的业务案例,看看在实际情况中,我们应该如何使用 DDD 去实现业务。业务需求描述如下:
public class ConfirmDeliverGoodsServiceImpl implements ConfirmDeliverGoodsService {@Autowiredprivate OrderDao orderDao;@Autowiredprivate DeliveryOrderDao deliveryOrderDao;@Autowiredprivate STOExpressService stoExpressService;@Override@Transcationalpublic Result<Boolean> confirmDeliverGoods(String deliveryOrderCode, String expressCode) {//参数校验if (StringUtils.isEmpty(deliveryOrderCode) || StringUtils.isEmpty(expressCode) ) {throw new IllegalArgumentException("deliveryOrderCode/expressCode 不能为空");}//业务校验if (!(expressCode.startsWith("268")|| expressCode.startsWith("368")|| expressCode.startsWith("468")|| expressCode.startsWith("968"))) {throw new RuntimeException("只支持申通发货");}//从数据库读取发货单、订单数据DeliveryOrderDO deliveryOrderDO = deliveryOrderDao.getByCode(deliveryOrderCode);OrderDO orderDO = orderDao.getByCode(deliveryOrderDO.getOrderCode());//调用申通快递开放接口,查询物流信息ExpressBaseInfoDTO expressBaseInfoDTO = stoExpressService.queryExpressBaseInfo(expressCode);//更新发货单信息deliveryOrderDO.setStatus(DeliveryOrderStatusEnum.SHIPPED.getCode());deliveryOrderDO.setExpressCompany(expressBaseInfoDTO.getCompanyName());deliveryOrderDO.setExpressCode(expressCode);//更新订单信息orderDO.setStatus(OrderStatusEnum.WAITING_RECEIVED.getCode());orderDO.setShippedTime(new Date());orderDO.setExpressCode(expressCode);//数据持久化deliveryOrderDao.update(deliveryOrderDO);orderDao.update(orderDO);//发送RocketMQ消息rocketMQProducer.send("SEND_GOODS_TOPIC", JSON.toJSONString(orderDO));return Result.success(true);}}
public class DeliveryOrder {//发货单号private String code;//发货单状态private DeliveryOrderStatusEnum status;//快递费用(分)private Long expressFee;//关联订单private String orderCode;//物流单号private String expressCode;}public class Order {//订单号码private String code;//订单状态private OrderStatusEnum status;//发货时间private Date shippedTime;//物流单号private String expressCode;}public class ExpressBaseInfo {//物流单号private String code;//快递费用private Long expressFee;//快递公司名private String companyName;}
//发货单号public class DeliveryOrderCode {private String code;public DeliveryOrderCode(String code) {if (code == null || "".equals(code.trim())) {throw new IllegalArgumentException("发货单code不能为空");}this.code = code;}public String getCode() {return code;}}//订单号public class OrderCode {private String code;public OrderCode(String code) {if (code == null || "".equals(code.trim())) {throw new IllegalArgumentException("订单code不能为空");}this.code = code;}public String getCode() {return code;}}//物流单号public class ExpressCode {private String code;public ExpressCode(String code) {if (code == null || "".equals(code.trim())) {throw new IllegalArgumentException("物流单号code不能为空");}if (!(code.startsWith("268")|| code.startsWith("368")|| code.startsWith("468")|| code.startsWith("968"))) {throw new IllegalArgumentException("物流单号code不是申通快递");}this.code = code;}public String getCode() {return code;}}
public class DeliveryOrder {//发货单号private DeliveryOrderCode code;//发货单状态private DeliveryOrderStatusEnum status;//快递费用(分)private Long expressFee;//关联订单private OrderCode orderCode;//物流单号private ExpressCode expressCode;}public class Order {//订单号码private OrderCode code;//订单状态private OrderStatusEnum status;//发货时间private Date shippedTime;//物流单号private ExpressCode expressCode;}public class ExpressBaseInfo {//物流单号private ExpressCode code;//快递费用private Long expressFee;//快递公司名private String companyName;}
@Getter@Setterpublic class DeliveryOrder {//发货单号private DeliveryOrderCode code;//发货单状态private DeliveryOrderStatusEnum status;//快递费用(分)private Long expressFee;//关联订单private OrderCode orderCode;//物流单号private ExpressCode expressCode;public void confirmShipped(ExpressBaseInfo baseInfo) {if (baseInfo == null) {throw new IllegalArgumentException("物流信息为空,无法却认发货");}setStatus(DeliveryOrderStatusEnum.SHIPPED);setExpressFee(baseInfo.getExpressFee());setExpressCode(baseInfo.getCode());}}@Getter@Setterpublic class Order {//订单号码private OrderCode code;//订单状态private OrderStatusEnum status;//发货时间private Date shippedTime;//物流单号private ExpressCode expressCode;public void confirmShipped(ExpressBaseInfo baseInfo) {if (baseInfo == null) {throw new IllegalArgumentException("物流信息为空,订单无法却认发货");}setStatus(OrderStatusEnum.WAITING_RECEIVED);setExpressCode(baseInfo.getCode());setShippedTime(new Date());}}
@Builder@Getter@Setter(AccessLevel.PRIVATE)@NoArgsConstructor(access = AccessLevel.PRIVATE)@AllArgsConstructorpublic class DeliveryOrder {//发货单号private DeliveryOrderCode code;//发货单状态private DeliveryOrderStatusEnum status;//快递费用(分)private Long expressFee;//关联订单private OrderCode orderCode;//物流单号private ExpressCode expressCode;public void confirmShipped(ExpressBaseInfo baseInfo) {if (baseInfo == null) {throw new IllegalArgumentException("物流信息为空,无法却认发货");}setStatus(DeliveryOrderStatusEnum.SHIPPED);setExpressFee(baseInfo.getExpressFee());setExpressCode(baseInfo.getCode());}}@Builder@Getter@Setter(AccessLevel.PRIVATE)@NoArgsConstructor(access = AccessLevel.PRIVATE)@AllArgsConstructorpublic class Order {//订单号码private OrderCode code;//订单状态private OrderStatusEnum status;//发货时间private Date shippedTime;//物流单号private ExpressCode expressCode;public void confirmShipped(ExpressBaseInfo baseInfo) {if (baseInfo == null) {throw new IllegalArgumentException("物流信息为空,订单无法却认发货");}setStatus(OrderStatusEnum.WAITING_RECEIVED);setExpressCode(baseInfo.getCode());setShippedTime(new Date());}}@Builder@Getter@Setter(AccessLevel.PRIVATE)@NoArgsConstructor(access = AccessLevel.PRIVATE)@AllArgsConstructorpublic class ExpressBaseInfo {//物流单号private ExpressCode code;//快递费用private Long expressFee;//快递公司名private String companyName;}
其次,我们通过业务发现,“确认发货” 这一逻辑,不是一个实体参与就能实现的,它需要发货单、以及发货单关联的订单这两个实体共同 “确认发货” 才能完成。
这种情况就需要创建领域服务(Domain Service),以组合多个实体的行为,实现完整的业务逻辑。我们专门创建一个 ConfirmShippedService:
public interface ConfirmShippedService {void confirmShipped(DeliveryOrder deliveryOrder, Order order, ExpressBaseInfo expressBaseInfo);}public class ConfirmShippedServiceImpl implements ConfirmShippedService {@Overridepublic void confirmShipped(DeliveryOrder deliveryOrder, Order order, ExpressBaseInfo expressBaseInfo) {deliveryOrder.confirmShipped(expressBaseInfo);order.confirmShipped(expressBaseInfo);}}
public interface DeliveryOrderRepository {DeliveryOrder find(DeliveryOrderCode code);void save(DeliveryOrder order);}@Repositorypublic class DeliveryOrderRepositoryImpl implements DeliveryOrderRepository {@Autowiredprivate DeliveryOrderMapper mapper;@Overridepublic DeliveryOrder find(DeliveryOrderCode code) {if (code == null) {return null;}DeliveryOrderDO deliveryOrderDO = mapper.selectByPrimaryKey(code.getCode());return DeliveryOrderConverter.toEntity(deliveryOrderDO);}@Overridepublic void save(DeliveryOrder deliveryOrder) {DeliveryOrderDO deliveryOrderDO = DeliveryOrderConverter.toDO(deliveryOrder);if (deliveryOrderDO.getCode() == null) {mapper.insert(deliveryOrderDO);} else {mapper.update(deliveryOrderDO);}}}public class DeliveryOrderConverter {public static DeliveryOrder toEntity(DeliveryOrderDO deliveryOrderDO) {if (deliveryOrderDO == null) {return null;}return DeliveryOrder.builder().code(new DeliveryOrderCode(deliveryOrderDO.getCode())).status(DeliveryOrderStatusEnum.getEnumByCode(deliveryOrderDO.getCode())).expressFee(deliveryOrderDO.getExpressFee()).orderCode(new OrderCode(deliveryOrderDO.getOrderCode())).expressCode(new ExpressCode(deliveryOrderDO.getCode())).build();}public static DeliveryOrderDO toDO(DeliveryOrder deliveryOrder) {if (deliveryOrder == null) {return null;}DeliveryOrderDO deliveryOrderDO = new DeliveryOrderDO();deliveryOrderDO.setCode(deliveryOrder.getCode().getCode());deliveryOrderDO.setStatus(deliveryOrder.getStatus().getCode());deliveryOrderDO.setExpressCode(deliveryOrder.getExpressCode().getCode());deliveryOrderDO.setExpressFee(deliveryOrder.getExpressFee());deliveryOrderDO.setOrderCode(deliveryOrder.getOrderCode().getCode());return deliveryOrderDO;}}public interface OrderRepository {Order find(OrderCode code);void save(Order order);}@Repositorypublic class OrderRepositoryImpl implements OrderRepository {@Autowiredprivate OrderMapper mapper;@Overridepublic Order find(OrderCode code) {if (code == null) {return null;}OrderDO orderDO = mapper.selectByPrimaryKey(code.getCode());return OrderConverter.toEntity(orderDO);}@Overridepublic void save(Order order) {OrderDO orderDO = OrderConverter.toDO(order);if (orderDO.getCode() == null) {mapper.insert(orderDO);} else {mapper.update(orderDO);}}}public class OrderConverter {public static Order toEntity(OrderDO orderDO) {if (orderDO == null) {return null;}return Order.builder().code(new OrderCode(orderDO.getCode())).status(OrderStatusEnum.getEnumByCode(orderDO.getStatus())).expressCode(new ExpressCode(orderDO.getExpressCode())).shippedTime(orderDO.getShippedTime()).build();}public static OrderDO toDO(Order order) {if (order == null) {return null;}OrderDO orderDO = new OrderDO();orderDO.setCode(order.getCode().getCode());orderDO.setStatus(order.getStatus().getCode());orderDO.setExpressCode(order.getExpressCode().getCode());orderDO.setShippedTime(order.getShippedTime());return orderDO;}}
public interface ExpressBaseInfoService {ExpressBaseInfo queryExpressBaseInfo(ExpressCode expressCode);}@Componentpublic class ExpressBaseInfoServiceImpl implements ExpressBaseInfoService {@Autowiredprivate STOExpressService stoExpressService;@Overridepublic ExpressBaseInfo queryExpressBaseInfo(ExpressCode expressCode) {if (expressCode == null) {return null;}ExpressBaseInfoDTO expressBaseInfoDTO = stoExpressService.queryExpressBaseInfo(expressCode.getCode());return ExpressBaseInfoConverter.toEntity(expressBaseInfoDTO);}}public class ExpressBaseInfoConverter {public static ExpressBaseInfo toEntity(ExpressBaseInfoDTO dto) {if (dto == null) {return null;}return ExpressBaseInfo.builder()....build();}}
public abstract class AbstractMessage {public abstract String getTopic();public String parseJsonStr() {return JSON.toJSONString(this);}public String getMessageId() {return UUID.randomUUID().toString().replace("-", "");}}public class ConfirmShippedMessage extends AbstractMessage {private String deliveryCode;//快递费用(分)private Long expressFee;//物流单号private String expressCode;public ConfirmShippedMessage(DeliveryOrder deliveryOrder) {this.deliveryCode = deliveryOrder.getCode().getCode();this.expressFee = deliveryOrder.getExpressFee();this.expressCode = deliveryOrder.getExpressCode().getCode();}@Overridepublic String getTopic() {return "SEND_GOODS_TOPIC";}}public interface MessageProducer {void send(AbstractMessage abstractMessage);}@Componentpublic class MessageProducerImpl implements MessageProducer, InitializingBean {private DefaultMQProducer producer;@Overridepublic void send(AbstractMessage abstractMessage) {try {Message message = new Message(abstractMessage.getTopic(),abstractMessage.parseJsonStr().getBytes(RemotingHelper.DEFAULT_CHARSET));SendResult sendResult = producer.send(message);} catch (MQClientException | RemotingException | MQBrokerException | InterruptedException | UnsupportedEncodingException e) {e.printStackTrace();}}@Overridepublic void afterPropertiesSet() throws Exception {producer = new DefaultMQProducer("group_name");producer.start();}}
@Componentpublic class ConfirmDeliverGoodsServiceV2Impl implements ConfirmDeliverGoodsServiceV2 {@Autowiredprivate DeliveryOrderRepository deliveryOrderRepository;@Autowiredprivate OrderRepository orderRepository;@Autowiredprivate ExpressBaseInfoService expressBaseInfoService;@Autowiredprivate ConfirmShippedService confirmShippedService;@Autowiredprivate MessageProducer messageProducer;@Overridepublic Result<Boolean> confirmDeliverGoods(DeliveryOrderCode deliveryOrderCode, ExpressCode expressCode) {DeliveryOrder deliveryOrder = deliveryOrderRepository.find(deliveryOrderCode);Order order = orderRepository.find(deliveryOrder.getOrderCode());ExpressBaseInfo expressBaseInfo = expressBaseInfoService.queryExpressBaseInfo(expressCode);confirmShippedService.confirmShipped(deliveryOrder, order, expressBaseInfo);deliveryOrderRepository.save(deliveryOrder);orderRepository.save(order);messageProducer.send(new ConfirmShippedMessage(deliveryOrder));return Result.success(true);}}






应用的 Spring Boot 启动模块,依赖基础设施层的各个模块,带动各个端口启动。
应用采用 Spring 的 IOC 机制保证基础设施模块对于 Domain 模块的接口协议的实现能够灵活插拔、替换,使得我们的应用很好遵循了依赖倒置原则。
client 模块是我们应用的客户端模块,是要被其他应用所集成、使用的,一般微服务应用的 client 模块就是 RPC 服务的接口协议的 jar 包。
其他应用通过我们应用的 client 模块访问我们的服务。既然要使用我们的服务,就要遵循我们的规则、协议。
所以,client 模块依赖 common 模块,一起打包给其他业务方,让我们的通用业务概念、业务规则能够被兄弟业务方感知、遵循。所以,common 模块内的值对象应该实现 Serializable 接口,支持序列与反序列化。
同时,client 模块内一些接口的入参(读命令(Query)、写命令(Command))实例的构造应该包含一些参数校验逻辑,将这些逻辑抛给服务调用方去执行,保证服务调用方传过来的参数一定是合法的,杜绝一些荒唐的无效调用,节省带宽资源。
至此,DDD 的大致思想和落地实践就表述完了。当然还有很多点没有涉及到,聚合根该怎么设计?领域事件的作用?CQRS 模式怎么构建?..... DDD 应用的优化方向、细节问题还有很多,需要我们在具体业务中不断思考、实践。欢迎关注本公众号后续文章,博主会在这里继续分享在 DDD 实践方面的心得。




