【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

时间: 2024-10-14 05:06:28

【DDD】领域驱动设计实践 —— Application层实现的相关文章

DDD领域驱动设计之领域基础设施层

1.DDD领域驱动设计实践篇之如何提取模型 2.DDD领域驱动设计之聚合.实体.值对象 其实这里说的基础设施层只是领域层的一些接口和基类而已,没有其他的如日子工具等代码,仅仅是为了说明领域层的一些基础问题 1.领域事件简单实现代码,都是来至ASP.NET设计模式书中的代码 namespace DDD.Infrastructure.Domain.Events { public interface IDomainEvent { } } namespace DDD.Infrastructure.Dom

EntityFramework之领域驱动设计实践

EntityFramework之领域驱动设计实践 - 前言 EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject EntityFramework之领域驱动设计实践 (二):分层架构 EntityFramework之领域驱动设计实践 (三):案例:一个简易的销售系统 EntityFramework之领域驱动设计实践 (四):存储过程 - 领域驱动的反模式 EntityFramework之领域驱动设计实践 (五):聚合 EntityFramewor

(转)EntityFramework之领域驱动设计实践

EntityFramework之领域驱动设计实践 - 前言 EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject EntityFramework之领域驱动设计实践 (二):分层架构 EntityFramework之领域驱动设计实践 (三):案例:一个简易的销售系统 EntityFramework之领域驱动设计实践 (四):存储过程 - 领域驱动的反模式 EntityFramework之领域驱动设计实践 (五):聚合 EntityFramewor

DDD领域驱动设计之领域服务

1.DDD领域驱动设计实践篇之如何提取模型 2.DDD领域驱动设计之聚合.实体.值对象 3.DDD领域驱动设计之领域基础设施层 什么是领域服务,DDD书中是说,有些类或者方法,放实体A也不好,放实体B也不好,因为很可能会涉及多个实体或者聚合的交互(也可能是多个相同类型的实体),此时就应该吧这些代码放到领域服务中,领域服务其实就跟传统三层的BLL很相似,只有方法没有属性,也就没有状态,而且最好是用动词命名,service为后缀,但是真正到了实践的时候,很多时候是很难区分是领域实体本身实现还是用领域

DDD领域驱动设计初探(一):聚合

前言:又有差不多半个月没写点什么了,感觉这样很对不起自己似的.今天看到一篇博文里面写道:越是忙人越有时间写博客.呵呵,似乎有点道理,博主为了证明自己也是忙人,这不就来学习下DDD这么一个听上去高大上的东西.前面介绍了下MEF和AOP的相关知识,后面打算分享Automapper.仓储模式.WCF等东西的,可是每次准备动手写点什么的时候,就被要写的Demo难住了,比如仓储模式,使用过它的朋友应该知道,如果你的项目不是按照DDD的架构而引入仓储的设计,那么会让它变得很“鸡肋”,用不好就会十分痛苦,之前

(转载)浅谈我对DDD领域驱动设计的理解

原文地址:http://www.cnblogs.com/netfocus/p/5548025.html 从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品.所以,自然而然就想到要做一个普通电商系统,用于实现在线销售自己企业产品的目的. 再比如,我是一家互联网公司,公司有很多系统对外提供服务,面向很多客户端设备.但是最近由于各种原因,导致服务经常出故

WCF客户端配置以及代理-----基于DDD领域驱动设计的WCF+EF+WPF分层框架(4)

写在最前面:转载请注明出处 目录置顶: 关于项目--------------------基于DDD领域驱动设计的WCF+EF+WPF分层框架(1) 架构搭建--------------------基于DDD领域驱动设计的WCF+EF+WPF分层框架(2) WCF服务端具体实现---------基于DDD领域驱动设计的WCF+EF+WPF分层框架(3) WCF客户端配置以及代理-----基于DDD领域驱动设计的WCF+EF+WPF分层框架(4) Domain具体实现------------基于DD

DDD领域驱动设计之聚合、实体、值对象

关于具体需求,请看前面的博文:DDD领域驱动设计实践篇之如何提取模型,下面是具体的实体.聚合.值对象的代码,不想多说什么是实体.聚合等概念,相信理论的东西大家已经知晓了.本人对DDD表示好奇,没有在真正项目实践过,甚至也没有看过真正的DDD实践的项目源码,处于极度纠结状态,甚至无法自拔,所以告诫DDD爱好者们,如果要在项目里面实践DDD,除非你对实体建模和领域职责非常了解(很多时候会纠结一些逻辑放哪里好,属于设计问题)以及你的团队水平都比较高认同DDD,否则请慎重...勿喷! 代码在后,请先看D

DDD领域驱动设计仓储Repository

DDD领域驱动设计初探(二):仓储Repository(上) 前言:上篇介绍了DDD设计Demo里面的聚合划分以及实体和聚合根的设计,这章继续来说说DDD里面最具争议的话题之一的仓储Repository,为什么Repository会有这么大的争议,博主认为主要原因无非以下两点:一是Repository的真实意图没有理解清楚,导致设计的紊乱,随着项目的横向和纵向扩展,到最后越来越难维护:二是赶时髦的为了“模式”而“模式”,仓储并非适用于所有项目,这就像没有任何一种架构能解决所有的设计难题一样.本篇