无论是DDD、MVC,他们更像是家里三居或者四局的格局,每一种格局方式都是为了更好的实现对应架构下的设计思想。但,不是说给你一个通用的架构模式,你就能开发出干净(高内聚)、整洁(低耦合)、漂亮(模块化)的代码。这就像是你家住三居、他家也住三居,但是你们屋子的舒适情况就一样吗?{再有,你家里会把厕所安在厨房吗?但你的代码是否这么干过,不合理的摆放导致重构延期。}
另外DDD之所以看着简单但又不那么好落地,个人认为很重要就是领域思想,DDD只是指导但是不能把互联网天下每一个业务行为开发都拿出来举例子给你看,每个领域需要设计。所以需要一些领域专家{产品+架构+不是杠精的程序猿}来讨论梳理,将业务形态设计出合理的架构&代码。
案例目标
本案例通过一个商品下单规则的场景来进行演示DDD;
- 假设产品需求业务运行人员可以对不同的商品配置一些规则,这些规则可以满足不同用户类型可以下单不同商品。
- 另外一些行为规则是会随着业务发展而增加或者变动的,所以不能写死{写死太吓人了}。
- 数据库的PO类不应该被外部服务调用,这也是必须的。如果你开发过很多系统,那么可能已经吃过亏并意识到这个问题。
- 按照DDD思想我们尝试需要设计一个规则引擎的服务,通过给外部提供非常简单的接口(application)来获取最终结果。
- 通过这样的案例可以很容易的感受到目前的四层架构确实在实现DDD思想上有很多的帮助。
如图;DDD分层结构 | 指导设计架构
bugstack虫洞栈 & 分层结构
DDD思想 · 开发设计
通过领域驱动设计的思想,从领域知识中提取和划分为一个一个的子领域(核心子域,通用子域,支撑子域),并在子领域上建立模型。那么在技术实现上就需要去支撑这种建模,以使我们的代码模块独立、免污染、易于扩展。
在上面我们提到需要开发一个可扩展使用的规则树,那么如果只是单纯的一次性需求,最快的方式是if语句就搞定了。但是为了使这个领域服务具备良好的使用和扩展性,我们需要做些拆分,那么如下;
- 你是否想过系统在过滤过则的时候其实就像执行一棵二叉树一样非左即右侧,每一条线上都有着执行条件,通过判断来达到最终的结果。
- 按照树形结构我们将定义出来四个类;树、节点、果实、指向线(From-To),用于描述我们的规则行为。
- 再此基础上需要实现一个逻辑定义与规则树执行引擎,通过统一的引擎服务来执行我们每次配置好的规则树。
如图;领域开发设计服务
bugstack虫洞栈 & 领域开发设计服务
工程模型
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应用层
application/MallRuleService.java | 应用层定义接口服务,也可以适当做简单包装
public interface MallRuleService {
@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 | 逻辑决策定义
public interface LogicFilter {
public interface EngineFilter {
EngineResult process(final DecisionMatter matter) throws Exception;
}
infrastructure基础层
1、实现领域层仓储定义 2、数据库操作为非业务属性的功能操作 3、在仓储实现层进行组合装配DAO&Redis&Cache等
infrastructure/repository/RuleRepository.java
public interface EngineFilter {
EngineResult process(final DecisionMatter matter) throws Exception;
}
interfaces接口层
1、包装应用接口对外提供api 2、外部传输对象采用DTO类,主要为了避免内部类被污染{不断的迭代的需求会在类中增加很多字段} 3、目前依然是提供的Http服务,如果提供的rpc服务,将需要对外提供可引用jar
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;
@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);
}
}
@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": "用户年龄"
}
]
}
微信公众号:bugstack虫洞栈 & 查询规则树信息
规则树行为信息决策 测试接口:http://localhost:8080/api/tree/decisionRuleTree 请求参数:{"treeId":10001}
{
"userId": "fuzhengwei",
"treeId": 10001,
"nodeId": 112,
"nodeValue": "果实B",
"success": true
}
微信公众号:bugstack虫洞栈 & 规则树行为信息决策
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: 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: [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> 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"}]}
综上总结
- 以上模拟购物场景下的规则处理抽象为树决策引擎,以达到独立领域服务。另外决策服务可以使用drools,任何抽象并不一定永远使用,不要拘泥于一种形式。
- 一些大型架构设计往往不是换一个设计模型就能彻底提升效率,还是需要人员整体素质,这是一个不断培养的过程。
- 领域驱动设计的思想并不只是教会程序猿写代码,也是非程序员以外的所有互联网人员都适合学习的内容。
- 家里住的舒适不舒适,并不一定取决于三居或者四居,大部分还是依赖于怎么对格局的布置。事必躬亲、亲力亲为的精益求精之路,终究会让你设计出更加合理的代码。