原文地址:https://www.deathearth.com/1238.html
DDD(领域驱动设计)作为一种软件开发方法,它可以帮助我们设计高质量的软件模型。
如果你
有开发卓越软件的激情和毅力;渴望学习和进步;有能力理解软件模式,并懂得如何应用这些模式;有发掘不同设计方法的能力和耐性;勇于改变现状;着重细节,希望亲自试验;希望编写更好的代码。
可以学习并实施DDD
领域模型是什么?
领域模型是关于某个特定业务领域的软件模型。通常,领域模型通过对象模型来实现,这些对象同事包含了数据和行为,并且表达了准确的业务含义。
为什么我们需要DDD
- 使领域专家和开发者一起工作,这样开发出来的软件能够准确地传达业务规则。
- “准确传达业务规则”的意思是说,软件就像领域专家开发出来的一样。
- 可以帮助业务人员自我提高。
- 可以确保软件知识并不是掌握在少数人手中。
- 在领域专业、开发者和软件本身间不存在‘翻译’。大家都是用相同的语言进行交流。
- 设计就是代码、代码就是设计。
- DDD同时提供了战略设计和战术设计两种方式。战略设计帮助我们理解哪些投入是最重要的;战术设计则帮助我们创建DDD模型中各个部件。
通常来说,领域专家将关注点放在交付业务价值上,而开发者则将注意力放在技术实现上。并不是说开发者的动机是错误的,而是说开发者的眼光被自然而然地吸引到了实现层面上。即便让领域专家和开发者一同工作,他们之间的协作也只是表面的,这时在所开发的软件中便产生了一种映射:将业务人员所想的映射到开发者所理解的。这样一来,软件便不能完全反映出领域专家的思维模型。随着时间的推移,这种鸿沟将增加软件的开发成本。随着开发者转到其他项目或离职,本应该驻留在软件中的领域知识也就丢失了。
DDD作为一种软件开发方法,主要关注以下三个方面:
1. DDD将领域专家和开发人员聚集到一起,这样所开发的软件能够反映出领域专家的思维模型。
2. DDD关注业务战略。实现面向服务架构或业务驱动架构
3. 通过使用战术设计建模工具,DDD满足了软件真正的技术需求。这些战术设计工具能使开发人员按照领域专家的思维模型开发软件。
DDD的作用是简化,而不是复杂化。
贫血领域对象实例代码
通过一个代码的改造理解DDD的意图。
第一版本
/**----version1.0---------------------------------------------**/
//不太明确本来意图
//保存客户对象
public void saveCustomer(
String customerId,
String customerFirstName,String customerLastName,
String streetAddress1,String streetAddress2,
String city, String stateOrProvince,
String postalCode,String country,
String homePhone,String mobilePhone,
String primaryEmailAddress,String secondaryEmailAddress
) {
//判断对象是否存在
Customer customer = customerDao.readCustomer(customerId);
if(customer == null) {
customer = new Customer();
customer.setCustomerId(customerId);
}
customer.setCustomerFirstName(customerFirstName);
customer.setCustomerLastName(customerLastName);
customer.setStreetAddress1(streetAddress1);
customer.setStreetAddress2(streetAddress2);
customer.setCity(city);
customer.setStateOrProvince(stateOrProvince);
customer.setPostalCode(postalCode);
customer.setCountry(country);
customer.setHomePhone(homePhone);
customer.setMobilePhone(mobilePhone);
customer.setPrimaryEmailAddress(secondaryEmailAddress);
customer.setSecondaryEmailAddress(secondaryEmailAddress);
//保存对象
customerDao.saveCustomer(customer);
}
第二版本
/**----version2.0---------------------------------------------**/
/**
* "由贫血导致的失忆症"
* 缺点:
* 1. saveCustomer()业务意图不明显
* 2. 方法的实现本身增加了潜在的复杂性
* 3. Customer领域对象根本就不是对象,而只是一个数据持有器。
*/
//保存客户对象
public void saveCustomer1(
String customerId,
String customerFirstName,String customerLastName,
String streetAddress1,String streetAddress2,
String city, String stateOrProvince,
String postalCode,String country,
String homePhone,String mobilePhone,
String primaryEmailAddress,String secondaryEmailAddress
) {
//判断对象是否存在
Customer customer = customerDao.readCustomer(customerId);
if(customer == null) {
customer = new Customer();
customer.setCustomerId(customerId);
}
if(customerFirstName != null) {
customer.setCustomerFirstName(customerFirstName);
}
if(customerLastName != null) {
customer.setCustomerLastName(customerLastName);
}
if(streetAddress1 != null) {
customer.setStreetAddress1(streetAddress1);
}
if(streetAddress2 != null) {
customer.setStreetAddress2(streetAddress2);
}
if(city != null) {
customer.setCity(city);
}
if(stateOrProvince != null) {
customer.setStateOrProvince(stateOrProvince);
}
if(postalCode != null) {
customer.setPostalCode(postalCode);
}
if(country != null) {
customer.setCountry(country);
}
if(homePhone != null) {
customer.setHomePhone(homePhone);
}
if(mobilePhone != null) {
customer.setMobilePhone(mobilePhone);
}
if(secondaryEmailAddress != null) {
customer.setPrimaryEmailAddress(secondaryEmailAddress);
}
if(secondaryEmailAddress != null) {
customer.setSecondaryEmailAddress(secondaryEmailAddress);
}
//保存对象
customerDao.saveCustomer(customer);
}
第三版本
通过阅读代码你便能够理解它的业务意图。还可以通过测试来保证它功能。
/**----version3.0---------------------------------------------**/
/**
* 对Customer模型进行了细分,每一个应用层的方法都对应着一个单一的用例流。
* 这意味着用户界面所反映的用户操作也变得更加狭窄。
* (对具体对象操作按业务进行细分,实际数据操作时,如mybatis的generator生成类可以通用)
*/
public interface Customer{
public void changePersonalName(String firstName,String lastName);
public void postalAddress(PostalAddress postalAddress);
public void relocateTo(PostalAddress changedPostalAddress);
public void changeHomeTelephone(Telephone telephone);
public void disconnectHomeTelephone();
public void changeMobileTelephone(Telephone telephone);
public void disconnectMobileTelephone();
public void primaryEmailAddress(EmailAddress emailAddress);
public void secondaryEmailAddress(EmailAddress emailAddress);
}
public void changeCustomerPersonalName(
String customerId,
String customerFirstName,
String customerLastName) {
Customer customer = customerRespository.customerOfId(customerId);
if(customer == null) {
throw new IllegalStateException("Customer does not exist.");
}
customer.changePersonalName(customerFirstName,customerLastName);
}
使用通用语言来捕捉特核心业务领域中的概念和属于,它是一种团队模式。软件模型包含名词、形容词、动词和一些富有含义的语句等。团队成员通过这些语言进行交流。
关于通用语言的说明
- 这里的‘通用’意思是‘普遍的’,或‘导出都存在的’;通用语言在团队范围内使用,只表达一个单一的领域模型。
- ‘通用语言’并不表示全企业、全公司或全球性的万能的领域语言。
- 限界上下文和通用语言间存在一对一的关系。
- 界限上下文是一个相对较小的概念。刚好能够容纳下一个独立的业务领域所使用的通用语言。
- 只有当团队在一个独立的限界上下文中时,通用语言才通用。
- 虽然只工作在一个限界上下文中,但通常我们还需要和其他限界上下文打交道。这时可以通过上下文映射图进行集成。
DDD所带来的的业务价值
- 获得一个有用的领域模型
- 你的业务得到了更准确的定义和理解
- 领域专业可以为软件设计做贡献
- 更好的用户体验
- 清晰的模型边界
- 更好的企业架构
- 敏捷、迭代式和持续建模
- 使用战略和战术新工具
纯数据模型的优劣对比
示例1: 采用的是以数据为中心的方式,此时调用方必须知道如何正确的将一个待定项提交到BacklogItem对象中。这样的模型是不能成为领域模型的。
public class BacklogItem extends Entity{
private SprintId sprintId;
private BacklogItemStatusType status;
//其他属性
public void setSprintId(String sprintId) {
this.sprintId = sprintId;
}
public void setStatus(BacklogItemStatusType status) {
this.status = status;
}
//其他get \set
}
//调用代码
BacklogItem bl = new BacklogItem();
bl.setSprintId(xxx);
bl.setStatus(xxxx);
示例2:将行为暴露给客户,行为方法的名字清楚地表名了业务含义。
public class BacklogItem extends Entity{
private SprintId sprintId;
private BacklogItemStatusType status;
//其他属性
public void commitTo(Sprint aSprint) {
if(!this.isScheduledForRelease()) {
throw new IllegalStateException("Must be scheduled for xxx");
}
//回收事件
if(this.isCommiteedToSprint()) {
if(!aSprint.sprintId().equals(this.sprintId())) {
this.uncommitFromSprint();
}
}
this.elevateStatusWith(BacklogItemStatus.COMMITTED);
this.setSprintId(aSprint.sprintId());
//事件形式通知调用方
DomainEventPublisher.instance().publish(
new BacklogItemCommitted(
this.tenant(),
this.backlogItemId(),
this.sprintId()
)
);
}
//其他处理
}
//调用代码
BacklogItem bl = new BacklogItem();
bl.commitTo(sprint);
整理自《实现领域驱动设计》