【DDD】業務建模實踐 —— 刪除帖子


  本文是基於上一篇‘業務建模戰術’的實踐,主要講解‘刪除帖子’場景的業務建模,包括:業務建模、業務模型、示例代碼;示例代碼會使用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上。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM