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

时间:2022-08-31 12:59:14

       本文是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