zoukankan      html  css  js  c++  java
  • 【DDD】领域驱动设计实践 —— Application层实现

      本文是DDD框架实现讲解的第二篇,主要介绍了DDD的Application层的实现,详细讲解了service、assemble的职责和实现。文末附有github地址。相比于《领域驱动设计》原书中的航运系统例子,社交服务系统的业务场景对于大家更加熟悉,相信更好理解。本文是【DDD】系列文章的其中一篇,其他可参考:使用领域驱动设计思想实现业务系统

    Application层

      在DDD设计思想中,Application层主要职责为组装domain层各个组件及基础设施层的公共组件,完成具体的业务服务。Application层可以理解为粘合各个组件的胶水,使得零散的组件组合在一起提供完整的业务服务。在复杂的业务场景下,一个业务case通常需要多个domain实体参与进来,所以Application的粘合效用正好有了用武之地。

      Application层主要由:service、assembler组成,下面分别对其做讲解。

    Service

    service是组件粘合剂

      这里的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 entity
    25          * 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 entity
    57          * NOTE:这里的PostAuthor不需要从repository重载,原因在于:deletePost场景需要用户登录后才能操作,
    58          *         在进入service之前,已经在controller层完成了用户身份鉴权,故到达这里的userId肯定是合法的用户
    59          */
    60         PostAuthor postAuthor = new PostAuthor(userIdInLong);
    61         /**
    62          * 从repository中重载domain model entity
    63          * 借此判断该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     @Override
    76     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 }

    Assembler

    Assembler是组装器

      Assembler是组装器,负责完成domain model对象到dto的转换,组装职责包括:

    1. 完成类型转换、数据格式化;如日志格式化,状态enum装换为前端认识的string;
    2. 将多个domain领域对象组装为需要的dto对象,比如查询帖子列表,需要从Post(帖子)领域对象中获取帖子的详情,还需要从User(用户)领域对象中获取用户的社交信息(昵称、简介、头像等);
    3. 将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 @Component
    10 public class PostAssembler {
    11     
    12     private final static String POSTING_TIME_STRING_DATE_FORMAT = "yyyy-MM-dd hh:mm:ss";
    13     
    14     @Autowired
    15     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是否为null
    29          */
    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

  • 相关阅读:
    Webkit CSS properties
    轻量级前端MVVM框架avalon
    ExtJS4 源码解析(一)带项目分析
    web app开发利器
    运用webkit绘制渲染页面原理解决iscroll4闪动的问题
    吐槽:基于PhoneGap开发移动项目
    轻量级前端MVVM框架avalon
    轻量级前端MVVM框架avalon
    WinDbg 命令三部曲:(一)WinDbg 命令手册
    Unit Testing with NSubstitute
  • 原文地址:https://www.cnblogs.com/daoqidelv/p/7589092.html
Copyright © 2011-2022 走看看