本文是基於上一篇‘業務建模戰術’的實踐,主要講解‘刪除帖子’場景的業務建模,包括:業務建模、業務模型、示例代碼;示例代碼會使用java編寫,文末附有github地址。相比於《領域驅動設計》原書中的航運系統例子,社交服務系統的業務場景對於大家更加熟悉,相信更好理解。本文是【DDD】系列文章的其中一篇,其他可參考:使用領域驅動設計思想實現業務系統
業務建模
這里的‘刪除帖子’場景是指帖子作者主動刪除帖子,至於管理員通過后台管理端下線帖子,我們認為該行為不同於‘刪帖’,需要單獨處理。
我們來分析下“刪除帖子”這一業務場景:刪除帖子的人需要查詢到帖子,然后才能刪除帖子,否則沒有刪除帖子的入口,也就是說刪除帖子的人也同時是帖子的查看者;帖子的查看者只能刪除自己發表的帖子,否則整個社區就亂套了。我們嘗試用一句話來描述‘刪除帖子’場景:
- 帖子刪除者(同時也是帖子查看者)可以刪除本人發布的帖子。
可以看出這里有三個實體:帖子刪除者、帖子查看者、帖子,一個業務行為:刪除帖子,一個業務規則:只可以刪除本人發布的帖子。現在我們要討論下“帖子刪除者”和“帖子查看者”有何區別,實際上在“刪除帖子”這個業務場景下,這兩個實體只是對同一個人的不同表述,也就是說這個人在“刪除帖子”這個場景下擁有了兩個角色,在“刪除帖子”這個業務場景下,他們擁有的業務行為沒有區別,都是“刪除帖子”;但是“帖子刪除者”相對於”帖子查看者”要狹隘一點,它只能適用於“刪除帖子”這一個場景,而“帖子查看者”還可以適用於“查看帖子詳情”、“查看帖子列表”等業務場景。因此,我們建模在“帖子查看者”這一個實體上。即:“帖子查看者”(PostReader)實體擁有“刪除帖子”(deletePost)這樣一個業務行為。
結合前一篇blog(業務建模實踐 —— 發布帖子)中已有的業務模型來看下,發現之前已經有一個“人”相關的業務實體——帖子作者(PostAuthor),既然他們都是人,就有必要做一些重構。觀察PostAuthor和PostReader,他們共有大部分的屬性和方法:id、name、nickname、headphoto等等,因此我們可以在兩者之上抽離出一個實體——“用戶”(User),User持有用戶通用的屬性和方法。
再回過頭來看下“刪除帖子”的業務規則:只能刪除本人發布的帖子,也就是說PostReader需要判定是否和Post的PostAuthor是同一個人,而這個業務判定並不只是在這個場景下獨用,所以我們可以讓User來持有這個業務行為,稱之為isMyself(User otherUser)。
毫無懸念,PostReader應當持有一個業務行為:“刪除帖子”(deletePost)。顯然deletePost方法的參數應當是一個Post實體,而在deletePost方法中需要調用User.isMyself(User otherUser)方法判定讀者和作者是否是同一個人,因此,Post對象必須提供一個PostAuthor出來,有兩種方式:一種是使用post.authorId創建一個PostAuthor,另外一種方式是重構模型,直接讓Post持有一個PostAuthor。方案一會讓PostReader“依賴”PostAuthor,方案二會讓Post和PostAuthor之間耦合太緊,本身PostAuthor已經“依賴”了Post,現在Post反過來又“關聯”PostAuthor,出現了雙向關系,不是好現象。但是,綜合考慮,方案一不符合實際業務關系,PostReader沒有必要依賴於PostAuthor,因此我們選擇了方案二。
業務模型
綜合上面的建模討論,可以得到如下的業務模型:
注:PostAuthor和Post之間的關系變成了無向的“關聯”關系,是因為Post“依賴”於PostAuthor,PostAuthor“關聯”Post,變成了雙向關系,因此使用無向的“關聯”關系表示比較合適。
匯總《業務建模實踐 —— 發布帖子》中的業務模型,我們得到最終版本的業務模型,如下:
可以看到在帖子模塊,業務模型涉及三個聚合:User、Post、Topic;涉及contentFilter相關領域服務;其中值對象TopicPost即在Post聚合中也在Topic聚合中。
示例代碼
Post.java增加postAuthor屬性,並在構造函數中初始化,同時新增delete()業務方法。
1 public class Post { 2 ...... 3 /** 4 * 帖子作者 5 */ 6 private PostAuthor postAuthor; 7 8 public Post(long authorId, String title, String sourceContent) { 9 this(); 10 this.setAuthorId(authorId); 11 this.setTitle(title); 12 this.setSourceContent(sourceContent); 13 this.setPostAuthor(new PostAuthor(authorId)); //在構造函數初始化PostAuthor 14 } 15 16 /** 17 * 刪除帖子 18 */ 19 public void delete() { 20 this.setStatus(PostStatus.HAS_DELETED); 21 } 22 ...... 23 24 }
新增User.java,重新equals()和hashCode(),並提供isMyself(User)業務方法:
1 public class User { 2 /** 3 * 用戶id 4 */ 5 private long id; 6 7 public User() { 8 super(); 9 } 10 11 public User(long id) { 12 this.setId(id); 13 } 14 15 /** 16 * 判定另外一個用戶是否是本人 17 * @param otherUser 18 * @return 19 * true —— 本人 20 * false —— 非本人 21 */ 22 public boolean isMyself(User otherUser) { 23 if(this.equals(otherUser)) { 24 return true; 25 } else { 26 return false; 27 } 28 } 29 30 @Override 31 public boolean equals(Object anObject) { 32 if(anObject == null) { 33 return false; 34 } 35 if(this == anObject) { 36 return true; 37 } 38 if(anObject instanceof User) { 39 if(this.id == ((User)anObject).getId()) { 40 return true; 41 } 42 } 43 return false; 44 } 45 46 @Override 47 public int hashCode() { 48 return Long.hashCode(this.id); 49 }59 ...... 60 }
新增PostReader.java,提供deletePost(Post)方法:
1 public class PostReader extends User { 2 3 public PostReader(long id) { 4 super(id); 5 } 6 7 /** 8 * 刪帖 9 * @param post 擬被刪除的帖子實體 10 * @return post 刪帖后的帖子實體 11 * @throws BusinessException 12 */ 13 public Post deletePost(Post post) throws BusinessException { 14 if (post == null) { 15 throw new BusinessException(ExceptionCode.POST_IS_NOT_EXIT); 16 } 17 if (!this.isMyself(post.getPostAuthor())) {18 throw new BusinessException(ExceptionCode.CAN_NOT_DELETE_OTHER_USERS_POST); 19 } 20 post.delete(); 21 return post; 22 } 23 }
思考
我們在這里將deletePost行為建模在PostReader實體上,現在看來合情合理,事實上,最開始建模的時候我們誤入歧途,將該行為賦予了Post,那么是怎么發現“將deletePost行為賦予PostReader”(稱之為方案2)要好於“將deletePost賦予Post"(稱之為方案1)呢?是在開始寫application層代碼時發現的,我們發現方案1會將業務邏輯散落在應用服務層,而方案2則不會,他將整個”刪除帖子”場景的業務邏輯內聚在了一個方法中。
這涉及到application層的一些東西,不過沒關系,對理解這個決策影響不大。口說無憑,上代碼。
方案1的application層代碼:
1 public class PostsServiceImpl implements IPostsService { 2 ...... 3 public void deletePost(BaseInNewBean<DeletePostInBean> inBean, HttpServletRequest request) throws Exception { 4 ...... 5 Post post = postRepository.queryPostDetail(postId); 6 if (post == null) { //Post實體對象不能判定自己是否為空,只能放到這里 7 throw new BusinessException(ExceptionCode.POST_IS_NOT_EXIT); 8 } 9 if (postReader.isMyself(post.getPostAuthor())) { //Post實體並不持有PostReader對象,因此無法完成此判定 10 throw new BusinessException(ExceptionCode.CAN_NOT_DELETE_OTHER_USERS_POST); 11 } 12 post.deletePost(post); 13 postRepository.deletePost(post); 14 ...... 15 } 16 ...... 17 }
方案2中application層代碼:
1 public class PostsServiceImpl implements IPostsService { 2 ...... 3 public void deletePost(BaseInNewBean<DeletePostInBean> inBean, HttpServletRequest request) throws Exception { 4 ...... 5 Post post = postRepository.queryPostDetail(postId); 6 postReader.deletePost(post); //domain層在application層只有一個api暴露 7 postRepository.deletePost(post); 8 ...... 9 } 10 ...... 11 }
從代碼的簡潔性、可讀性、可維護性已經模型的合理性來講,方案2完勝。
建模經驗
使用“繼承”方式實現不同角色的同類實體
對於同一類實體的不同角色,考慮使用“繼承”方式來實現,將實體中共有的屬性和業務行為建模在父實體上,將角色獨有的屬性和業務行為建模在子實體上。比如:PostAuthor和PostReader都是“用戶”,因此我們抽象出一個父實體——User,它持有所有用戶共有的屬性和行為,PostAuthor則持有“帖子作者"獨有的”發布帖子”的業務行為,PostReader則持有“帖子讀者”獨有的“刪除帖子”的業務行為。
持續集成盡早發現模型中的不足
每一個版本迭代完成后,盡早在application層完成集成,這樣能盡早發現模型的不足;如果沒有條件及早的開展application層的集成,那么必須寫單元測試,以便以客戶端的視角來審視模型的合理性。比如:對於deletePost建模在Post上還是PostReader上的例子中,便是盡早完成application層集成之后發現的問題。
源碼
此業務建模的demo已上傳至github,歡迎下載和討論,但拒絕被用於任何商業用途。
github地址:https://github.com/daoqidelv/community-ddd-demo/tree/deletePost
branch:deletePost
迭代
按"混沌攻城獅"的建議,應當將“deletePost”業務行為賦予PostAuthor而不是PostReader,這樣更符合業務場景,想來甚是合理。摘錄他的評論如下:
我覺得將刪貼行為賦予帖子查看者還不如給帖子作者,這樣作者擁有發帖,刪貼操作理解起來也順了很多。從場景分析,帖子列表中也只有是本人的帖子才會提供刪貼操作,不會讓帖子查看人逐個嘗試刪除帖子。如果把刪除帖子的行為賦予作者,那么在刪除時也只需要判斷待刪帖子的作者是否是本人。
按照上面的思路,我們調整下業務模型如下,之前的文章內容也就不修改了,這樣讀者也能看出業務模型迭代的過程,也服務DDD設計思想的實施建議。之前模型中的PostReader暫不刪除,后面的“查詢帖子詳情”會使用到。
對應的代碼已經修改,並上傳到github上。