【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继承这个基础类。
类图

代码示例
public class BaseController {
private static Logger logger = LogManager.getLogger(BaseController.class);
@Autowired
private ApplicationUtil applicationUtil;
@Autowired
private ExceptionHandler exceptionHandler;
/**
* format 失败 response。
* @param e
* @return
*/
protected ResponseDto formatErrorResponse(final Exception e) {
ResponseDto responseDto = new ResponseDto();
//将response 的data body置为空
responseDto.setBody(null);
//依据异常类型进行分别处理,将异常信息转义为用户友好的提示信息
Map<String, String> exceptionMap = exceptionHandler.handle(e);
responseDto.setReturnCode(exceptionMap.get("errorCode"));
responseDto.setReturnMsg(exceptionMap.get("errorMsg"));
logger.debug("Response is: "+responseDto);
return responseDto;
}
/**
* format成功的response
* @param responseBody
* @return ResponseDto
*/
protected ResponseDto formatSuccessResponse(ResponseBody responseBody) {
ResponseDto responseDto = new ResponseDto();
//设置返回码和返回信息为成功
responseDto.setReturnCode(ReturnCode.SUCCESS);
responseDto.setReturnMsg(applicationUtil.getReturnMsg(ReturnCode.SUCCESS));
//将response 的data body置为实际的业务body
responseDto.setBody(responseBody);
logger.debug("Response is: "+responseDto);
return responseDto;
}
}
BaseController.java
@Controller
@RequestMapping("/post")
public class PostController extends BaseController { @Autowired
private PostService postService; /**
* 发布帖子
* @param requestDto
* @return ResponseDto
*/
@ResponseBody
@RequestMapping(value = "/posting", method = RequestMethod.POST)
public ResponseDto posting(@RequestBody @Valid RequestDto<PostingReqBody> requestDto) {
try {
PostingRespBody postingRespBody = postService.posting (requestDto);
return this.formatSuccessResponse(postingRespBody);
} catch (Exception e) {
return this.formatErrorResponse(e);
}
}
.......
}
PostController.java
DTO
DTO是controller和service之间数据传输的载体
DTO(Data Transfer Object),顾名思义,DTO用于组件/分层间传递数据,它是数据传递的载体。在这里我们用它作为 “controller <=> service” 之间传递数据的载体。controller传递一个RequestDto给service,service完成业务处理后,返回一个ReponseDto。
类图

请求中公共的属性(请求头)放置到RequestDto中。RequestDto持有一个RequestBody(请求体),包含各个场景的具体业务请求参数,所有的业务请求都会有自己的ReqBody,并继承至RequestBody。
ResponseDto同理,不再赘述。
示例代码
/**
* User Interface layer Data Transfer Object
* @author daoqidelv
* @createdate 2017年9月24日
*/
public interface UIDto { }
UIDto.java
public class RequestDto<T> implements UIDto {
/**
* 请求渠道
*/
private String channel;
/**
* 请求id
*/
private String requestId;
/**
* 对body使用validation
*/
@Valid
private T body;
public String getChannel() {
return channel;
}
public void setChannel(String channel) {
this.channel = channel;
}
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
public T getBody() {
return body;
}
public void setBody(T body) {
this.body = body;
}
}
RequestDto.java
public class ResponseDto implements UIDto{
/**
* 状态码
*/
private String returnCode;
/**
* 提示信息
*/
private String returnMsg;
/**
* 各个接口返回的数据
*/
private Object body;
public String getReturnCode() {
return returnCode;
}
public void setReturnCode(String returnCode) {
this.returnCode = returnCode;
}
public String getReturnMsg() {
return returnMsg;
}
public void setReturnMsg(String returnMsg) {
this.returnMsg = returnMsg;
}
public Object getBody() {
return body;
}
public void setBody(Object body) {
this.body = body;
}
}
ResponseDto
public class RequestBody {
}
RequestBody.java
public class ResponseBody {
}
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
public class PostingRespBody extends ResponseBody {
private String postId;
public String getPostId() {
return postId;
}
public void setPostId(String postId) {
this.postId = postId;
}
}
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
【DDD】领域驱动设计实践 —— UI层实现的更多相关文章
- DDD领域驱动设计之运用层代码
1.DDD领域驱动设计实践篇之如何提取模型 2.DDD领域驱动设计之聚合.实体.值对象 3.DDD领域驱动设计之领域基础设施层 4.DDD领域驱动设计之领域服务 5.整体DEMO代码 什么是运用层,说 ...
- 【DDD】领域驱动设计实践 —— Application层实现
本文是DDD框架实现讲解的第二篇,主要介绍了DDD的Application层的实现,详细讲解了service.assemble的职责和实现.文末附有github地址.相比于<领域驱动设计> ...
- 【DDD】领域驱动设计实践 —— Domain层实现
本文是DDD框架实现讲解的第三篇,主要介绍了DDD的Domain层的实现,详细讲解了entity.value object.domain event.domain service的职责,以及如何识别出 ...
- DDD领域驱动设计和实践(转载)
-->目录导航 一. DDD领域驱动设计介绍 1. 什么是领域驱动设计(DDD) 2. 领域驱动设计的特点 3. 如果不使用DDD? 4. 领域驱动设计的分层架构和构成要素 5. 事务脚本和领域 ...
- DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(2)
上一篇:<DDD 领域驱动设计-谈谈 Repository.IUnitOfWork 和 IDbContext 的实践(1)> 阅读目录: 抽离 IRepository 并改造 Reposi ...
- DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(1)
好久没写 DDD 领域驱动设计相关的文章了,嘎嘎!!! 这几天在开发一个新的项目,虽然不是基于领域驱动设计的,但我想把 DDD 架构设计的一些东西运用在上面,但发现了很多问题,这些在之前的短消息项目中 ...
- C#进阶系列——DDD领域驱动设计初探(七):Web层的搭建
前言:好久没更新博客了,每天被该死的业务缠身,今天正好一个模块完成了,继续来完善我们的代码.之前的六篇完成了领域层.应用层.以及基础结构层的部分代码,这篇打算搭建下UI层的代码. DDD领域驱动设计初 ...
- DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(转)
http://www.cnblogs.com/xishuai/p/ddd-repository-iunitofwork-and-idbcontext.html 好久没写 DDD 领域驱动设计相关的文章 ...
- DDD领域驱动设计初探(七):Web层的搭建
前言:好久没更新博客了,每天被该死的业务缠身,今天正好一个模块完成了,继续来完善我们的代码.之前的六篇完成了领域层.应用层.以及基础结构层的部分代码,这篇打算搭建下UI层的代码. DDD领域驱动设计初 ...
随机推荐
- TortoiseSVN使用经验
TortoiseSVN使用经验.. SVN新下载的项目(有感叹号和对勾)正常的-- 没有标识--重启一下电脑, ---------------ipConfig----- SVN是用哪个IP地址chec ...
- PyQt4 初试牛刀一
建立了一个MainWindow,创建最基本的菜单栏.状态栏.工具栏,并重新定义了"X"关闭的默认行为. # -*- coding: utf-8 -*- import sys fro ...
- java创建目录与文件
java创建目录与文件: package com.led.file; import java.io.File; import java.io.IOException; public class Fil ...
- JSP慕课网阶段用户登录小例子(不用数据库)
getAttribute和setAttribute一起使用,而getParameter用于取得如request传来的参数. Web是请求/响应架构的使用,而request和response就是在服务器 ...
- 数据结构之合并链表STL
#include <iostream> #include <list> using namespace std; int main() { int n, m; while (c ...
- C# 使用NPOI 实现Excel的简单导入导出
private void btnImport_Click(object sender, EventArgs e) { DataSet ds = new DataSet(); DataTable dt ...
- jQuery遍历-过滤
缩写搜索元素的范围 三个最基本的过滤方法是:first(), last() 和 eq(),它们允许您基于其在一组元素中的位置来选择一个特定的元素. 其他过滤方法,比如 filter() 和 not() ...
- django框架(Model)
-------------------使用MySql数据库-------------------1.进行对应mysql-python包的下载 pip install mysql-python 2.在m ...
- 自己动手写fullPage插件
仿造fullPage.js https://alvarotrigo.com/fullPage/#firstPage 自己参照网上教程写了一个,加了注释.主要是练习造轮子的能力,需求是不断变化的只拿来用 ...
- SessionStateMode之Redis共享session
周六的时候用SQL Server来实现session共享,今天下班早就试了下使用Redis来实现session共享.接着上一篇博客,上一篇使用sessionState节点mode="SQLS ...