《实现领域驱动设计》系列(1) DDD入门

时间:2022-02-23 04:47:51

最近在学习Vaughn Vernon所著的《实现领域驱动设计》,发现这本身对我们如何设计一个产品及产品的实现过程有一定的指导作用。


1.DDD概述

领域驱动设计(DDD)作为一种软件开发方法,它可以帮助我们设计高质量的软件模型(软件开发模型是指软件开发全部过程、活动和任务的结构框架。软件开发包括需求、设计、编码和测试等阶段,有时也包括维护。软件开发模型能清晰、直观地表达软件开发全过程,明确规定了要完成的主要活动和任务,用来作为软件项目的基础)。领域模型的设计结果达到更专业的团队合作,更高效的开发过程,更贴近业务需求的实现的效果。


2.为什么需要DDD

真正的业务价值是能够很好地符合业务战略,并且可以将竞争优势融合到解决方案中。但在软件开发中我们常会遇到以下问题:

(1)业务人员所想映射到开发者所理解,不能完全反映出领域专家的思维模型。

(2)领域专家间意见不统一。

(3)软件技术实现错误地改变软件业务的规则。

注:领域专家并不是一个职位,他可以是精通业务的任何人。他们可能了解更多的关于业务领域的背景知识,他们可能是软件产品的设计者,甚至有可能是销售员。

DDD针对以上三个问题,提出了3个解决方案:

(1)创建通用语言。通用语言指产品组每位伙伴都可以理解的技术术语、业务术语等。

(2)服务于业务战略方向。DDD的战略设计用于清楚地界分不同的系统和业务关注点(即功能),这样可以保护每个业务层面的服务。

(3)使用战术设计建模工具,这些战术设计工具使开发人员能够按照领域专家的思维模型开发软件。

使用DDD的原则:在最重要最复杂的业务场景下使用,对于那些可以轻易替换或业务流程并不复杂的系统来说,不建议使用。


3.简单Deom:贫血领域对象

软件业中有很多开发者都是学着示例代码做开发的,这并不是什么坏事,只要示例代码本身是好的。然而,通常情况下是,示例代码只是用尽可能简单的方式来展示某个特定的概念或者API特性,而并不强调要遵循好的设计原则。一些极度简化的示例代码总是包含了大量的getter和setter,于是这些getter和setter随着示例代码每天被程序员们原封不动地来回复制,当然还有一些历史原因,如JavaBean的标准,这就不赘言了。如果你的领域对象中主要是些公有的getter和setter方法,并且几乎没有业务逻辑,或者即使有业务逻辑,也多数情况下需要调用那些getter和setter,那么这个对象就被称为贫血领域对象。

比如有这么一个业务场景:我们现在开发使用的是一个Scrum模型,我们需要将一个待定项(Backlog Item)提交到冲刺(sprint)中,有以下代码:

public class BcaklogItem1 extends Entity{
	
	private SprintId sprintId;
	private BcaklogItemStatusType status;
	...
	public void setSprintId(SprintId sprintId) {
		this.sprintId = sprintId;
	}

	public void setStatus(BcaklogItemStatusType status) {
		this.status = status;
	}
	...
}
客户端代码如下:

	bcaklogItem.setSprintId(sprintId);
	bcaklogItem.setStatus(BcaklogItemStatusType.COMMITTED);
以上例子采用的是以数据为中心的方式,此时客户代码必须知道如何正确地将一个待定项提交到冲刺中。这样的模型是不能被称为领域模型的。如何客户代码错误的修改了sprintId,而没有修改status会发生什么呢?或者,如果在将来有另外一个属性需要设值时又该怎么办?我们需要认真分析客户代码来完成从客户数据到BacklogItem属性的映射。这种方式同时也暴露了BacklogItem的数据结构,并且将关注点集中在数据属性上,而不是对象行为,因为这里的getter和setter并没有真正的业务价值。


4.贫血领域对象的解决思路

使用领域对象的行为,这种行为表达除了领域中的通用语言,将以上例子代码修改如下:

public class BcaklogItem2 extends Entity{	
	private SprintId sprintId;
	private BcaklogItemStatusType status;
	...
	
	public void commitTo(Sprint aSprint){
		//检测该待定项是否已在发布计划中
		if(!this.isScheduledForRelease()){
			throw new IllegalStateException("Must be scheduled for release to commit to sprint.");
		}
		
		//检测该待定项是否已在另外的冲刺中,如果在则回收
		if(this.isCommittedToSprint()){
			if(!aSprint.sprintId().equals(this.sprintId())){
				this.uncommitFromSprint();
			}
		}
		
		this.elevateStatusWith(BcaklogItemStatusType.COMMITTED);
		this.setSprintId(aSprint.sprintId());
		//对外发布事件
		DomainEventPublisher.instance().publish(new BacklogItemCommitted(
				this.tenant,this.backlogItemId(),this.sprintId()));
		...
	}
}	
客户端代码如下:

bcaklogItem.commitTo(sprint);

该例子中,客户代码并不需要知道提交BacklogItem的实现细节,实现代码所表达的逻辑恰能描述业务行为。我们很容易地添加几句代码,就可确保在发布计划之外的待定项是不能被提交的。


以上是DDD入门的一些简介、术语和Demo,粗略的介绍了DDD的概念和基本应用,接下来的文章会详细的介绍DDD个方面,今天到此为止啦。