领域对象
领域对象
- 失血模型:是仅包含属性的
getter/setter 方法的数据载体,没有行为和动作,业务逻辑由服务层完成。 - 贫血模型:包括了属性、
getter/setter 方法,和不依赖于持久化的原子领域逻辑,依赖于持久层的业务逻辑将会放到服务层中。 - 充血模型:包含了属性、
getter/setter 方法、大部分的业务逻辑,包括依赖于持久层的业务逻辑,所以使用充血模型的领域层是依赖于持久层,服务层是很薄的一层,仅仅封装事务和少量逻辑。 - 胀血模型:取消了
Service 层,胀血模型就是把和业务逻辑不相关的其他应用逻辑(如授权、事务等)都放到领域模型中。
胀血模型是显而易见不可取的,这里不做过多讨论。失血模型是绝大数企业开发应用的模式,一些火热的
采用领域模型的开发方式,将数据和业务逻辑封装在一起,从服务层移动到领域将业务逻辑模型中,这样服务层可以只负责应用逻辑(事务、日志、认证、监控、编排等
Entity(实体)
每个实体是具有唯一标识的领域概念,并且可以相当长的一段时间内持续地变化。例如实体订单
我们可以对实体做多次修改,故一个实体对象可能和它先前的状态大不相同。但是,由于它们拥有相同的身份标识,他们依然是同一个实体。并且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。
实体的业务形态
在
实体的代码形态
在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在
实体的运行形态
实体以
实体的数据库形态
与传统数据模型设计优先不同,
ValueObject(值对象)
所谓值对象,就是通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体;当我们只关心一个模型元素的属性时,在
建议将值对象设计成一个不变(Immutable)对象,当度量和描述改变时,可以用另外一个值对象予以替换。它可以和其它值对象进行相等性比较,且不会对协作对象造成副作用,也避免了并发带来的冲突等问题。在领域驱动设计中,提倡尽量定义值对象来替代基本类型,因为基本类型无法体现统一语言中的领域概念。假设一个实体定义了许多属性,这些属性都是基本类型,就会导致与这些属性相关的领域行为都要放到实体中,导致实体的职责变得不够单一。引入值对象后情况就不同了,我们可以利用合理的职责分配,将这些职责(领域行为)按照内聚性分配到各个值对象中,这个领域模型就能变得协作良好。值对象可以与其所在的实体对象保存在同一张表中,值对象的每一个属性保存为一列;值对象也可以独立于其所在的实体对象保存在另一张表中,值对象获得委派主键,该主键对客户端是不可见的。
值对象的业务形态
值对象是
在值对象中也有部分共享的标准类型的值对象,它们有自己的限界上下文,有自己的持久化对象,可以建立共享的数据类微服务,比如数据字典。
值对象的代码形态
值对象在代码中有这样两种形态。如果值对象是单一属性,则直接定义为实体类的属性;如果值对象是属性集合,则把它设计为

值对象的运行形态
实体实例化后的
引用单一属性的值对象或只有一条记录的多属性值对象的实体,可以采用属性嵌入的方式嵌入。引用一条或多条记录的多属性值对象的实体,可以采用序列化大对象的方式嵌入。比如,人员实体可以有多个通讯地址,多个地址序列化后可以嵌入人员的地址属性。值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。
- 以属性嵌入的方式形成的人员实体对象,地址值对象直接以属性值嵌入人员实体中。

- 以序列化大对象的方式形成的人员实体对象,地址值对象被序列化成大对象
Json 串后,嵌入人员实体中。

值对象的数据库形态
举个例子,还是基于上述人员和地址那个场景,实体和数据模型设计通常有两种解决方案:第一是把地址值对象的所有属性都放到人员实体表中,创建人员实体,创建人员数据表;第二是创建人员和地址两个实体,同时创建人员和地址两张表。第一个方案会破坏地址的业务涵义和概念完整性,第二个方案增加了不必要的实体和表,需要处理多个实体和表的关系,从而增加了数据库设计的复杂性。
我们可以综合这两个方案的优势,扬长避短。在领域建模时,我们可以把地址作为值对象,人员作为实体,这样就可以保留地址的业务涵义和概念完整性。而在数据建模时,我们可以将地址的属性值嵌入人员实体数据库表中,只创建人员数据库表。这样既可以兼顾业务含义和表达,又不增加数据库的复杂度。
值对象就是通过这种方式,简化了数据库设计,总结一下就是:在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。另外,也有
实体与值对象的关系
值对象是一把双刃剑,它的优势是可以简化数据库设计,提升数据库性能。但如果值对象使用不当,它的优势就会很快变成劣势
值对象采用属性嵌入的方法提升了数据库的性能,但如果实体引用的值对象过多,则会导致实体堆积一堆缺乏概念完整性的属性,这样值对象就会失去业务涵义,操作起来也不方便。实体和值对象是微服务底层的最基础的对象,一起实现实体最基本的核心领域逻辑。值对象和实体在某些场景下可以互换,很多

在领域模型中人员是实体,地址是值对象,地址值对象被人员实体引用。在数据模型设计时,地址值对象可以作为一个属性集整体嵌入人员实体中,组合形成上图这样的数据模型;也可以以序列化大对象的形式加入到人员的地址属性中,前面表格有展示。从这个例子中,我们可以看出,同样的对象在不同的场景下,可能会设计出不同的结果。有些场景中,地址会被某一实体引用,它只承担描述实体的作用,并且它的值只能整体替换,这时候你就可以将地址设计为值对象,比如收货地址。而在某些业务场景中,地址会被经常修改,地址是作为一个独立对象存在的,这时候它应该设计为实体,比如行政区划中的地址信息维护。
贫血模型与充血模型案例
贫血模型
此种模型下领域对象的作用很简单,只有所有属性的
@Entity
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class User {
@Id
private String userId;
private String userName;
private String password;
private boolean isLock;
}
而真正的业务逻辑则由领域服务负责实现,此服务引入持久化仓库,在业务逻辑完成之后持久化到仓库中,并在此可以发布领域事件
public interface UserService {
void create(User user);
void edit(User user);
void changePassword(String userId, String newPassword);
void lock(String userId);
void unlock(String userId);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository repo;
@Override
public void edit(User user) {
User dbUser = repo.findById(user.getUserId()).get();
dbUser.setUserName(user.getUserName());
repo.save(dbUser);
// 发布领域事件 ...
}
@Override
public void lock(String userId) {
User dbUser = repo.findById(userId).get();
dbUser.setLock(true);
repo.save(dbUser);
// 发布领域事件 ...
}
// ... 省略完整代码
}
- 优点:结构简单,职责单一,相互隔离性好,使用单例模型提高运行性能
- 缺点:对象状态与行为分离,不能直观地描述领域对象。行为的设计主要考虑参数的输入和输出而非行为本身,不太具有面向对象设计的思考方式。行为间关联性较小,更像是面向过程式的方法,可复用性也较小。
充血模型
此种模型下领域对象作用此领域相关行为,包含此领域相关的业务逻辑,同时也包含对领域对象的持久化操作。
@Entity
@Data
@Builder
@AllArgsConstructor
public class User implements UserService {
@Id
private String userId;
private String userName;
private String password;
private boolean isLock;
// 持久化仓库
@Transient
private UserRepository repo;
// 是否是持久化对象
@Transient
private boolean isRepository;
@PostLoad
public void per() {
isRepository = true;
}
public User() {
}
public User(UserRepository repo) {
this.repo = repo;
}
@Override
public void create(User user) {
repo.save(user);
}
@Override
public void edit(User user) {
if (!isRepository) {
throw new RuntimeException("用户不存在");
}
userName = user.userName;
repo.save(this);
// 发布领域事件 ...
}
@Override
public void lock() {
if (!isRepository) {
throw new RuntimeException("用户不存在");
}
isLock = true;
repo.save(this);
// 发布领域事件 ...
}
}
在领域对象行为逻辑较复杂的情况下,需要多个行为共享对象状态的时候,充血模型表现力更强。
- 优点:对象自洽程度很高,表达能力很强,因此非常适合于复杂的企业业务逻辑的实现,以及可复用程度比较高,更符合面向对象设计思想
- 缺点:对象属性中掺杂持久化仓库,不够纯粹,持久化操作是否属于业务逻辑有待求证。但由于持久化仅需暴露接口,对业务逻辑与持久化操作的耦合度有一定降低。
无持久化充血模型
为了解决业务逻辑不纯粹问题,也有将持久化操作移出业务逻辑的作法。
@Entity
@Data
@Builder
@AllArgsConstructor
public class User implements UserService {
@Id
private String userId;
private String userName;
private String password;
private boolean isLock;
// 是否是持久化对象
@Transient
private boolean isRepository;
@Override
public void create(User user) {
user.userId = UUID.randomUUID().toString();
}
@Override
public void edit(User user) {
userName = user.userName;
}
@Override
public void lock() {
isLock = true;
}
}
@Service
public class UserManager {
@Autowired
private UserRepository repo;
public User findOne(String userId){
return repo.findById(userId).get();
}
public void edit(User u) {
User user = findOne(u.getUserId());
user.edit(u);
repo.save(user);
// 发布领域事件 ...
}
public void lock(String userId) {
User user = findOne(userId);
user.lock();
repo.save(user);
// 发布领域事件 ...
}
}
此种方式是前两种方式的折中,充分地做到了解耦,但也牺牲了部分内聚:
- 优点:保持了业务逻辑的纯粹性,去掉了持久化的入侵
- 缺点:降低了领域服务的自治性,破坏了行为逻辑的完整性,部分逻辑混入了
application 层,尤其是领域事件的发布