作者:小傅哥
博客: https://bugstack.cn
沉淀、分享、成长,让自己和他人都能有所收获!
在上一章节介绍了领域驱动设计的基本概念以及按照领域驱动设计的思想进行代码分层,但是仅仅只是从一个简单的分层结构上依然没法理解DDD以及如何去开发这样的微服务。另外往往按照这样分层后依然感觉和MVC也没有什么差别,也没有感受到带来什么非常大的好处。那么问题出在哪呢?我个人觉得DDD学起来更像是一套指导思想,不断的将学习者引入到领域触发的思维中去,而这恰恰也是最难学习的地方。时而感觉会了,而实际开发中又不对,本来已经拆解清晰了,但怎么又那么像MVC了。甚至怀疑自己,我在干嘛?
无论是DDD、MVC,他们更像是家里三居或者四局的格局,每一种格局方式都是为了更好的实现对应架构下的设计思想。但,不是说给你一个通用的架构模式,你就能开发出干净(高内聚)、整洁(低耦合)、漂亮(模块化)的代码。这就像是你家住三居、他家也住三居,但是你们屋子的舒适情况就一样吗?{再有,你家里会把厕所安在厨房吗?但你的代码是否这么干过,不合理的摆放导致重构延期。}
另外DDD之所以看着简单但又不那么好落地,个人认为很重要就是领域思想,DDD只是指导但是不能把互联网天下每一个业务行为开发都拿出来举例子给你看,每个领域需要设计。所以需要一些领域专家{产品+架构+不是杠精的程序猿}来讨论梳理,将业务形态设计出合理的架构&代码。
如图;DDD分层结构 | 指导设计架构
通过领域驱动设计的思想,从领域知识中提取和划分为一个一个的子领域(核心子域,通用子域,支撑子域),并在子领域上建立模型。那么在技术实现上就需要去支撑这种建模,以使我们的代码模块独立、免污染、易于扩展。
在上面我们提到需要开发一个可扩展使用的规则树,那么如果只是单纯的一次性需求,最快的方式是if语句就搞定了。但是为了使这个领域服务具备良好的使用和扩展性,我们需要做些拆分,那么如下;
1、你是否想过系统在过滤过则的时候其实就像执行一棵二叉树一样非左即右侧,每一条线上都有着执行条件,通过判断来达到最终的结果。
2、按照树形结构我们将定义出来四个类;树、节点、果实、指向线(From-To),用于描述我们的规则行为。
3、再此基础上需要实现一个逻辑定义与规则树执行引擎,通过统一的引擎服务来执行我们每次配置好的规则树。
如图;领域开发设计服务
itstack-demo-ddd-02 └── src ├── main │ ├── java │ │ └── org.itstack.demo │ │ ├── application │ │ │ ├── MallRuleService.java │ │ │ └── MallTreeService.java │ │ ├── domain │ │ │ ├── rule │ │ │ │ ├── model │ │ │ │ │ ├── aggregates │ │ │ │ │ │ └── UserRichInfo.java │ │ │ │ │ └── vo │ │ │ │ │ ├── DecisionMatter.java │ │ │ │ │ ├── EngineResult.java │ │ │ │ │ ├── TreeNodeInfo.java │ │ │ │ │ ├── TreeNodeLineInfo.java │ │ │ │ │ └── UserSchool.java │ │ │ │ ├── repository │ │ │ │ │ └── IRuleRepository.java │ │ │ │ └── service │ │ │ │ ├── engine │ │ │ │ │ ├── impl │ │ │ │ │ └── EngineFilter.java │ │ │ │ ├── logic │ │ │ │ │ ├── impl │ │ │ │ │ └── LogicFilter.java │ │ │ │ └── MallRuleServiceImpl.java │ │ │ └── tree │ │ │ ├── model │ │ │ │ ├── aggregates │ │ │ │ │ └── TreeCollect.java │ │ │ │ └── vo │ │ │ │ ├── TreeInfo.java │ │ │ │ └── TreeRulePoint.java │ │ │ ├── repository │ │ │ │ └── ITreeRepository.java │ │ │ └── service │ │ │ └── MallTreeServiceImpl.java │ │ ├── infrastructure │ │ │ ├── common │ │ │ │ └── Constants.java │ │ │ ├── dao │ │ │ │ ├── RuleTreeDao.java │ │ │ │ ├── RuleTreeNodeDao.java │ │ │ │ └── RuleTreeNodeLineDao.java │ │ │ ├── po │ │ │ │ ├── RuleTree.java │ │ │ │ ├── RuleTreeConfig.java │ │ │ │ ├── RuleTreeNode.java │ │ │ │ └── RuleTreeNodeLine.java │ │ │ ├── repository │ │ │ │ ├── cache │ │ │ │ │ └── RuleCacheRepository.java │ │ │ │ ├── mysql │ │ │ │ │ ├── RuleMysqlRepository.java │ │ │ │ │ └── TreeMysqlRepository.java │ │ │ │ ├── RuleRepository.java │ │ │ │ └── TreeRepository.java │ │ │ └── util │ │ │ └── CacheUtil.java │ │ ├── interfaces │ │ │ ├── dto │ │ │ │ ├── DecisionMatterDTO.java │ │ │ │ └── TreeDTO.java │ │ │ └── DDDController.java │ │ └── DDDApplication.java │ └── resources │ ├── mybatis │ └── application.yml └── test └── java └── org.itstack.demo.test └── ApiTest.java
application/MallRuleService.java | 应用层定义接口服务,也可以适当做简单包装
/** * 商超规则过滤服务;提供规则树决策功能 * 微信公众号:bugstack虫洞栈 | 专注原创技术专题案例 * 论坛:http://bugstack.cn * Create by 小傅哥 on @2019 */ public interface MallRuleService { /** * 决策服务 * @param matter 决策物料 * @return 决策结果 */ EngineResult process(final DecisionMatter matter); }
domain中有两个领域服务;规则树信息领域、规则执行领域,通过合理的抽象化来实现高内聚、低耦合的模块化服务
domain/service/MallRuleServiceImpl.java | 领域层中的service来实现应用层接口
/** * 规则树服务;提供规则规律功能 * * 1、rule包下只进行规则决策领域的处理 * 2、封装决策行为到领域模型中,外部只需要调用和处理结果即可 * 3、可以扩展不同的决策引擎进行统一管理 * * 微信公众号:bugstack虫洞栈 | 专注原创技术专题案例 * 论坛:http://bugstack.cn * Create by 小傅哥 on @2019 */ @Service("mallRuleService") public class MallRuleServiceImpl implements MallRuleService { private Logger logger = LoggerFactory.getLogger(MallRuleServiceImpl.class); @Resource(name = "ruleEngineHandle") private EngineFilter ruleEngineHandle; @Override public EngineResult process(DecisionMatter matter) { try { return ruleEngineHandle.process(matter); } catch (Exception e) { logger.error("决策引擎执行失败", e); return new EngineResult(false); } } }
domain/service/logic/LogicFilter.java | 逻辑决策定义
/** * 微信公众号:bugstack虫洞栈 | 专注原创技术专题案例 * 论坛:http://bugstack.cn * Create by 付政委 on @2019 */ public interface LogicFilter { /** * 逻辑决策器 * @param matterValue 决策值 * @param treeNodeLineInfoList 决策节点 * @return 下一个节点Id */ Long filter(String matterValue, List<TreeNodeLineInfo> treeNodeLineInfoList); /** * 获取决策值 * * @param decisionMatter 决策物料 * @return 决策值 */ String matterValue(DecisionMatter decisionMatter); }
domain/service/engine/EngineFilter.java | 引擎执行定义
/** * 微信公众号:bugstack虫洞栈 | 专注原创技术专题案例 * 论坛:http://bugstack.cn * Create by 小傅哥 on @2019 */ public interface EngineFilter { EngineResult process(final DecisionMatter matter) throws Exception; }
infrastructure/repository/RuleRepository.java
/** * 微信公众号:bugstack虫洞栈 | 专注原创技术专题案例 * 论坛:http://bugstack.cn * Create by 小傅哥 on @2019 */ public interface EngineFilter { EngineResult process(final DecisionMatter matter) throws Exception; }
interfaces/DDDController.java
** * 微信公众号:bugstack虫洞栈 | 欢迎关注学习专题案例 * 论坛:http://bugstack.cn * Create by 小傅哥 on @2019 */ @Controller public class DDDController { private Logger logger = LoggerFactory.getLogger(DDDController.class); @Resource private MallTreeService mallTreeService; @Resource private MallRuleService mallRuleService; /** * 测试接口:http://localhost:8080/api/tree/queryTreeSummaryInfo * 请求参数:{"treeId":10001} */ @RequestMapping(path = "/api/tree/queryTreeSummaryInfo", method = RequestMethod.POST) @ResponseBody public ResponseEntity queryTreeSummaryInfo(@RequestBody TreeDTO request) { String reqStr = JSON.toJSONString(request); try { logger.info("查询规则树信息{}Begin req:{}", request.getTreeId(), reqStr); TreeCollect treeCollect = mallTreeService.queryTreeSummaryInfo(request.getTreeId()); logger.info("查询规则树信息{}End res:{}", request.getTreeId(), JSON.toJSON(treeCollect)); return new ResponseEntity<>(treeCollect, HttpStatus.OK); } catch (Exception e) { logger.error("查询规则树信息{}Error req:{}", request.getTreeId(), reqStr, e); return new ResponseEntity<>(e.getMessage(), HttpStatus.OK); } } /** * 测试接口:http://localhost:8080/api/tree/decisionRuleTree * 请求参数:{"treeId":10001,"userId":"fuzhengwei","valMap":{"gender":"man","age":"25"}} */ @RequestMapping(path = "/api/tree/decisionRuleTree", method = RequestMethod.POST) @ResponseBody public ResponseEntity decisionRuleTree(@RequestBody DecisionMatterDTO request) { String reqStr = JSON.toJSONString(request); try { logger.info("规则树行为信息决策{}Begin req:{}", request.getTreeId(), reqStr); DecisionMatter decisionMatter = new DecisionMatter(); decisionMatter.setTreeId(request.getTreeId()); decisionMatter.setUserId(request.getUserId()); decisionMatter.setValMap(request.getValMap()); EngineResult engineResult = mallRuleService.process(decisionMatter); logger.info("规则树行为信息决策{}End res:{}", request.getTreeId(), JSON.toJSON(engineResult)); return new ResponseEntity<>(engineResult, HttpStatus.OK); } catch (Exception e) { logger.error("规则树行为信息决策{}Error req:{}", request.getTreeId(), reqStr, e); return new ResponseEntity<>(e.getMessage(), HttpStatus.OK); } } }
规则树结构{数据库转Json} | 可自行定义
{ "treeNodeMap": { "1": { "nodeType": 1, "ruleDesc": "用户性别[男/女]", "ruleKey": "userGender", "treeId": 10001, "treeNodeId": 1, "treeNodeLineInfoList": [ { "nodeIdFrom": 1, "nodeIdTo": 11, "ruleLimitType": 1, "ruleLimitValue": "man" }, { "nodeIdFrom": 1, "nodeIdTo": 12, "ruleLimitType": 1, "ruleLimitValue": "woman" } ] }, "11": { "nodeType": 1, "ruleDesc": "用户年龄", "ruleKey": "userAge", "treeId": 10001, "treeNodeId": 11, "treeNodeLineInfoList": [ { "nodeIdFrom": 11, "nodeIdTo": 111, "ruleLimitType": 3, "ruleLimitValue": "25" }, { "nodeIdFrom": 11, "nodeIdTo": 112, "ruleLimitType": 3, "ruleLimitValue": "25" } ] }, "12": { "nodeType": 1, "ruleDesc": "用户年龄", "ruleKey": "userAge", "treeId": 10001, "treeNodeId": 12, "treeNodeLineInfoList": [ { "nodeIdFrom": 12, "nodeIdTo": 121, "ruleLimitType": 3, "ruleLimitValue": "25" }, { "nodeIdFrom": 12, "nodeIdTo": 122, "ruleLimitType": 3, "ruleLimitValue": "25" } ] }, "111": { "nodeType": 2, "nodeValue": "果实A", "treeId": 10001, "treeNodeId": 111, "treeNodeLineInfoList": [ ] }, "112": { "nodeType": 2, "nodeValue": "果实B", "treeId": 10001, "treeNodeId": 112, "treeNodeLineInfoList": [ ] }, "121": { "nodeType": 2, "nodeValue": "果实C", "treeId": 10001, "treeNodeId": 121, "treeNodeLineInfoList": [ ] }, "122": { "nodeType": 2, "nodeValue": "果实D", "treeId": 10001, "treeNodeId": 122, "treeNodeLineInfoList": [ ] } }, "treeRoot": { "treeId": 10001, "treeName": "购物分类规则树", "treeRootNodeId": 1 } }
通过postman调用 | raw => json
查询规则树信息
测试接口:` http://localhost :8080/api/tree/decisionRuleTree
请求参数: {"treeId":10001}
{ "treeInfo": { "treeId": 10001, "treeName": "购物分类规则树", "treeDesc": "用于分类不同类型用户可购物范围", "nodeCount": 7, "lineCount": 6 }, "treeRulePointList": [ { "ruleKey": "userGender", "ruleDesc": "用户性别[男/女]" }, { "ruleKey": "userAge", "ruleDesc": "用户年龄" } ] }
测试接口: http://localhost:8080/api/tree/decisionRuleTree
请求参数: {"treeId":10001}
{ "userId": "fuzhengwei", "treeId": 10001, "nodeId": 112, "nodeValue": "果实B", "success": true }
. ____ _ __ _ _ /// / ___'_ __ _ _(_)_ __ __ _ / / / / ( ( )/___ | '_ | '_| | '_ // _` | / / / / /// ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_/__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.0.5.RELEASE) 2019-10-19 18:22:05.672 INFO 13820 --- [ main] org.itstack.demo.DDDApplication : Starting DDDApplication on fuzhengwei-PC with PID 13820 (E:/itstack/itstack.org/itstack-demo-ddd-02/target/classes started by fuzhengwei in E:/itstack/itstack.org/itstack-demo-ddd-02) 2019-10-19 18:22:05.675 INFO 13820 --- [ main] org.itstack.demo.DDDApplication : No active profile set, falling back to default profiles: default 2019-10-19 18:22:05.952 INFO 13820 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@3c4297f: startup date [Sat Oct 19 18:22:05 CST 2019]; root of context hierarchy 2019-10-19 18:22:07.756 INFO 13820 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2019-10-19 18:22:07.870 INFO 13820 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2019-10-19 18:22:07.870 INFO 13820 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.34 2019-10-19 18:22:07.896 INFO 13820 --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [D:/Program Files Java/Java/jdk1.8.0_162/bin;C:/Windows/Sun/Java/bin;C:/Windows/system32;C:/Windows;C:/ProgramData/Oracle/Java/javapath;C:/Windows/system32;C:/Windows;C:/Windows/System32/Wbem;C:/Windows/System32/WindowsPowerShell/v1.0/;D:/Program Files Java/SlikSvn/bin;D:/Program Files Java/MySQL Server 5.1/bin;D:/Program Files Java/TortoiseGit/bin;D:/Program Files/nodejs/;D:/Program Files Java/Java/jdk1.6.0_24/bin;D:/Program Files Java/apache-maven-3.2.3/bin;C:/Users/fuzhengwei/AppData/Roaming/npm;D:/Program Files Java/Git/cmd;;.] 2019-10-19 18:22:08.040 INFO 13820 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2019-10-19 18:22:08.040 INFO 13820 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2088 ms 2019-10-19 18:22:08.102 INFO 13820 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet dispatcherServlet mapped to [/] 2019-10-19 18:22:08.126 INFO 13820 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*] 2019-10-19 18:22:08.127 INFO 13820 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*] 2019-10-19 18:22:08.127 INFO 13820 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*] 2019-10-19 18:22:08.127 INFO 13820 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*] 2019-10-19 18:22:09.118 INFO 13820 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2019-10-19 18:22:09.383 INFO 13820 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@3c4297f: startup date [Sat Oct 19 18:22:05 CST 2019]; root of context hierarchy 2019-10-19 18:22:10.261 INFO 13820 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/tree/decisionRuleTree],methods=[POST]}" onto public org.springframework.http.ResponseEntity org.itstack.demo.interfaces.DDDController.decisionRuleTree(org.itstack.demo.interfaces.dto.DecisionMatterDTO) 2019-10-19 18:22:10.263 INFO 13820 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/tree/queryTreeSummaryInfo],methods=[POST]}" onto public org.springframework.http.ResponseEntity org.itstack.demo.interfaces.DDDController.queryTreeSummaryInfo(org.itstack.demo.interfaces.dto.TreeDTO) 2019-10-19 18:22:10.272 INFO 13820 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest) 2019-10-19 18:22:10.274 INFO 13820 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse) 2019-10-19 18:22:10.309 INFO 13820 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2019-10-19 18:22:10.309 INFO 13820 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2019-10-19 18:22:16.272 INFO 13820 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup 2019-10-19 18:22:16.273 INFO 13820 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'dataSource' has been autodetected for JMX exposure 2019-10-19 18:22:16.279 INFO 13820 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource] 2019-10-19 18:22:16.375 INFO 13820 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2019-10-19 18:22:16.381 INFO 13820 --- [ main] org.itstack.demo.DDDApplication : Started DDDApplication in 11.458 seconds (JVM running for 20.584) 2019-10-19 18:22:31.336 INFO 13820 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet' 2019-10-19 18:22:31.336 INFO 13820 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started 2019-10-19 18:22:31.372 INFO 13820 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 36 ms 2019-10-19 18:22:32.427 INFO 13820 --- [nio-8080-exec-1] o.itstack.demo.interfaces.DDDController : 规则树行为信息决策10001Begin req:{"treeId":10001,"userId":"fuzhengwei","valMap":{"gender":"man","age":"25"}} 2019-10-19 18:22:32.508 INFO 13820 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2019-10-19 18:22:32.956 INFO 13820 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2019-10-19 18:22:33.028 INFO 13820 --- [nio-8080-exec-1] o.i.d.d.rule.service.engine.EngineBase : 树引擎=>Test分类规则树 userId:fuzhengwei treeId:10001 treeNode:11 ruleKey:userGender matterValue:man 2019-10-19 18:22:33.028 INFO 13820 --- [nio-8080-exec-1] o.i.d.d.rule.service.engine.EngineBase : 树引擎=>Test分类规则树 userId:fuzhengwei treeId:10001 treeNode:112 ruleKey:userAge matterValue:25 2019-10-19 18:22:33.039 INFO 13820 --- [nio-8080-exec-1] o.itstack.demo.interfaces.DDDController : 规则树行为信息决策10001End res:{"treeId":10001,"nodeValue":"果实B","success":true,"nodeId":112,"userId":"fuzhengwei"} 2019-10-19 18:23:36.989 INFO 13820 --- [nio-8080-exec-5] o.itstack.demo.interfaces.DDDController : 查询规则树信息10001Begin req:{"treeId":10001} 2019-10-19 18:23:37.006 INFO 13820 --- [nio-8080-exec-5] o.itstack.demo.interfaces.DDDController : 查询规则树信息10001End res:{"treeInfo":{"treeId":10001,"treeName":"购物分类规则树","treeDesc":"用于分类不同类型用户可购物范围","nodeCount":7,"lineCount":6},"treeRulePointList":[{"ruleDesc":"用户性别[男/女]","ruleKey":"userGender"},{"ruleDesc":"用户年龄","ruleKey":"userAge"}]}