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