標籤:類型轉換 enum amp logs err orm valueof ges 裁剪
本文是DDD架構實現講解的第二篇,主要介紹了DDD的Application層的實現,詳細講解了service、assemble的職責和實現。文末附有github地址。相比於《領域驅動設計》原書中的航運系統例子,社交服務系統的業務情境對於大家更加熟悉,相信更好理解。本文是【DDD】系列文章的其中一篇,其他可參考:使用領域驅動設計思想實現業務系統
Application層
在DDD設計思想中,Application層主要職責為組裝domain層各個組件及基礎設施層的公用組件,完成具體的商務服務。Application層可以理解為粘合各個組件的膠水,使得零散的組件組合在一起提供完整的商務服務。在複雜的業務情境下,一個業務case通常需要多個domain實體參與進來,所以Application的粘合效用正好有了用武之地。
Application層主要由:service、assembler組成,下面分別對其做講解。
Serviceservice是組件粘合劑
這裡的Service區別於domain層的domain service,是應用服務。它是組件的粘合劑,組合domain層的各個組件和 infrastructure層的持久化組件、訊息組件等等,完成具體的商務邏輯,提供完整的商務服務。
通過不斷的實踐,我們發現:通過DDD實現商務服務時,檢驗業務模型的品質的一個標準便是 —— service方法中不要有if/else。如果存在if/else,要麼就是系統用例存在耦合,要麼就是業務模型不夠友好,導致部分商務邏輯泄漏到service了。
通常意義上,一個業務case在service層便會對應一個service方法,這樣確保case實現的獨立性。拿社區服務中的“文章”模組來講,我們有如下幾個明顯的case:發帖(posting)、刪帖(deletePost)、查詢文章詳情(queryPostDetail),這些case在service層都對應獨立的業務方法。
思考
對於較為複雜的case:查詢貼文清單,可能需要根據不同的tag過濾文章,或者查詢不同類型的文章,或者查詢熱門文章,這個時候應當用一個service方法實現呢?還是多個呢?
考慮這個問題,主要從這兩方面入手:domain的一致性,資料存放區的一致性;如果兩個一致性都滿足,那麼我們可以在一個業務方法中完成,否則就要在獨立的業務方法中完成。
例如:根據文章運營標籤查詢文章 和 查詢全部貼文清單 這兩個case我們可以放到一個service方法中實現,因為前一個case只是在後一個case的基礎上加了一個過濾條件,這個過濾條件完全可以交給dao層的sql where條件處理掉,除此之外,domain和repository都完全一樣;
而“查詢熱門文章” 這個case就不能和上面的兩個case共用一個service方法了,因為熱門貼文清單的資料來源並不在資料庫中,而是存在於緩衝中,因此repository的取數邏輯存在很大差異,如果共用一個service方法,勢必要在service層出現if/else判定,這是不友好的。
類圖
程式碼範例
1 @Service 2 public class PostServiceImpl implements PostService { 3 4 @Autowired 5 private IPostRepository postRepository; 6 7 @Autowired 8 private PostAssembler postAssembler; 9 10 11 12 public PostingRespBody posting(RequestDto<PostingReqBody> requestDto) throws BusinessException {13 PostingReqBody postingReqBody = requestDto.getBody();14 /**15 *NOTE: 請求參數校正交給了validation,這裡無需校正userId和postId是否為空白16 */17 String userId = postingReqBody.getUserId();18 String title = postingReqBody.getTitle();19 String sourceContent = postingReqBody.getSourceContent();20 21 long userIdInLong = Long.valueOf(userId);22 23 /**24 * 組裝domain model entity25 * NOTE:這裡的PostAuthor不需要從repository重載,原因在於:deletePost情境需要使用者登入後才能操作,26 * 在進入service之前,已經在controller層完成了使用者身份鑒權,故到達這裡的userId肯定是合法的使用者27 */28 PostAuthor postAuthor = new PostAuthor(userIdInLong);29 Post post = postAuthor.posting(title, sourceContent);30 31 /**32 * NOTE:使用repository將model entity 寫入儲存33 */34 postRepository.save(post);35 36 /**37 * NOTE:使用postAssembler將Post model組裝成dto返回。38 */39 return postAssembler.assemblePostingRespBody(post);40 }41 42 43 public DeletePostRespBody delete(RequestDto<DeletePostReqBody> requestDto) throws BusinessException {44 DeletePostReqBody deletePostReqBody = requestDto.getBody();45 46 /**47 *NOTE: 請求參數校正交給了validation,這裡無需校正userId和postId是否為空白48 */49 String userId = deletePostReqBody.getUserId();50 String postId = deletePostReqBody.getPostId();51 52 long userIdInLong = Long.valueOf(userId);53 long postIdInLong = Long.valueOf(postId);54 55 /**56 * 組裝domain model entity57 * NOTE:這裡的PostAuthor不需要從repository重載,原因在於:deletePost情境需要使用者登入後才能操作,58 * 在進入service之前,已經在controller層完成了使用者身份鑒權,故到達這裡的userId肯定是合法的使用者59 */60 PostAuthor postAuthor = new PostAuthor(userIdInLong);61 /**62 * 從repository中重載domain model entity63 * 藉此判斷該postId是否真的存在文章64 */65 Post post = postRepository.query(postIdInLong);66 67 postAuthor.deletePost(post);68 69 postRepository.delete(post); 70 71 return null;72 }73 74 75 @Override76 public QueryPostDetailRespBody queryPostDetail(RequestDto<QueryPostDetailReqBody> requestDto)77 throws BusinessException {78 QueryPostDetailReqBody queryPostDetailReqBody = requestDto.getBody();79 80 String readerId = queryPostDetailReqBody.getReaderId();81 String postId = queryPostDetailReqBody.getPostId();82 83 long readerIdInLong = Long.valueOf(readerId);84 long postIdInLong = Long.valueOf(postId);85 86 //TODO 可能有一些許可權校正,比如:判定該讀者是否有查看作者文章的許可權等。這裡暫且不展開討論。87 PostReader postReader = new PostReader(readerIdInLong);88 89 Post post = postRepository.query(postIdInLong);90 91 /**92 * NOTE: 使用postAssembler將domain層的model組裝成dto,組裝過程:93 * 1、完成類型轉換、資料格式化;94 * 2、將多個model組合成一個dto,一併返回。95 */96 return postAssembler.assembleQueryPostDetailRespBody(post);97 } 98 99 }
AssemblerAssembler是組合器
Assembler是組合器,負責完成domain model對象到dto的轉換,組裝職責包括:
- 完成類型轉換、資料格式化;如日誌格式化,狀態enum裝換為前端認識的string;
- 將多個domain領域對象組裝為需要的dto對象,比如查詢貼文清單,需要從Post(文章)領域對象中擷取文章的詳情,還需要從User(使用者)領域對象中擷取使用者的社交資訊(暱稱、簡介、頭像等);
- 將domain領域對象屬性裁剪並組裝為dto;某些情境下,可能並不需要所有domain領域對象的屬性,比如User領域對象的password屬性屬於隱私相關屬性,在“查詢使用者資訊”case中不需要返回,需要裁剪掉。
範例程式碼
1 /** 2 * Post模組的組合器,完成domain model對象到dto的轉換,組裝職責包括: 3 * 1、完成類型轉換、資料格式化;如日誌格式化,狀態enum裝換為前端認識的string; 4 * 2、將多個model組合成一個dto,一併返回。 5 * TODO: 不太好的地方每個assemble方法都需要先判斷入參對象是否為空白。 6 * @author daoqidelv 7 * @createdate 2017年9月24日 8 */ 9 @Component10 public class PostAssembler {11 12 private final static String POSTING_TIME_STRING_DATE_FORMAT = "yyyy-MM-dd hh:mm:ss";13 14 @Autowired15 private ApplicationUtil applicationUtil;16 17 public PostingRespBody assemblePostingRespBody(Post post) {18 if(post == null) {19 return null;20 }21 PostingRespBody postingRespBody = new PostingRespBody();22 postingRespBody.setPostId(String.valueOf(post.getId()));23 return postingRespBody;24 }25 26 public QueryPostDetailRespBody assembleQueryPostDetailRespBody(Post post) {27 /**28 * NOTE: 判定入參post是否為null29 */30 if(post == null) {31 return null;32 }33 QueryPostDetailRespBody queryPostDetailRespBody = new QueryPostDetailRespBody();34 queryPostDetailRespBody.setAuthorId(String.valueOf(post.getAuthorId())); //完成類型轉換35 queryPostDetailRespBody.setPostId(String.valueOf(post.getId()));//完成類型轉換36 queryPostDetailRespBody.setPostingTime(37 applicationUtil.convertTimestampToString(post.getPostingTime(), POSTING_TIME_STRING_DATE_FORMAT));//完成日期格式化38 queryPostDetailRespBody.setSourceContent(post.getSourceContent());39 queryPostDetailRespBody.setTitle(post.getTitle());40 return queryPostDetailRespBody;41 }42 43 }
思考
上述代碼實現中,每一個assemble方法都需要校正入參對象是否為空白,實踐中發現,這一個關鍵點很容易遺漏,沒有想到好的辦法解決。
類圖
demo
此demo的代碼已上傳至github,歡迎下載和討論,但拒絕被用於任何商業用途。
github地址:https://github.com/daoqidelv/community-ddd-demo/tree/master
branch:master
【DDD】領域驅動設計實踐 —— Application層實現