【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