【DDD】领域驱动设计实践 —— Domain层实现

时间:2023-03-09 15:08:20
【DDD】领域驱动设计实践 —— Domain层实现

本文是DDD框架实现讲解的第三篇,主要介绍了DDD的Domain层的实现,详细讲解了entity、value object、domain event、domain service的职责,以及如何识别出领域中的这些对象,并附有具体的业务建模示例。相比于《领域驱动设计》原书中的航运系统例子,社交服务系统的业务场景对于大家更加熟悉,相信更好理解。本文是【DDD】系列文章的其中一篇,其他可参考:使用领域驱动设计思想实现业务系统

Domain层

  Domain层是具体的业务领域层,是发生业务变化最为频繁的地方,是业务系统最核心的一层,是DDD关注的焦点和难点。这一层包含了如下一些domain object:entity、value object、domain event、domain service、factory、repository等。DDD实践的难点其实就在于如何识别这些object。下面将一一说明他们。

domain entity

  领域实体是domain的核心成员。domain entity具有如下三个特征:

  • 唯一业务标识
  • 持有自己的业务属性和业务行为
  • 属性可变,有着自己的生命周期

   在社区这一业务领域中,‘帖子’就是一个业务实体,它需要有一个唯一性业务标识表征,拥有这个业务实体相关的业务属性(作者、标题、内容等)和业务行为(关联话题、删帖等),同时他的状态和内容可以不断发生变化。

   示例代码如下:

public class Post {

    /**
* 帖子id
*/
private long id; //1、‘帖子’实体有唯一业务标识
/**
*帖子作者
*/
private long authorId;
/**
* 帖子标题
*/
private String title;//2、‘帖子’实体拥有自己的业务属性
/**
* 帖子源内容
*/
private String sourceContent;
/**
* 发帖时间
*/
private Timestamp postingTime;
/**
* 帖子状态
* NOTE:使用enum实现,限定status的字典值
* @see com.dqdl.community.domain.model.post.PostStatus
*/
private PostStatus status;
/**
* 帖子作者
*/
private PostAuthor postAuthor; /**
* 帖子加入的话题
*/
private Set<TopicPost> topics = new HashSet<TopicPost>(); private Post() {
this.postingTime = new Timestamp(System.currentTimeMillis());
} public Post(long id) {
this.setId(id);
} public Post(long authorId, String title, String sourceContent) {
this();
this.setAuthorId(authorId);
this.setTitle(title);
this.setSourceContent(sourceContent);
this.setPostAuthor(new PostAuthor(authorId));
} /**
* 删除帖子
*/
public void delete() {
this.setStatus(PostStatus.HAS_DELETED);//3、帖子的状态可以改变
} /**
* 将帖子关联话题
* @param topicIds 话题集合
*/
public void joinTopics(String topicIds) throws BusinessException{//2、‘帖子’实体拥有自己的业务行为
if(StringUtils.isEmpty(topicIds)) {
return;
}
String[] topicIdArray = topicIds.split(CommonConstants.COMMA);
for(int i=0; i<topicIdArray.length; i++) {
TopicPost topicPost = new TopicPost(Long.valueOf(topicIdArray[i]), this.getId());
this.topics.add(topicPost);
if(topicSize() > MAX_JOINED_TOPICS_NUM) {
throw new BusinessException(ReturnCode.ONE_POST_MOST_JOIN_INTO_FIVE_TOPICS);
}
}
}
//......

value object

领域值对象。value object是相对于domain entity来讲的,对照起来value object有如下特征:

  • 可以有唯一业务标识    【区别于domain entity】
  • 持有自己的业务属性和业务行为 【同domain entity】
  • 一旦定义,他是不可变的,它通常是短暂的,这和java中的值对象(基本类型和String类型)类似 【区别于domain entity】

  比如社区业务领域中,‘帖子的置顶信息’可以理解为是一个值对象,不需要为这一值对象定义独立的业务唯一性标识,直接使用‘帖子id‘便可表征,同时,它只有’置顶状态‘和’置顶位置‘,一旦其中一个属性需要发生变化,则重建值对象并赋值给’帖子‘实体的引用,不会对领域带来任何负面影响。

  代码示例:(TODO:关于PostTopInfo 这个value object的使用,示例代码中暂未涉及。)

/**
* 帖子置顶消息,value object
* @author daoqidelv
* @createdate 2017年10月10日
*/
public class PostTopInfo {
/**
* 帖子id
*/
private long postId;
/**
* 置顶标志。true -- 置顶, false -- 不置顶。
*/
private boolean isTop;
/**
* 置顶位置,当isTop == true时,该字段有意义。
*/
private int topIndex; public PostTopInfo(long postId, boolean isTop, int topIndex) {
this.setPostId(postId);
this.setTop(isTop);
this.setTopIndex(topIndex);
} public long getPostId() {
return postId;
} public void setPostId(long postId) {
this.postId = postId;
} public boolean isTop() {
return isTop;
} public void setTop(boolean isTop) {
this.isTop = isTop;
} public int getTopIndex() {
return topIndex;
} public void setTopIndex(int topIndex) {
this.topIndex = topIndex;
} }

domain service

  领域服务。区别于应用服务,他属于业务领域层。可以认为,如果某种行为无法归类给任何实体/值对象,则就为这些行为建立相应的领域服务即可。传统意义上的util static方法中,涉及到业务逻辑的部分,都可以考虑归入domain service。

  比如:‘社区’这一业务领域中的‘内容过滤’这一模块,便是领域服务,他不只属于Post实体,还会被用于评论(Comment)实体中,故我们将他独立成domain service。

  domain service的实现和使用的示例代码请参考:【DDD】业务建模实践 —— 发布帖子 中的‘示例代码’这一节。

domain event

  领域事件。领域中产生的一些消息事件,可以在性能和解耦层面得到好处。我们通常借助于消息中间件,通过事件通知/订阅的方式落地。

  在‘社区’业务领域中,‘发帖’之后,会同时为帖子作者生成一个‘发帖动态’,这个‘生成发帖动态’场景并不同步完成,而是通过领域事件发布异步完成。‘发帖’创建Post实体后,发布一个‘发帖动态’领域事件(PostingDynamic),‘动态’(Dynamic)相关服务消费该领域事件,并生成Dynamic实体。

  示例代码暂未给出。

domain factory

  领域对象工厂。用于复杂领域对象的创建/重建。重建是指通过respostory加载持久化对象后,重建领域对象。

  示例代码中暂未涉及,试实际情况而定是否引入factory。

repository

  仓库。我们将仓库的接口定义归类在domain层,因为他和domain entity联系紧密。仓库接口定义了和基础实施的持久化层交互契约,完成领域对应的增删改查操作。domain层的repository只是定义契约的接口,实际实现仍然由infrastructure完成。

  仓库的实际实现根据不同的存储介质而不同,可以是redis、oracle、mongodb等。具体仓库的实现会讲给infrastructure层完成,我们会在下一篇blog中详细阐述repository的实现。

  对于repository的接口定义,建议规范接口名命名,比如:查询都叫着query等等,减小沟通成本。

  示例代码只包含了‘社区’领域模型中Post实体相关的repository接口定义,如下:

  

public interface IPostRepository {

    Post query(long postId);

    int save(Post post);

    int delete(Post post);

}

领域建模示例

  接下来附上‘社区’业务领域中‘帖子’实体建模过程的blog,讲述了如何通过不断迭代完善业务模型,希望对你有用:

demo

  此demo的代码已上传至github,欢迎下载和讨论,但拒绝被用于任何商业用途。

  github地址:https://github.com/daoqidelv/community-ddd-demo/tree/master

  branch:master