实体(Entity,又称为Reference Object)很多对象不是通过他们的属性定义的,而是通过一连串的连续事件和标识定义的。主要由标识定义的对象被称为ENTITY。
传统开发人员总将关注点放在数据,而不是领域。因为在软件开发中,DB占据主导地位。首先考虑的是数据的属性(即数据库的列)和关联关系(外键关联),而不是富有行为的领域概念。导致将数据模型直接反映在对象模型,那些表示领域模型的实体(Entity)被包含了大量getter/setter。虽然在实体模型中加入getter/setter并非大错, 但这不是DDD的做法。
实体最主要有两点特征,一是唯一标识,二是连续性。
唯一标志: 当一些对象不是由属性定义,而是由一个唯一标志定义的话,我们就可以认为它是一个实体。好比我们不能通过一个人的外在特征去唯一定位一个人,因为人从小到大,从年轻到衰老其外在特征都是在改变的。而身份证号码可以贯穿一个人的一生而不发生变化。而且唯一标识不一定仅有一个属性表示,有可能通过多个属性标识某一个唯一对象,就好比数据库中的联合外键(可能有根据电话以及姓名唯一确定一个实体的情况)。
连续性: 对象的连续性体现在对象是有生命周期的。在这个生命周期内,对象内的属性可能是变化着的。好比银行账户表,它就属于一个实体,用户的银行卡号可以唯一的确定一个人的账户,而账户的内的余额随着时间变化(利息)或者随着交易变化。
DDD的不同设计过程,实体的形态也不同。
业务形态
在战略设计时,实体是领域模型的一个重要对象。领域模型中的实体是多个属性、操作或行为的载体。
事件风暴中,可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。
实体和值对象是组成领域模型的基础单元。
代码形态
实体的表现形式是实体类,该类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在DDD里,这些实体类通常采用充血模型,与该实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。
运行形态
实体以DO(领域对象)形式存在,每个实体对象都有唯一ID。可以对实体做多次修改,所以一个实体对象可能和它之前状态存在较大差异。但它们拥有相同的身份标识(identity),所以始终是同一实体。
比如商品是商品上下文的一个实体,通过唯一的商品ID标识,不管这商品的数据(比如价格)如何变,商品ID不会变,始终是同一商品。
数据库形态
DDD是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象。
在领域模型映射到数据模型时,一个实体可能对应0个、1个或者多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。在某些场景中,有些实体只是暂驻静态内存的一个运行态实体,它不需要持久化。比如,基于多个价格配置数据计算后生成的折扣实体。
有些复杂场景,实体与持久化对象可能是一对多或多对一:
一对多:用户user与角色role两个持久化对象可生成权限实体,一个实体对应两个持久化对象
多对一:有时为避免DB的联表查询,会将客户信息customer和账户信息account两类数据保存至同一张数据库表,客户和账户两个实体可根据需要从一个持久化对象中生成
探索实体的本质 一开始团队便遇到陷阱,在Java代码中建模大量实体-关系。将太多关注点放在数据库、表、列和对象映射上。导致所创建 的模型实际上只是含有大量getter/setter的贫血领域模型。他们应该在DDD 上有更多的思考。那时正值他们将安全处理机制从核心域中分离之际,他们学到了如何使用通用语言来更好地辅助建模。
但如果我们认为对象就是一组命名的类和在类上定义的操作,除此之外并不包含其他内容,那就错了。在领域模型中还可包含很多其他内容。团队讨论和规范文档可以帮助我们创建更有意义的通用语言。到最后,团队可以直接使用通用语言来进行对话,而此时的模型也能够非常准确地反映通用语言。
如果一些特定的领域场景会在今后继续使用,这时可以用一个轻量的文档将它们记录下来。简单形式的通用语言可以是一组术语和一些简单的用例场景。 但是,如果我们就此认为通用语言只包含术语和用例场景,那么我们又错了。在最后,通用语言应该直接反映在代码中,而要保持设计文档的实时更新是非常困难的,甚至是不可能的。
账户实体
package com.xtoon.boot.sys.domain.model.user;
import com.xtoon.boot.common.domain.Entity;
/**
* 账号
*
* @author haoxin
* @date 2021-02-21
**/
public class Account implements Entity {
/**
* accountId
*/
private AccountId accountId;
/**
* 手机号
*/
private Mobile mobile;
/**
* 邮箱
*/
private Email email;
/**
* 密码
*/
private Password password;
/**
* token
*/
private Token token;
public Account(AccountId accountId, Mobile mobile, Email email, Password password, Token token) {
this.accountId = accountId;
this.mobile = mobile;
this.email = email;
this.password = password;
this.token = token;
}
public Account(Mobile mobile, String password) {
this.mobile = mobile;
this.password = Password.create(password);
}
public Account(Mobile mobile, Email email, Password password) {
this.mobile = mobile;
this.email = email;
this.password = password;
}
@Override
public boolean sameIdentityAs(Account other) {
return other != null && accountId.sameValueAs(other.accountId);
}
/**
* 密码是否正确
*
* @param passwordStr
* @return
*/
public boolean checkPassword(String passwordStr) {
return password != null && this.password.sameValueAs(Password.create(passwordStr,password.getSalt()));
}
/**
* 修改密码
*
* @param oldPasswordStr
* @param newPasswordStr
* @return
*/
public void changePassword(String oldPasswordStr,String newPasswordStr) {
if(!checkPassword(oldPasswordStr)) {
throw new RuntimeException("原密码不正确");
}
this.password = Password.create(newPasswordStr,password.getSalt());
}
/**
* 检查token是否有效
* @return
*/
public boolean isTokenValid() {
return this.token != null && this.token.getExpireTime()!= null &&
this.token.getExpireTime().getTime() >= System.currentTimeMillis();
}
/**
* 更新Token
*/
public Account updateToken(String tokenStr) {
this.token = Token.create(tokenStr);
return this;
}
public AccountId getAccountId() {
return accountId;
}
public Mobile getMobile() {
return mobile;
}
public Email getEmail() {
return email;
}
public Password getPassword() {
return password;
}
public Token getToken() {
return token;
}
}
Account实体中accountId是唯一标识,mobile,email等是值对象,实体只能通过构造方法创建,不能有set方法(get方法可以),属性只能通过调用内部方法修改,如checkPassword,changePassword等。