实体
传统MVC设计场景下,首先考虑的是数据的属性(对应数据库的列)和关联关系(外键关联)而不是富有行为的领域概念,会导致表示领域模型的实体包含大量的getter和setter方法,虽然不是什么大错,但这却不是DDD的做法。
为什么使用实体
一个实体是一个唯一的东西,并且可以在相当长的一段时间内持续的变化,并且会对其多次修改,但由于他们拥有相同的身份标识,它们依然是一个实体。唯一的身份标识和可变性特征将实体对象和值对象(Value Object)区分开来。
有时实体不见得是一种适当的建模工具,更适合建模成值对象。
我们 通过标识对对象进行区分,而不是属性,此时我们应该将标识作为主要的模型定义。同时我们需要保持简单的类定义,并且关注对象在其生命周期中的连续性和唯一性标识。我们不应该通过对象的状态形式和历史来区分不同的实体对象,对于什么是相同的东西,实体模型应该给出定义。
唯一标识
在设计实体时,我们首先需要考虑实体的本质特征,特别是实体对唯一标识和对实体的查找,而不是一开始便关注实体的属性和行为。只有在对实体的本质特征有用的情况下,才加入相应的属性和行为。将唯一标识用于实体匹配通常取决于标识的可读性。
以下是一些常用的创建实体身份标识的策略,从简单到复杂依次是:
-
用户提供一个或多个初始唯一值作为程序输入,程序应该保证这些初始值是唯一的。
-
程序内部通过某种算法自动生成身份标识,此时可以使用一些类库或者框架,当然程序自身也可以完成这样的功能。
-
程序依赖于持久化存储,比如数据库,来生成唯一标识。
-
另一个限界上下文(系统或程序)已经决定出了唯一标识,这作为程序的输入,用户可以在一组标识中进行选择。
用户提供唯一标识
让用户手动地输入对象标识看起来是一种很直接的作为,但这种方法也可能变得很复杂。复杂之一便是需要用户自己生产高质量的标识,此时标识可能是唯一的,也可能是不正确的。在大多数情况下唯一标识是不变的,但如果需要提供修改方法,就需要考虑后续的一系列验证问题。
应用程序生成唯一标识
有很多可靠的方法都可以自动生成唯一标识,但是如果在集群环境或者分布在不同的节点中,可以使用UUID或GUID来生成完全唯一的标识。
UUID是一种快速生成唯一标识的方法,不需要与外界进行交互。对于有性能要求的领域来说,我们可以将UUID实例缓存起来,使其在背后不间断地向缓存中填入新的UUID值。根据UUID能够表达实体的唯一程度,我们可以只使用UUID中的一部分来标记实体。
在聚合边界之内,我们可以将缩短后的标识作为实体的本地标识。而另一方面聚合根的实体则需要全局的唯一标识。
持久化机制生成唯一标识
我们向数据库获取一个序列值或递增值,结果总是唯一的。根据标识的所需范围,数据库可以生成2字节、4字节和8字节的唯一标识。
性能可能是这个方法的一个缺点,从数据库中获取标识比直接从应用程序中生成标识要慢的多。如果可以使用延迟生成的方式,那么缓存标识就不是什么问题了。以mysql为例:
<id name="id" type="long" columm="product_id">
<generator class="native"/>
</id>
另一个限界上下文提供唯一标识
如果另一个限界上下文用于给实体标识赋值,那么我们需要对每一个标识进行查找、匹配和赋值。其中最重要的是精确匹配,此时用户需要提供一种或多种属性,比如账户、email等。
在这种方式中,对象同步可能是个问题。外部对象的改变将如何影响本地对象?这个可以通过事件驱动架构和领域事件予以解决。
标识生成时间
实体唯一标识的生成即可以发生在对象创建的时候,也可以发生在持久化对象的时候。在生成标识的时候,我们需要知道将哪些因素考虑在内。
值对象
领域服务
领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务。当某个操作不适合放在聚合和值对象上时,为了避免过程式的编程方式,最好的方式便是使用领域服务来实现该操作。
什么是领域服务?
当领域中的某个操作过程或转换过程不是实体或者值对象的职责时,我们应该将该操作放在一个单独的接口中,即领域服务。请确保该领域服务和业务、技术通用语言是一致的;并且保证它是无状态的;并且能够明确地表达限界上下文中的通用语言。
另外一种解释:
在战术建模当中,并非所有模型都是事物。有些模型是对 领域中的一些行为操作进行建模。此类模型我们称之为领域服务。
我们希望在领域设计当中统一用模型对象进行交互。此时领域服务使用细粒度的领域对象如实体或者值对象进行交互,在服务内部描述领域知识得出结果并将其返回(即领域服务的参数和返回类型应该是领域对象)。
通常来说,领域模型主要关注于某个特定于某个领域的业务,同样,领域服务也具有相似的特点,以下是需要使用领域服务的三个特征:
- 它是与领域相关的操作,如执行一个显著的业务操作过程,但它又并不适合放入实体与值对象中。
- 对领域对象进行转换,或以多个领域对象作为输入进行计算,结果产生一个值对象
- 操作是无状态的
区分不同的服务
在传统的开发中我们已经有 Service 服务的概念了,这时候再引入领域服务时,我们可能就会开始混淆。 在领域驱动设计中我们主要将服务分为三类,一类是应用服务,一类是领域服务,一类是基础服务。
如何去区分这三种服务呢?简单的理解是通过服务自身所服务的客户端来进行区分。
- 应用服务提供面向用户的服务,它所完成的是一整个用户需求。
- 领域服务提供面向应用层的服务,它所完成的是封装领域知识,供应用层使用。
- 基础服务提供面向应用层和领域层的服务,它所提供的是项目中各个层都可能使用到的通用功能。
我们举一个银行转账的例子,通过不同服务所处理的事情来说明
- 应用服务:获取输入,发送消息给领域层,监听确认消息,决定使用基础服务来发送邮件。
- 领域服务:协调账户模型和总账模型进行交互,执行相应的领域行为。
- 基础服务:按照应用服务的指示发送邮件。
是否需要一个领域服务
请不要倾向于将一个领域概念建模成领域服务,而是只有在有必要的时候才这么做。过度地使用领域服务将导致贫血领域模型,即所有的业务逻辑都位于领域服务中。
领域服务使用案例分析
场景:
我们需要对一个 User 进行认证(不能使用明文密码),并且只有当 Tenant 处于激活状态时,我们才会通过认证。
V1
从应用服务层来讲,我们的实现可能如下:
boolean authentic=false;
User user = DomainRegistry.userRepository().userwithUsername(aTenantId,ausername);
if (user != null){
authentic= user.isAuthentic(aPassword);
}
return authentic;
对于以上设计,存在如下几个问题。
- 应用服务层需要知道某些认证细节,他们需要找到一个User,然后再对该User进行密码匹配。
- 不能显式地表达通用语言。这里,我们询问的是一个Usr“是否被认证了”,而没有表达出“认证”这个过程。在有可能的情况下,我们应该尽量使建模术语直接地表达出团队成员的交流用语。
- 缺少了“检查Tenant是否处于激活状态”这个前提条件。如果一个User所属的Tenant处于非激活状态,我们便不应该对该User进行认证。
V2
//应用层
public boolean authenticate(String aTenantId,String aUsername,String aPassword) {
boolean authentic = false;
//查找租户信息
Tenant tenant = tenantRepository.tenantofId(aTenantId);
if (tenant != null && tenant.isActive()){
//查找租户下的用户信息
User user = userRepository.userWithUsername(aTenantId,aUsername);
if(user != null){
//在租户业务范围下,判断用户是否可以授权
authentic = tenant.authenticate(user,aPassword);
}
}
return authentic;
}
这种方式也存在一些问题:
- 应用服务层需要知道更多的认证细节,而这些是应用服务层可以不感知的东西。
- 我们可以将Tenant 的isActive() 方法放在 authenticat() 方法中,减少应用服务层需要感知的认证细节。但这并不是一个显式的模型行为。同时,这将带来另外一个问题,即此时的Tenant需要知道如何对密码进行操作。
V3
对于以上解决方案,我们似乎给模型带来了太多的问题。对于V2,
我们可以从如下几种方面去思考解决方案:
-
在Tenantr中处理对密码的加密,然后将加密后的密码传给User。这种方法违背了单一职责原则。
-
由于一个User 必须保证对密码的加密,它可能已经知道了一些加密信息。如果是这样,我们可以在Usr上创建一个方法,该方法对明文密码进行认证。
-
Tenant依赖于User对密码进行加密,然后将加密后的密码与原有密码进行匹配。这种方法在对象协作之间增加了额外的步骤。此时,Tenant依然需要知道认证细节。
-
让客户端对密码进行加密,然后将其传给Tenant。这样导致的问题在于,应用服务层承载了它本不应该有的职责。
以上这些方法都会带来一些问题,应用服务层处理逻辑变的复杂、应用服务负责了对身份与访问权限的管理等。
//应用层
public boolean authenticate(String aTenantId,String aUsername,String aPassword) {
UserDescriptor userDescriptor = authenticationservice()
.authenticate(aTenantId,aUsername,aPassword);
}
public class UserDescriptor implements Serializable{
private String emailAddress;
private TenantId tenantId;
private String username;
public UserDescriptor(TenantId aTenantId,String aUsername,String anEmailAddress){
....
}
}
应用服务只需要获取到一个无状态的AuthenticationService,然后调用它的authenticate()方法即可。将所有的认证细节放在领域服务中,而不是应用服务。
在需要的情况下,领域服务可以使用任何领域对象来完成操作,包括对密码的加密过程。客户端不需要知道任何认证细节。
为领域服务创建一个迷你层
一个方法是放入领域对象还是放入领域服务有时候会是一个比较困难的选择。我们可能希望在实体和值对象之上创建一个领域服务的迷你层,这样简化了分析的工作。
这样做可能会导致贫血领域模型这种反模式。但是,对于有些系统来说,为领域服务创建一个不至于导致贫血领域模型的迷你层是值得的。当然,这取决于领域模型的特征。
如果决定为领域服务创建一个迷你层,请注意这样的迷你层和应用层中的服务是不同的。在应用服务中,我们关心的是事务和安全,但是这些不应该出现在领域服务中。