如果不了解DDD基本概念的读者可以去看这篇文章,传送门:DDD架构思想专栏一《初识领域驱动设计DDD落地》-CSDN博客
在上一章节介绍了领域驱动设计的基本概念以及按照领域驱动设计的思想进行代码分层,但是仅仅只是从一个简单的分层结构上依然没法理解DDD以及如何去开发这样的微服务。另外往往按照这样分层后依然感觉和MVC也没有什么差别,也没有感受到带来什么非常大的好处。那么问题出在哪呢?我个人觉得DDD学起来更像是一套指导思想,不断的将学习者引入到领域触发的思维中去,而这恰恰也是最难学习的地方。时而感觉会了,而实际开发中又不对,本来已经拆解清晰了,但怎么又那么像MVC了。甚至怀疑自己,我在干嘛?
无论是DDD、MVC,他们更像是家里三居或者四局的格局,每一种格局方式都是为了更好地实现对应架构下的设计思想。但,不是说给你一个通用的架构模式,你就能开发出干净(高内聚)、整洁(低耦合)、漂亮(模块化)的代码。这就像是你家住三居、他家也住三居,但是你们屋子的舒适情况就一样吗?(再有,你家里会把厕所安在厨房吗?但你的代码是否这么干过,不合理的摆放导致重构延期。)
另外DDD之所以看着简单但又不那么好落地,个人认为很重要就是领域思想,DDD只是指导但是不能把互联网天下每一个业务行为开发都拿出来举例子给你看,每个领域需要设计。所以需要一些领域专家来讨论梳理,将业务形态设计出合理的架构&代码。
下面我会通过实战来演示一个领域是如何依靠领域层的决策设计思想设计出来的。
本案例通过一个商品下单规则的场景来进行演示DDD领域设计实战;
- 假设产品需求业务运行人员可以对不同的商品配置一些规则,这些规则可以满足不同用户类型可以下单不同商品。
- 另外一些行为规则是会随着业务发展而增加或者变动的,所以不能写死。
- 数据库的PO类不应该被外部服务调用,这也是必须的。如果你开发过很多系统,那么可能已经吃过亏并意识到这个问题。
- 按照DDD思想我们尝试需要设计一个规则引擎的服务,通过给外部提供非常简单的接口(application)来获取最终结果。
- 通过这样的案例可以很容易的感受到目前的四层架构确实在实现DDD思想上有很多的帮助。
那么我来讲一下我的设计思路吧
通过领域驱动设计的思想,从领域知识中提取和划分为一个一个的子领域(核心子域,通用子域,支撑子域),并在子领域上建立模型。那么在技术实现上就需要去支撑这种建模,以使我们的代码模块独立、免污染、易于扩展。
那么我们应该采用怎样的结构来设计项目的分层使各模块独立、免污染、易于扩展呢?我们可以想到开发一个可扩展使用的规则树,由于树的特殊结构我们可以在树的底部或者任意节点对业务逻辑(行为规则)进行扩充。
那么我们应该怎么将整条服务流程拆分成一个规则树呢?
- 业务执行流程其实就是不断地在已有的小模块中(每个模块中对应一段业务逻辑)中进行选择,将这些选择的小模块合起来就是一条完整的业务流程,也就是规则树的一条遍历路径。因此我们可以通过在树的每一条线上都设置相关的执行条件,如果满足该执行条件则沿着这条线到达下一个子节点,通过判断来达到最终的结果。
- 定义相关的实体类(注意要为充血模型)按照树形结构我们将定义出来四个类;树、节点、果实、指向线(From-To),用于描述我们的规则行为。
- 需要实现一个逻辑定义与规则树执行引擎,通过统一的引擎服务来执行我们每次配置好的规则树
如下图就是一个领域层的简单规则树的示例
下面仅展示部分核心代码,想要完整代码的请下载绑定资源
application/MallRuleService.java | 应用层定义接口服务,也可以适当做简单包装
package com.kjz.application;
import com.kjz.domain.rule.model.vo.DecisionMatter;
import com.kjz.domain.rule.model.vo.EngineResult;
/**
* 商超规则过滤服务;提供规则树决策功能
*/
public interface MallRuleService {
/**
* 决策服务
* @param matter 决策物料
* @return 决策结果
*/
EngineResult process(final DecisionMatter matter);
}
domain中有两个领域服务;规则树信息领域、规则执行领域,通过合理的抽象化来实现高内聚、低耦合的模块化服务
domain/service/MallRuleServiceImpl.java | 领域层中的service来实现应用层接口
package com.kjz.domain.rule.service;
import com.kjz.application.MallRuleService;
import com.kjz.domain.rule.model.vo.DecisionMatter;
import com.kjz.domain.rule.model.vo.EngineResult;
import com.kjz.domain.rule.service.engine.EngineFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 规则树服务;提供规则规律功能
*
* 1、rule包下只进行规则决策领域的处理
* 2、封装决策行为到领域模型中,外部只需要调用和处理结果即可
* 3、可以扩展不同的决策引擎进行统一管理
*
*/
@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 | 逻辑决策定义
package com.kjz.domain.rule.service.logic;
import com.kjz.domain.rule.model.vo.DecisionMatter;
import com.kjz.domain.rule.model.vo.TreeNodeLineInfo;
import java.util.List;
public interface LogicFilter {
/**
* 逻辑决策器
* @param matterValue 决策值
* @param treeNodeLineInfoList 决策节点
* @return 下一个节点Id
*/
Long filter(String matterValue, List treeNodeLineInfoList);
/**
* 获取决策值
*
* @param decisionMatter 决策物料
* @return 决策值
*/
String matterValue(DecisionMatter decisionMatter);
}
domain/service/engine/EngineFilter.java | 引擎执行定义
package com.kjz.domain.rule.service.engine;
import com.kjz.domain.rule.model.vo.DecisionMatter;
import com.kjz.domain.rule.model.vo.EngineResult;
public interface EngineFilter {
EngineResult process(final DecisionMatter matter) throws Exception;
}
1、实现领域层仓储定义 2、数据库操作为非业务属性的功能操作 3、在仓储实现层进行组合装配DAO&Redis&Cache等
infrastructure/repository/RuleRepository.java
package com.kjz.infrastructure.repository;
import com.kjz.domain.rule.model.aggregates.TreeRuleRich;
import com.kjz.domain.rule.repository.IRuleRepository;
import com.kjz.infrastructure.repository.cache.RuleCacheRepository;
import com.kjz.infrastructure.repository.mysql.RuleMysqlRepository;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
@Repository("ruleRepository")
public class RuleRepository implements IRuleRepository {
@Resource(name = "ruleMysqlRepository")
private RuleMysqlRepository ruleMysqlRepository;
@Resource(name = "ruleCacheRepository")
private RuleCacheRepository ruleCacheRepository;
@Override
public TreeRuleRich queryTreeRuleRich(Long treeId) {
TreeRuleRich treeRuleRich = ruleCacheRepository.queryTreeRuleRich(treeId);
if (null != treeRuleRich) return treeRuleRich;
return ruleMysqlRepository.queryTreeRuleRich(treeId);
}
}
1、包装应用接口对外提供api 2、外部传输对象采用DTO类,主要为了避免内部类被污染(不断的迭代的需求会在类中增加很多字段) 3、目前依然是提供的Http服务,如果提供的rpc服务,将需要对外提供可引用jar
interfaces/DDDController.java
package com.kjz.interfaces;
import com.alibaba.fastjson.JSON;
import com.kjz.application.MallRuleService;
import com.kjz.application.MallTreeService;
import com.kjz.domain.rule.model.vo.DecisionMatter;
import com.kjz.domain.rule.model.vo.EngineResult;
import com.kjz.domain.tree.model.aggregates.TreeCollect;
import com.kjz.interfaces.dto.DecisionMatterDTO;
import com.kjz.interfaces.dto.TreeDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
@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);
}
}
}
测试接口:http://localhost:8080/api/tree/decisionRuleTree 请求参数:{"treeId":10001}
{
"treeInfo": {
"treeId": 10001,
"treeName": "购物分类规则树",
"treeDesc": "用于分类不同类型用户可购物范围",
"nodeCount": 7,
"lineCount": 6
},
"treeRulePointList": [
{
"ruleKey": "userGender",
"ruleDesc": "用户性别[男/女]"
},
{
"ruleKey": "userAge",
"ruleDesc": "用户年龄"
}
]
}
利用postman测试得出的结果如下
测试接口:http://localhost:8080/api/tree/decisionRuleTree 请求参数:{"treeId":10001}
{
"userId": "fuzhengwei",
"treeId": 10001,
"nodeId": 112,
"nodeValue": "果实B",
"success": true
}
postman测试结果如下