写点什么

领域驱动设计 101 - 实体

用户头像
luojiahu
关注
发布于: 2021 年 05 月 16 日
领域驱动设计101 - 实体

1 实体定义

在对现实世界进行建模的过程中,有一类事物,他们具有唯一性,一旦生成便区别于其他个体。虽然本身的特征(属性)可能发生变化,但是他们有一个唯一确定的身份标识。


在 DDD 中,将具有唯一身份标识特征状态可变性特征的对象称为实体。


例如,对于一个人来说,从出生起,他便唯一区别于其他任何人,通过向政府机构申请身份号码,便可以具备一个唯一的身份标识,这个标识将会伴随其一生,并且不能改变。但是其他的属性,例如身高、体重、年龄都会持续发生变化,甚至其他看似不能轻易改变的属性,如性别、容貌等等,在如今也可能发生变化。与一个人相对应的,住址作为一个对象,并不需要唯一标识,可能被关联到不同的对象上进行复用,例如一个地址可能是员工的通信地址,也可能是公司的办公地址;同时,其属性状态也不可变,一旦发生变化就代表着另外一个对象,例如解放路在很多城市都存在,但显然代表不同的地址。

2 唯一标识

在识别出实体之后,首先需要明确的是实体的唯一标识如何确定。常见的做法有几种


  • 用户输入

让用户输入唯一标识最大的好处是简单。但是,让用户输入意味着极高的错误率和不可控,为此程序必须进行添加足够的校验和检查逻辑,引导用户输入可用的标识。

从便捷性和用户体验等角度出发,采用用户输入的方式来生成唯一标识极少在实际生产应用中见到。除非是一些非常简单的应用场景。


  • 应用程序生成

让应用程序生成唯一标识是比较常见的方法,在实际实现中常见的有 UUID、雪花算法等。

采用这些算法的一个问题是生成的标识无序,这对于那些期望通过标识来体现顺序的场景不够适用;另外这些无序标识在一些按照索引组织数据的关系数据库(例如 MySQL)中进行查询和持久化时,相比于有序标识,有较大的性能不足。

另一个问题,是采用这些算法生成的标识对于大部分场景显得过长,同时不能体现出业务含义。


对于这个问题,一个解决办法是只采用生成 UUID 的一部分,同时采用另外一部分有业务含义的属性,两部分拼接起来作为唯一标识。

UUID: 391550d0-b380-43a6-b9b3-9985e57fe630

Date: 2021-05-07

唯一标识:2021-05-07-391550d0


  • 利用数据库等持久化存储

各种存储通常都提供了对数值类型的操作,例如 MySQL 的 AUTO_INCREMENT 列作为 ID,采用 Redis 的 STRING 结构通过命令 INCR/INCRBY 等命令进行 ID 的自增等等。


  • 利用其它上下文能力

当应用的规模扩大时,进行服务拆分带来的可扩展性可能仍旧不能满足数据量和并发请求量的要求,这时瓶颈可能会从应用服务器转移到服务的后端存储服务。这个时候再采用前面的几种方法来产生唯一标识可能都不能满足要求,这时就需要引入专门的上下文来承担生成唯一标识的职责。

独立的 ID 生成上下文与具体的业务解耦,独享资源,因此具备更高的复用性、性能容量,同时也可以独立演进,提供更加符合实际要求的服务。


实体不应该仅由唯一标识和属性构成。

3 贫血模型与实体行为


贫血模型(Anemic Domain Model)的概念由Martin Fowler于2003年提出。概括来说,贫血模型是指那些本该具备丰富行为的领域模型,退化为仅仅由属性和 getter/setter 构成的实体类,这些实体类不具有任何可以体现其核心行为的操作,就像一个丧失行动能力的贫血症患者。


这些贫血实体的行为被从实体本身剥离之后,经常会被设计成一个个 Service,这些 Service 提供计算、存储等各种行为,并且利用这些贫血实体进行数据的传递、持久化存储。


贫血模型带来两个问题:


  • 将本来应该具备丰富操作行为的实体退化成了一个干瘪的属性集合,实体仅仅是一个传输数据的媒介,而不是描述业务规则和逻辑的充盈对象。

  • 那些被设计用来执行业务操作的 Service 更像是一个个事务脚本(Transaction Scripts),这更像是面向过程编程,而不是面向对象编程。


毋庸置疑,DDD 所描述的实体不应该是贫血模型。**因此,实体除了具备唯一标识和可变属性的特征之外,还应该具备体现其核心业务规则和逻辑的操作行为。**这些操作,正是实体被建模用来解决的问题领域中的核心业务逻辑。


下面对文章编辑发布场景,以文章这一对象为例,对贫血实体和含有行为的实体进行比较


/** * 文章模型 */public class Article {
int id;
String title;
ArticleStatus status;
ArticleRepository articleRepository;
/** * 改变当前文章的状态. * @param to * @return */ public Boolean changeStatusTo(ArticleStatus to) { if (status == ArticleStatus.DRAFT && to != ArticleStatus.COMMITTED) { // draft can only change to committed. return false; }
if (status == ArticleStatus.COMMITTED && to != ArticleStatus.APPROVED && to != ArticleStatus.REJECTED && to != ArticleStatus.PUBLISHED) { // committed can only change to approved and committed and published. return false; }
/** other logic ommitted. */ status = to; return true; }
/** * save this article to db or other sores. * @return */ public Boolean save() { ArticlePO articlePO = toPersistenceObject(); return articleRepository.save(articlePO); } /** other function omitted... */
}
复制代码


首先是,含有行为的文章实体,重点关注changeStatusTo方法,提供了改变文章状态的操作,并且将文章状态迁转时的逻辑内置于其中,这样从业务操作只需要调用该函数便可完成各种状态改变的操作,例如:审核通过、审核拒绝等等。例如如下审核通过业务:


/** * 审核文章 * @param id * @return */public Boolean approveArticle(int id) {    ArticlePO articlePO = articleRepository.getById(id);    if (null == articlePO) {        throw new IllegalStateException("can not find such article of id: " + id);    }    Article article = Article.fromPersistenceObject(articlePO);
Boolean success = article.changeStatusTo(ArticleStatus.APPROVED); if (!success) { return false; } article.save();
// do other things... return true;}
复制代码


而一个贫血的文章模型可能如下所示, 仅仅含有属性:


public class ArticlePO {
int id;
String title;
int status;
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }}
复制代码


这个时候对应的审核操作如下面的代码所示,原本应该由Article实体承担的文章状态迁转逻辑校验变成了文章审核方法的一部分。采用这种方法,在其他有文章状态变化的地方,例如文章下架操作,可能还会有其他对应的文章状态校验逻辑片段,从而造成逻辑散落。


/** * 贫血模型对应文章审核. * @param id * @return */@Deprecatedpublic Boolean approveAnemicArticle(int id) {    ArticlePO articlePO = articleRepository.getById(id);    if (null == articlePO) {        throw new IllegalArgumentException("can not find such article of id: " + id);    }
int status = articlePO.getStatus(); // 事务脚本, 业务规则散落 if (status == ArticleStatus.DRAFT.getValue() || status == ArticleStatus.APPROVED.getValue() || status == ArticleStatus.REJECTED.getValue() || status == ArticleStatus.PUBLISHED.getValue()) { // those status can not change to approved. return false; } // set status to approved. articlePO.setStatus(ArticleStatus.APPROVED.getValue()); articleRepository.update(articlePO);
// do other things... return true;}
复制代码


上面仅仅是贫血模型的简单例子,对于一般具有一定复杂度的应用,如果所有问题都采用贫血模型和事务脚本来处理,经过长时间的演化,最终代码将逐渐变得很难维护,因为业务规则会逐渐散落各处;相应地,也会给单元测试案例的编写造成不小的成本。经过一段时间,可能工程将不得不进行一次彻底重构来适应新的业务需求。

发布于: 2021 年 05 月 16 日阅读数: 47
用户头像

luojiahu

关注

喜欢思考组织、过程、产品的后端开发 2017.01.08 加入

还未添加个人简介

评论

发布
暂无评论
领域驱动设计101 - 实体