领域驱动设计(五) - 战术设计 - 【3/3】聚合与模型的构建方法

2022年05月12日 阅读数:3
这篇文章主要向大家介绍领域驱动设计(五) - 战术设计 - 【3/3】聚合与模型的构建方法,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

这一小章主要阐述下如何组织上述分析后的模型。java

使用聚合(Aggergate)进行建模,而且在设计中结合工厂(Factory)和资源库(Repositiory,注意Orm映射出的持久化对象不是领域模型的范围,在后续章节中会详细阐述这二者的区别),这样就可以把生命周期作为一个原子单元进行模型对象的操做。 编程

一般,Aggergate用于划分范围,这个范围内的模型元素在生命周期各个阶段都应维护其固定规则和事务一致性 。Factory和Repositiory在Aggergate基础上进行操做,将特定生命周期转换的复杂性封装起来。下图是一张总体构建思路:设计模式

领域驱动设计(五) - 战术设计 - 【3/3】聚合与模型的构建方法_资源库

  • 聚合Aggregate:定义清晰的关联项和边界
  • 工厂Factory:建立和重建复杂对象,并用aggergate来封装其内部结构
  • 资源库Repository:提供检索和持久化,能够委托给Factory来建立对象

1、聚合

聚合的识别和建立已在《快速开始-战术设计》章节详细描述过,此处不在累述,在本小节中笔者试图从概念角度讲述这个主题,但愿能够加深读者对聚合的理想。首先须要强调的一点,在DDD中聚合是一个可选的模块,实现时须要在代码复杂性和是否应用聚合之间作出权衡。安全

聚合经过定义清晰的关系避免混乱和保持模型的高内聚。每一个聚合都包含一个特定的根实体(聚合根)和边界(内部都有什么属性)。session

  • 对外引用:聚合根是惟一容许外部对象保持对聚合引用的对象,经过他来控制外界对内部成员的访问,不能根引用绕过它来修改内部对象。这种设计有利于维护业务规则的完整性,设计时尽可能减小非必要的关联,有助于对象间的遍历和复杂性;
  • 聚合内部:在聚合内部对象之间则能够相互引用,不一样的对象能够有惟一的标识,但只有在内部才能区分开,对外部是不可见的。

1.一、建立聚合需遵循的原则

  • 在聚合边界内保护业务规则不变性:由业务最终决定,技术处理上要保证事务的一致性;
  • 聚合要设计的小巧:职责单一性,这是一个决策问题;
  • 只能经过标识符引用其它聚合:出于容器的考虑,聚合间最好是外键引用,而不是实体引用,同时尽可能消除冗余字段,这也是一个决策问题;
  • 使用最终一致性更新其它聚合:避免复杂性,最经常使用的是异步和消息机制;

1.二、程序实现时的注意事项

1.2.一、建立行为

  • 迪米特法则:强调了最小知识原则,任何对象的任何方法只能调用:一、对象自己;二、所传入的参数对象;三、它所建立的对象;四、自身包含的对象的方法;
  • 告诉而非询问原则:客户端不该该先询问执行端再决定是否进行某种操做,这个询问应该由执行端来决定,它只需告诉客户端0和1便可;

1.2.二、并发控制

  • 为聚合建立版本号,但通常没有必要,比较复杂。

1.2.三、避免依赖注入

  • 这主要是出于性能考虑,不要在聚合中注意资源库和领域服务;可在调用聚合命令以前先查询此聚合命令执行的前置条件,而后再执行的方式来解决。若是须要其它引用,可把其它聚合以参数的方式传入到聚合中的方法中;

2、工厂

若是建立实体或聚合的过程过于复杂,在程序设计时有必要将建立职责剥离出来,而后分配给一个单独的对象为领域对象减负,该对象自己并不承担领域模型中的职责,可是依然是领域设计的一部分。由此对象提供一个建立对象的方法/接口,该接口封装了全部建立对象的复杂操做过程,一次性地建立整个聚合,而且确保它的不变条件获得知足。此过程还要作到用户无感知。mybatis

这个被剥离出来的对象就是工厂,概念上讲工厂不受限界上下文的约束;因此不建议建立单独的Factory类,最好的方式是在领域服务(受限界上下文限制)或聚合中添加工厂方法。另外工厂方法自己是可替换的,因此在工厂方法实现时不建议作防护编程以及任何业务规则 ,这个职责应该由各个领域模型来分担,但被建立对象的完整性检查应该由工厂负责。使用用工厂方法除了能保证安全性等,还能简化客户端调用。通用的工厂设计以下:架构

领域驱动设计(五) - 战术设计 - 【3/3】聚合与模型的构建方法_聚合_02

2.一、建立工厂方法的方式

这里说的工厂不是软件设计模式中的工厂模式,是一种概念其意义在于解耦,只要知足这个条件的实现均可以认为是工厂,工厂的实现须要知足两个条件:建立方法必须是原子的;建立的东西最好是具体的;工厂通常有三种建立方式:并发

  • 工厂方法:
  • 抽象工厂:
  • 构建函数:

2.二、聚合中的工厂方法

通常适用于建立被引用的聚合,且建立过程当中与其它上下文没有太多关系;下面是一个简单的示例,在Calendar聚合类中建立CaendarEntry聚合。但聚合里不建议访问领域服务;框架

public class Calendar extends EventSourcedRootEntity {
public CalendarEntry scheduleCalendarEntry(
CalendarIdentityService aCalendarIdentityService,
String aDescription,
String aLocation,
Owner anOwner,
TimeSpan aTimeSpan,
Repetition aRepetition,
Alarm anAlarm,
Set<Participant> anInvitees) {

CalendarEntry calendarEntry =
new CalendarEntry(
this.tenant(),
this.calendarId(),
aCalendarIdentityService.nextCalendarEntryId(),//不建议在工厂方法中访问领域服务
aDescription,
aLocation,
anOwner,
aTimeSpan,
aRepetition,
anAlarm,
anInvitees);

return calendarEntry;
}
}

若是因为特殊缘由须要要在聚合中访问service,则建议把上面的代码优化成以下方式。异步

public class Calendar extends EventSourcedRootEntity {
public CalendarEntry scheduleCalendarEntry(
CalendarIdentityService aCalendarIdentityService,
String aDescription,
String aLocation,
Owner anOwner,
TimeSpan aTimeSpan,
Repetition aRepetition,
Alarm anAlarm,
Set<Participant> anInvitees) {

CalendarEntry calendarEntry =
new CalendarEntry(
this.tenant(),
this.calendarId(),
getProductgetProduct(productId),//***
aDescription,
aLocation,
anOwner,
aTimeSpan,
aRepetition,
anAlarm,
anInvitees);

return calendarEntry;
}

public Product getProduct(Long productId){
return new AggregateService().getProduct(productId);
}
}

2.三、领域服务中的工厂方法

适合建立顶层聚合或是建立过程当中须要与其它上下文进行通讯。最好的方式是在领域服务(受限定上下文限制)中使用工厂方法。瓜熟蒂落的也能够处理防腐层、发布语言、开放主机服务等相关内容。从是否使用工厂方法的维度来分,领域服务也可分为工厂和职能两大类。下面是一个简单的示例:

public class CalendarApplicationService {

private CalendarRepository calendarRepository;
private CalendarEntryRepository calendarEntryRepository;
private CalendarIdentityService calendarIdentityService;
private CollaboratorService collaboratorService;

public void scheduleCalendarEntry(
String aTenantId,
String aCalendarId,
String aDescription,
String aLocation,
String anOwnerId,
Date aTimeSpanBegins,
Date aTimeSpanEnds,
String aRepeatType,
Date aRepeatEndsOnDate,
String anAlarmType,
int anAlarmUnits,
Set<String> aParticipantsToInvite,
CalendarCommandResult aCalendarCommandResult) {

Tenant tenant = new Tenant(aTenantId);

Calendar calendar =
this.calendarRepository()
.calendarOfId(
tenant,
new CalendarId(aCalendarId));

CalendarEntry calendarEntry =
calendar.scheduleCalendarEntry(
this.calendarIdentityService(),
aDescription,
aLocation,
this.collaboratorService().ownerFrom(tenant, anOwnerId),
new TimeSpan(aTimeSpanBegins, aTimeSpanEnds),
new Repetition(RepeatType.valueOf(aRepeatType), aRepeatEndsOnDate),
new Alarm(AlarmUnitsType.valueOf(anAlarmType), anAlarmUnits),
this.inviteesFrom(tenant, aParticipantsToInvite));

this.calendarEntryRepository().save(calendarEntry);

aCalendarCommandResult.resultingCalendarId(aCalendarId);
aCalendarCommandResult.resultingCalendarEntryId(calendarEntry.calendarEntryId().id());
}
}

3、资源库

资源库一般表示一个安全的存储区域,原则上只为聚合建立资源库,他们之间存在一对一的关系。在设计时要考虑如何向资源库中添加行为、层级资源库的设计、资源库与Dao在概念上的区别等;这只是一种设计原则,具体仍是要根据软件的复杂度来评判。概念上资源库不是Dao,但在代码实现上又很是相似,因此资源库的设计实现每每是DDD实践过程当中打破领域设计的最大风险。

另外在设计时简单的业务可能只须要简单的存储便可,即Dao能够直接使用领域对象,复杂的可能须要用到按聚合建立资源库这种设计模式。因此在创建资源库时有面向集合(collection-oriented)和面向持久化(persistence-oriented)两种设计方式。

在平衡性能与聚合的大小时,当不易经过遍历的方式来访问某些聚合的时候,就须要使用这资源库,因此资源库常常被设计成用来查找而不是更新。

3.一、资源库设计的注意事项

  1. 资源库是领域的概念;
  2. 不一样的聚合有不一样的资源库,两个或多个聚合位于同一个对象层级时能够共享同一个资源库
  3. 在第二条成立的前提下,比较合适选用适配器架构 ;
  4. 资源库的设计要体出可替换的原则,程序表现为资源库的接口定义在领域层,实现拿到基础层实现(注意与适配器架构的区别);

3.二、面向集合的资源库设计

一个资源库应该模拟一个SET集合。不管采用什么类型的持久化机制,咱们都不该该容许屡次添加同一个聚合实例。另外,当从资源库中获取到一个对象并对其进行修改时,咱们并不须要从新保存此对象到资源库中。

在一个聚合中不该该存在一个显示的持久化接口调用,而应该用相似Hibernate那种会话机制实现隐式读时复制或隐式写时复制的方式。前者是读时建立一个副本当客户端提交时,对比这两个版本决定是否更新到DB中;后者是利用一个委托对象管理持久化对象,当持久化对象首次被调用时生成一个副本。

public interface GroupRepository {
public void add(Group aGroup);
public Collection<Group> allGroups(TenantId aTenantId);
public Group groupNamed(TenantId aTenantId, String aName);
public void remove(Group aGroup);
}
public class HibernateGroupRepository
extends AbstractHibernateSession
implements GroupRepository {

public HibernateGroupRepository() {
super();
}

@Override
public void add(Group aGroup) {
try {
this.session().saveOrUpdate(aGroup);
} catch (ConstraintViolationException e) {
throw new IllegalStateException("Group is not unique.", e);
}
}
}

3.三、面向持久化的资源库设计

若是持久化技术框架不支持对象变化的跟踪,不管是显式仍是隐匿的,那么采用面向集全资源库便不合适了,此时最好采用基于操做的资源库—面向持久化的资源库,这是现阶段广泛采用的方式,实现技术多为mybatis。在向数据存储中添加新建对象或修改时,必须显示的调用save方法,

和面向集合的设计的一个区别就是,是否须要显示调用save方法。

3.四、资源库设计时的事务的管理

相对来讲领域模型和领域层的操做比较细粒度,事务的管理应该是以粗粒度的方式放在应用层中比较合适,实现时能够硬编码也能够采用声明的方式,可是在分布式系统中,不建议过多的使用事务,而是采用最终一致性设计,示例以下:

public class AccessApplicationService {
@Autowired
private GroupRepository groupRepository;
@Autowired
private UserRepository userRepository;
public AccessApplicationService() {
super();
}
@Transactional
public void assignUserToRole(AssignUserToRoleCommand aCommand) {
//领域模型
TenantId tenantId = new TenantId(aCommand.getTenantId());
User user =
this.userRepository().userWithUsername(
tenantId,
aCommand.getUsername());
if (user != null) {
Role role = this.roleRepository().roleNamed(
tenantId,
aCommand.getRoleName());

if (role != null) {
role.assignUser(user);
}
}
}

@Transactional(readOnly=true)
public boolean isUserInRole(
String aTenantId,
String aUsername,
String aRoleName) {

User user = this.userInRole(aTenantId, aUsername, aRoleName);

return user != null;
}
}


3.五、资源库VS数据访问对象(DAO)

虽然Dao和面向集合持久化的资源库在代码实现上有多是是同样的,但必定要弄清它们之间的概念区别,由于它会影响咱们的程序设计:

  • Dao是针对数据的,它只是对DB的一层封装。而资源库更加面向对象;
  • 资源库是领域模型的一部分,而Dao并不领域范畴内的东西;
  • 以上定位会影响包结构的设计以及调用关系。好比Dao一般会把接口和实现单独管理,而资源库则会用领域层来管理资源库;

END,至此DDD中相关的概念和基础知识所有讲解了,后续笔者会侧重实践落地,分专题讲述下笔者在实践DDD中关于架构设计、考核、流程制定等相关的内容。