【DDD】領域驅動設計實踐 —— UI層實現


  前面幾篇blog主要介紹了DDD落地架構及業務建模戰術,后續幾篇blog會在此基礎上,講解具體的架構實現,通過完整代碼demo的形式,更好地將DDD的落地方案呈現出來。本文是架構實現講解的第一篇,主要介紹了DDD的User Interface層的實現,詳細講解了controller、dto的職責和實現,已經UI層使用到的公共組件:CheckLogin、Loging、Validation的職責和實現細節。文末附有github地址。相比於《領域驅動設計》原書中的航運系統例子,社交服務系統的業務場景對於大家更加熟悉,相信更好理解。本文是【DDD】系列文章的其中一篇,其他可參考:使用領域驅動設計思想實現業務系統

User Interface

  User Interface層是用戶接口層,為用戶/調用方提供可訪問的接口,我們簡稱為“UI”層。在 “【DDD】領域驅動設計實踐 —— 框架實現” 一文中,我們將dto、controller歸入了UI層。同時,在UI層中,我們還會去使用infrastructure層中的validation、logging、checkLogin等公共組件完成一些通用的動作。接下來我們將逐一講解。

Controller

controller是公司前台

  這里的controller承擔這一個請求受理的角色,就像是一個公司的前台,接受不同的請求(人員來訪、電話咨詢、快遞/信件簽收等等),必要時還會對不同的請求進行權限校驗,以防壞人搗蛋(比如:查驗來訪者的身份,確認被訪問者是否真有其人);並將不同格式的請求轉換為通用的請求格式(用標准的郵件/電話/短信通知責任人);將請求轉發到對應的負責人(可能是將電話轉接給負責人,也可能是將應聘者介紹給面試官,還可能是叫某個程序猿出來取快遞);到最后還會將來訪者登錄在案,必要時,還會通過前台告知具體的處理結果(告知電話咨詢者其要求是否能得到滿足,告知來訪者他要找的人沒有上班等)。

controller的職責

  類比與前台工作,我們可以發現controller有如下職責:

  •   接受請求;
  •        請求格式校驗及轉換;
  •   權限校驗;
  •   路由請求;
  •   記錄請求;
  •   回復響應;

controller的實現

  在示例代碼中,我們使用spring-mvc框架實現,controller就直接使用spring-mvc中的controller層完成。對應到上述職責分別有如下組件完成:

  •   接受請求 —— Spring-MVC的DispatcherServlet組件完成;
  •        請求格式校驗及轉換 —— 格式校驗遵循java Validation規范,使用Hibernate的validator組件完成;最終會被轉換為DTO組件,並在DTO組件中落地validation,放到后面專門講解;
  •   權限校驗 —— 自實現的CheckLogin組件完成;放到后面專門講解;
  •   路由請求 ——  Spring-MVC的@RequestMapping組件完成;
  •   記錄請求 —— 自實現的Loggin組件完成;放到后面專門講解;
  •   回復響應 —— Spring-MVC的@ResponseBody組件完成;

BaseController

  在實際編碼中,發現所有的controller會有一些公共的行為,比如異常處理,封裝響應dto,我們將這些行為抽象出來,放在BaseController中,所有的業務Controller繼承這個基礎類。

類圖

 

 

 代碼示例

 

 1 public class BaseController {
 2 
 3     private static Logger logger = LogManager.getLogger(BaseController.class);
 4     
 5     @Autowired
 6     private ApplicationUtil applicationUtil;
 7     
 8     @Autowired
 9     private ExceptionHandler exceptionHandler;
10 
11     /**
12      * format 失敗 response。
13      * @param e
14      * @return
15      */
16     protected ResponseDto formatErrorResponse(final Exception e) {
17         ResponseDto responseDto = new ResponseDto();
18         //將response 的data body置為空
19         responseDto.setBody(null);
20 
21         //依據異常類型進行分別處理,將異常信息轉義為用戶友好的提示信息
22         Map<String, String> exceptionMap = exceptionHandler.handle(e);
23         responseDto.setReturnCode(exceptionMap.get("errorCode"));
24         responseDto.setReturnMsg(exceptionMap.get("errorMsg"));
25         logger.debug("Response is: "+responseDto);
26         return responseDto;
27     }
28     
29     
30     /**
31      * format成功的response
32      * @param responseBody
33      * @return ResponseDto
34      */
35     protected ResponseDto formatSuccessResponse(ResponseBody responseBody) {
36         ResponseDto responseDto = new ResponseDto();
37         //設置返回碼和返回信息為成功
38         responseDto.setReturnCode(ReturnCode.SUCCESS);
39         responseDto.setReturnMsg(applicationUtil.getReturnMsg(ReturnCode.SUCCESS));
40         
41         //將response 的data body置為實際的業務body
42         responseDto.setBody(responseBody);
43         logger.debug("Response is: "+responseDto);
44         return responseDto;
45     }
46     
47     
48 }
BaseController.java
 1 @Controller
 2 @RequestMapping("/post")
 3 public class PostController extends BaseController {
 4     
 5     @Autowired
 6     private PostService postService;
 7 
 8     /**
 9      *  發布帖子
10      * @param requestDto
11      * @return  ResponseDto
12      */
13     @ResponseBody
14     @RequestMapping(value = "/posting", method = RequestMethod.POST)
15     public ResponseDto posting(@RequestBody @Valid RequestDto<PostingReqBody> requestDto) {
16         try {
17             PostingRespBody postingRespBody = postService.posting (requestDto);
18             return this.formatSuccessResponse(postingRespBody);
19         } catch (Exception e) {
20             return this.formatErrorResponse(e);
21         }
22     }
23     .......
24 }
PostController.java

 

 DTO

DTO是controller和service之間數據傳輸的載體

  DTO(Data Transfer Object),顧名思義,DTO用於組件/分層間傳遞數據,它是數據傳遞的載體。在這里我們用它作為 “controller <=> service” 之間傳遞數據的載體。controller傳遞一個RequestDto給service,service完成業務處理后,返回一個ReponseDto。

類圖

 

 

  請求中公共的屬性(請求頭)放置到RequestDto中。RequestDto持有一個RequestBody(請求體),包含各個場景的具體業務請求參數,所有的業務請求都會有自己的ReqBody,並繼承至RequestBody。

  ResponseDto同理,不再贅述。

示例代碼

1 /**
2  * User Interface layer Data Transfer Object
3  * @author daoqidelv
4  * @createdate 2017年9月24日
5  */
6 public interface UIDto {
7 
8 }
UIDto.java
 1 public class RequestDto<T> implements UIDto {
 2     
 3     /**
 4      * 請求渠道
 5      */
 6     private String channel;
 7     
 8     /**
 9      * 請求id
10      */
11     private String requestId;
12     
13     /**
14      * 對body使用validation
15      */
16     @Valid 
17     private T body;
18 
19     public String getChannel() {
20         return channel;
21     }
22 
23     public void setChannel(String channel) {
24         this.channel = channel;
25     }
26 
27     public String getRequestId() {
28         return requestId;
29     }
30 
31     public void setRequestId(String requestId) {
32         this.requestId = requestId;
33     }
34 
35 
36     public T getBody() {
37         return body;
38     }
39 
40     public void setBody(T body) {
41         this.body = body;
42     }
43     
44 }
RequestDto.java
 1 public class ResponseDto implements UIDto{
 2     
 3     /**
 4      * 狀態碼
 5      */
 6     private String returnCode;
 7 
 8     /**
 9      * 提示信息
10      */
11     private String returnMsg;
12 
13     /**
14      * 各個接口返回的數據
15      */
16     private Object body;
17 
18     public String getReturnCode() {
19         return returnCode;
20     }
21 
22     public void setReturnCode(String returnCode) {
23         this.returnCode = returnCode;
24     }
25 
26     public String getReturnMsg() {
27         return returnMsg;
28     }
29 
30     public void setReturnMsg(String returnMsg) {
31         this.returnMsg = returnMsg;
32     }
33 
34     public Object getBody() {
35         return body;
36     }
37 
38     public void setBody(Object body) {
39         this.body = body;
40     }
41     
42 }
ResponseDto
1 public class RequestBody {
2 
3 }
RequestBody.java
1 public class ResponseBody {
2 
3 }
ResponseBody.java
public class PostingReqBody extends RequestBody {

    @NotEmpty(message="{request.userId.not.empty}")
    private String userId;
    
    private String title;
    
    @NotEmpty(message="{request.post.posting.content.not.empty}")
    private String sourceContent;

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getSourceContent() {
        return sourceContent;
    }

    public void setSourceContent(String sourceContent) {
        this.sourceContent = sourceContent;
    }  

}
PostingReqBody.java
 1 public class PostingRespBody extends ResponseBody {
 2     
 3     private String postId;
 4 
 5     public String getPostId() {
 6         return postId;
 7     }
 8 
 9     public void setPostId(String postId) {
10         this.postId = postId;
11     }
12 }
PostingRespBody.java

 infrastructure層的公共組件

 checkLogin

稱職的門將

  checkLogin組件負責登錄態校驗,或者叫做會話控制,有時候還需要做權限校驗,它是一個稱職的門將。不同的部署架構對checkLogin的實現要求不一樣。在傳統的單體應用中,會話控制一般交給web容器來管理,通常使用filter統一管控會話,在分布式系統中,由於服務組件本身要求無會話狀態,故會單獨有一個SessionServer來負責會話控制。checkLogin針對上述兩種不同的實現方案。

  傳統的單體應用中個,直接使用filter實現,交給web容器來管理會話。對於分布式系統,則由checkLogin組件完成SessionServer的交互,接受sessionServer返回的會話校驗結果,並結合當前請求的權限要求,做出相應的會話控制。

  本示例代碼中未給出CheckLogin的具體實現,基本思路如上面所示。如果使用SpringMVC框架,可以使用AOP的方式,將CheckLogin以注解的方式實現,在需要進行會話控制的controller方法上,加上@CheckLogin注解。或者可以使用Spring 的HandlerInterceptor攔截器實現,可以讓RequestBody實現類根據業務場景決定是否實現LoginAuthority接口,且可以根據不同的權限設置不同的LoginAuthority接口,然后在Interceptor統一做攔截處理。

類圖

  采用Interceptor形式實現CheckLogin,可能的類圖如下:

  UserLoginAuthority表示需要用戶登錄態,AccountLoginAuthority表示需要賬戶登錄態,AccountLoginAuthority權限要求高於UserLoginAuthority。而PostingReqBody和DeletePostReqBody在所有登錄態下都可以進行,因此只需實現LoginAuthority接口即可。

 

Logging

  Logging組件負責記錄應用服務日子及關鍵操作日志,包括:api訪問請求/響應內容、持久層訪問記錄、第三方服務調用記錄等等。通常各個業務系統都有自己的日志組件和日志記錄規范。所以本demo也不打算給出具體的代碼實現。

  如果使用SpringMVC框架,可以嘗試使用AOP的方式完成,可以做到對domain層無侵入,是比較友好的實踐方式。

Validation

  Validation主要負責參數格式校驗,確保進入到service層的參數是合法的,使入參對service更友好。

  JSR303 是一套JavaBean參數校驗的標准,它定義了很多常用的校驗注解。Hibernate validator 實現了JSR303g標准,並在標准基礎上對校驗注解進行了擴展,主要增加了@NotEmpty注解,這在demo中有使用到。

  如何集成Hibernate validator 可以參閱如下blog:Java Bean Validation 最佳實踐

  或者參考demo代碼,不再贅述。

ExceptionHandler

異常都交給UI層統一處理

  ExceptionHandler是自定義的異常處理器,主要負責對service拋出的所有異常進行攔截及處理,針對不同的異常類型,轉義成不同的錯誤碼和錯誤提示信息返回給調用方。這樣做的好處在於:讓UI層之外的所有層都不再去關注exception,全部由UI層hold住,同時完成異常的統一化處理,確保exception不會漏處理,提升服務的友好性。

  demo示例中,主要有如下幾類異常,項目中可以根據具體情況做擴展:

  •   BusinessException —— domain層拋出的業務領域相關異常,比如:用戶訪問的帖子不存在;
  •   CommunicationException —— 通訊相關異常,出現此異常,表明系統出現了故障,需要做友好提示;往往是和第三方服務交互時,第三方服務不可用或者響應超時;
  •   OutsideServiceException —— 第三方服務返回的業務異常,注意是業務異常,區別於CommunicationException在於:請求響應成功,但是業務上沒能成功;比如:調用用戶體系服務組件查詢用戶信息,用戶體系服務組件返回“查無此人”;
  •   DataAccessException —— 持久層返回的數據存取相關的異常,可能是程序bug或者系統故障,需要做友好提示。

類圖

UI層類圖

代碼示例  

  此demo的代碼已上傳至github,歡迎下載和討論,但拒絕被用於任何商業用途。

  github地址:https://github.com/daoqidelv/community-ddd-demo/tree/master

  branch:master

 


免責聲明!

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



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