why
代码的复杂度随着业务的复杂度而不断增加,导致系统臃肿,难以维护。
what
DDD主要是通过领域内聚的方式降低代码复杂度,核心思想是让技术复杂度与业务复杂度隔离,并通过统一语言组织业务逻辑,降低认知成本。
具体主要体现在:
- 基础设施层:它负责隔离技术复杂度,通过抽象封装来对内提供服务,而不是让内部服务直接使用它。这意味着当外部基础设施变化时,业务并不会被迫进行变更。比如项目中的数据总线是Kafka,之后替换成了Pulsar,业务对其应该是无感知的。
- 厚领域层:同一领域的知识聚合在一个领域中,领域知识不再被割裂。这是单一职责原则的一种体现。
- 实体:用充血模型代替贫血模型,完全符合面向对象的思想。将业务中的对象完全投射到实体中,从面向资源转换成面向过程和面向对象。
how
架构分层
DDD也会分为3层
- 应用层
- 组装领域层和基础设施层完成对应的用户指令
- 领域层
- 保存领域状态(基础字段)和业务规则(对象操作方法)
- 基础设施层
- 消息处理
- 缓存
- RPC
- 数据库操作
应用层
@Service
@Transactional(rollbackFor = Exception.class)
public class DesignerOrderServiceImpl implements DesignerOrderService {
@Autowired
private DesignerOrderRepository designerOrderRepository;
@Autowired
private RefundOrderRepository refundOrderRepository;
@Override
public DesignerOrder createOrder(int customerId, int designerId) {
DesignerOrder order = DesignerOrderFactory.createOrder(customerId, designerId);
designerOrderRepository.create(order);
return designerOrderRepository.selectByKey(order.getId());
}
@Override
public void pay(int orderId, float amount) {
DesignerOrder order = designerOrderRepository.selectByKey(orderId);
if (order == null) {
AppException.throwAppException(AppExceptionMessage.DESIGNER_ORDER_NOT_EXIST_CODE, AppExceptionMessage.DESIGNER_ORDER_NOT_EXIST, orderId);
}
order.pay(amount);
designerOrderRepository.update(order);
}
@Override
public RefundOrder refund(int orderId, String cause) {
DesignerOrder order = designerOrderRepository.selectByKey(orderId);
if (order == null) {
AppException.throwAppException(AppExceptionMessage.DESIGNER_ORDER_NOT_EXIST_CODE, AppExceptionMessage.DESIGNER_ORDER_NOT_EXIST, orderId);
}
RefundOrder refundOrder = order.refund(cause);
designerOrderRepository.update(order);
refundOrderRepository.create(refundOrder);
return refundOrderRepository.selectByKey(refundOrder.getId());
}
}
领域层
@Data
@EqualsAndHashCode(of = {"id"})
public class DesignerOrder implements Entity<DesignerOrder> {
private int id;
private DesignerOrderState state;
private int customerId;
private int designerId;
private float area;
private float expectedAmount;
private int estimatedDays;
private DesigningProgressReport progressReport;
private String abortCause;
private float actualPaidAmount;
private int feedbackStar;
private String feedbackDescription;
private Date createdTime;
private Date updatedTime;
public void pay(float amount) {
Assert.isTrue(amount > 0, "The amount must be bigger than 0.");
if (!DesignerOrderWorkflowService.canChangeState(state, DesignerOrderState.PAID)) {
DomainException.throwDomainException(DomainExceptionMessage.PAYMENT_NOT_IN_READY_STATE_CODE, DomainExceptionMessage.PAYMENT_NOT_IN_READY_STATE, this.id, this.state);
}
if (Math.abs(amount - this.expectedAmount) > 0.01) {
DomainException.throwDomainException(DomainExceptionMessage.PAYMENT_NOT_MATCHED_CODE, DomainExceptionMessage.PAYMENT_NOT_MATCHED, this.id, this.expectedAmount, amount);
}
this.state = DesignerOrderWorkflowService.changeState(this.id, state, DesignerOrderState.PAID);
this.actualPaidAmount = amount;
// 付款完成后,自动启动进度跟踪
this.progressReport.startup();
}
public RefundOrder refund(String cause) {
this.assertCanRefund();
this.state = DesignerOrderWorkflowService.changeState(this.id, state, DesignerOrderState.REFUND);
return RefundOrderFactory.newRefundOrder(this, cause);
}
private void assertCanRefund() {
DesigningProgressNode constructionDrawingDesignNode = this.progressReport.getNode(DesigningProgressNodeType.CONSTRUCTION_DRAWING_DESIGN);
if (constructionDrawingDesignNode.getState() == DesigningProgressNodeState.REQUEST_COMPLETION ||
constructionDrawingDesignNode.getState() == DesigningProgressNodeState.CONFIRM_COMPLETION) {
DomainException.throwDomainException(DomainExceptionMessage.FAILED_TO_REFUND_FOR_PROGRESS_CODE, DomainExceptionMessage.FAILED_TO_REFUND_FOR_PROGRESS, this.id);
}
}
@Override
public boolean sameIdentityAs(DesignerOrder other) {
return this.equals(other);
}
}
基础设施层
@Repository
public class DesignerOrderRepositoryImpl implements DesignerOrderRepository {
private static final String DESIGNER_ORDER_TABLE = "designer_order";
@Autowired
private DesignerOrderMapper designerOrderMapper;
@Override
public void create(DesignerOrder order) {
if (designerOrderMapper.create(order) == 0) {
TableException.throwTableException(DESIGNER_ORDER_TABLE, TableOperation.CREATE);
}
}
@Override
public DesignerOrder selectByKey(int id) {
DesignerOrder order = designerOrderMapper.selectByKey(id);
buildConnection(order);
return order;
}
@Override
public DesignerOrder selectOneBySpecification(DesignerOrder example) {
DesignerOrder designerOrder = designerOrderMapper.selectOneBySpecification(example);
buildConnection(designerOrder);
return designerOrder;
}
@Override
public List<DesignerOrder> selectBySpecification(DesignerOrder example) {
List<DesignerOrder> designerOrders = designerOrderMapper.selectBySpecification(example);
buildConnection(designerOrders);
return designerOrders;
}
@Override
public void update(DesignerOrder order) {
if (designerOrderMapper.update(order) == 0) {
TableException.throwTableException(DESIGNER_ORDER_TABLE, TableOperation.UPDATE);
}
}
}
领域关系的维护
- 依赖(局部变量)
- 关联(全局变量)
- 聚合(整体与部分,部分可以单独存在)
- 组合(整体与部分,部分不能单独存在)
- 聚合、组合、关联关系在实现上的表现基本上是一个类(或者类的标识)作为另一个类的属性;而依赖关系则是一个类作为另一个类在方法的实现上的参数、变量,为另一个类提供功能实现。
领域建模
名词-形容词-动词法来进行建模
名称:领域名称
形容词:领域状态
动词:领域操作
比如:我们要创建一个订单,然后支付
那么 我们需要一个订单领域, 订单有各种属性,有创建和支付的操作。
领域服务
我们使用领域服务来封装不属于领域模型或者领域模型公共的业务规则,
当领域中的某个重要的过程或转换操作不属于实体或值对象的自然职责时,应该在模型中添加一个作为独立接口的操作,并将其声明为Service
public class DesignerOrderWorkflowService {
private DesignerOrderWorkflowService() { }
private static Map<DesignerOrderState, DesignerOrderState[]> states = new HashMap<>();
static {
states.put(DesignerOrderState.NEW, new DesignerOrderState[]{ DesignerOrderState.MEASURED, DesignerOrderState.ABORTED });
states.put(DesignerOrderState.MEASURED, new DesignerOrderState[]{ DesignerOrderState.QUOTED, DesignerOrderState.ABORTED });
states.put(DesignerOrderState.QUOTED, new DesignerOrderState[]{ DesignerOrderState.ACCEPT_QUOTE, DesignerOrderState.REJECT_QUOTE, DesignerOrderState.ABORTED });
states.put(DesignerOrderState.REJECT_QUOTE, new DesignerOrderState[]{ DesignerOrderState.QUOTED, DesignerOrderState.ABORTED });
states.put(DesignerOrderState.ACCEPT_QUOTE, new DesignerOrderState[]{ DesignerOrderState.PAID, DesignerOrderState.ABORTED });
states.put(DesignerOrderState.PAID, new DesignerOrderState[]{ DesignerOrderState.REFUND, DesignerOrderState.COMPLETION });
states.put(DesignerOrderState.COMPLETION, new DesignerOrderState[]{ DesignerOrderState.FEEDBACK });
states.put(DesignerOrderState.ABORTED, new DesignerOrderState[]{ DesignerOrderState.FEEDBACK });
states.put(DesignerOrderState.REFUND, new DesignerOrderState[]{ DesignerOrderState.FEEDBACK });
states.put(DesignerOrderState.FEEDBACK, new DesignerOrderState[]{ DesignerOrderState.FEEDBACK }); // 允许多次评价
}
public static boolean canChangeState(DesignerOrderState state, DesignerOrderState nextState) {
Assert.notNull(state, "The state can not be null.");
Assert.notNull(nextState, "The nextState can not be null.");
DesignerOrderState[] nextStates = states.get(state);
for (DesignerOrderState possibleNextState : nextStates) {
if (possibleNextState.equals(nextState)) {
return true;
}
}
return false;
}
public static boolean canAbort(DesignerOrder order) {
return canChangeState(order.getState(), DesignerOrderState.ABORTED);
}
public static DesignerOrderState changeState(long orderId, DesignerOrderState state, DesignerOrderState nextState) {
if (!canChangeState(state, nextState)) {
BusinessException.throwException(DomainExceptionMessage.STATE_CHANGE_ILLEGAL_CODE, DomainExceptionMessage.STATE_CHANGE_ILLEGAL, orderId, state, nextState);
}
return nextState;
}
public static boolean isCompleted(DesignerOrder order) {
return order.getState() == DesignerOrderState.ABORTED ||
order.getState() == DesignerOrderState.REFUND ||
order.getState() == DesignerOrderState.COMPLETION ||
order.getState() == DesignerOrderState.FEEDBACK;
}
}
聚合
问题: 比如一个采购订单有多个订单项,有一个相同的校验规则(采购订单内商品的总数量不能超过100),现在有2个用户在操作同一个订单,第一个采购员增加了订单项1的数量,第二个采购员增加了订单项2的数量,同时进行了提交,各自的操作都能成功,导致该订单商品的总数量超过100,违反了 业务规则。
解决:
聚合是一组相关对象的集合,作为数据修改的单元,在整个生命周期中满足固定的业务规则。
将订单和订单项做为1个聚合而存在,订单为聚合根,外部对象只能通过聚合根进行访问。这样,2个采购员在增加各自的订单项时,操作的是同1个订单对象,第1个可以成功,第二个提交时就会显示违反业务规则,从而提交失败。
这样可以保证订单和订单项的数据一致性
乐观离线锁
问题:
客户Customer,它有三个属性:标识Id,名称Name,描述Description,其中一条数据为:Id=1,Name=”a”,Description=”Hello”。
现在张三把Id为1的客户编辑界面打开,然后就吃饭去了。
李四对Id=1的客户进行编辑,修改了Name为“b”,保存成功。
张三吃完饭回来,继续干活,他把Description改成”Haha”,保存之后,李四修改的Name=”b”又变回Name=”a”,李四的工作白干了。
丢失更新是严重的数据修改错误,应该坚决避免。
解决:
乐观离线锁通过为每行数据添加一个版本号来识别当前数据的版本,在获取数据时将版本号保存下来,更新数据时将版本号作为Where中的过滤条件,如果该记录被更新,则版本号会发生变化,所以导致更新数据时影响行数为0,通过引发一个并发更新异常让你了解数据已经被别人更新
充血模型和贫血模型
贫血模型:只包含set/get方法的实体对象
充血模型:包含完整领域操作功能的实体对象
领域层的实现由聚合构成,每一个聚合通常包含了聚合根和领域模型实现、Service、工厂、Repository、领域异常等