文章目录
简介
整理至:https://www.bilibili.com/video/BV11q4y1q74f?share_source=copy_web
有理解错误的地方欢迎留言指出
模块化、微服务等技术 目的是降低软件耦合,降低软件开发与维护的复杂度
DDD 目的是降低业务耦合
通过改造一个案例 逐步理解DDD
用户注册 register方法
入参:姓名,手机号
逻辑:获取手机号相关信息 并划分到区域销售下,构建用户对象,持久化用户
注:以下代码全当做伪代码,只为方便看DDD的思路大概敲下,不可正常运行
>>>原始代码:
public class RegistrationServiceImpl implements RegistrationService {
//销售组数据库对象
private SalesRepRepo salesRepRepo;
//用户数据库对象
private UserRepo userRepo;
public User register(String name, String phone) throws ValidationException {
// 1. 参数校验
if (name == null || name.length() == 0) {
throw new ValidationException("name");
}
if (phone == null || !isValidPhoneNumber(phone)) {
throw new ValidationException("phone");
}
// 2.1 获取手机号里的归属地编码与运营商编码
String areaCode = getAreaCode(phone);
String operatorCode = getOperatorCode(phone);
// 2.2 通过编号找到区域内的销售组
SalesRep rep = salesRepRepo.findRep(areaCode, operatorCode);
// 3. 最后创建用户,落库
User user = new User();
user.name = name;
user.phone = phone;
user.repId = rep.repId;
return userRepo.save(user);
}
private boolean isValidPhoneNumber(String phone) {
String pattern = "^0[1-9]{2,3}-?\\d{8}$";
return phone.matches(pattern);
}
private String getAreaCode(String phone) {
......
}
private String getOperatorCode(String phone) {
......
}
}
-
此时的问题:
- register入参不明确,都是string,编译后三方系统调用不知道参数含义
- register可扩展性不强,增加入参就得新写方法
- 校验参数 与 业务实现 耦合在一块
-
解决方案:创建 入参的领域模型,用 领域模型 构建合法的入参对象
1. DP(Domain Primitive) 领域基础类型
- 作用:
抽象并自检一些隐形属性的计算逻辑,属性无状态
自定义类型PhoneNumber,将校验逻辑放入PhoneNumber,让PhoneNumber构建合法的入参
- 作用:
- 接口语义清晰
- 参数校验异常 与 业务逻辑异常分离
public class PhoneNumber {
private final String number;
private final String pattern = "^0[1-9]{2,3}-?\\d{8}$";
// 1. 参数校验,构建合法的入参对象
public PhoneNumber(String number) {
if (number == null) {
throw new ValidationException("number不能为空");
} else if (isValid(number)) {
throw new ValtdationException("number格式错误");
}
this.number = number;
}
private boolean isValid(String number) {
return number.matches(pattern);
}
public String getNumber() {
return number;
}
}
- 此时register方法改造如下
public User register(String name, PhoneNumber phone) throws ValidationException {
// 2.1 获取手机号里的归属地编码与运营商编码
String areaCode = getAreaCode(phone);
String operatorCode = getOperatorCode(phone);
// 2.2 通过编号找到区域内的销售组
SalesRep rep = salesRepRepo.findRep(areaCode, operatorCode);
// 3. 最后创建用户,落库
......
}
- 此时的问题:
register注册方法,本质功能应该只有:1拿用户信息,2存入数据库。
其他功能(如2.1获取手机号相关信息)不应该属于register注册方法 - 解决方案:划分业务领域
2. 业务域划分
获取手机号里的 getAreaCode 归属地编码与 getOperatorCode运营商编码 是属于电话号的隐形属性。 不应该属于用户注册领域,而是属于PhoneNumber 的功能
所以划分 注册域 与 手机号域,业务内聚 各实现各自的功能。
2.1 手机号域
public class PhoneNumber {
private final String number;
private final String pattern = "^0[1-9]{2,3}-?\\d{8}$";
// 1. 参数校验,构建合法的入参对象
public PhoneNumber(String number) {
if (number == null) {
throw new ValidationException("number不能为空");
} else if (isValid(number)) {
throw new ValtdationException("number格式错误");
}
this.number = number;
}
private boolean isValid(String number) {
return number.matches(pattern);
}
public String getNumber() {
return number;
}
// 获取归属地编码
public String getAreaCode() {
......
return areaCode;
}
// 获取运营商编码
public String getOperatorCode() {
......
return operatorCode;
}
}
- 作用
- 隐形概念显性化
- 隐形上下文显性化
- 封装域方法
2.2 注册域
public class RegistrationServiceImpl implements RegistrationService {
//销售组数据库对象
private SalesRepRepo salesRepRepo;
//用户数据库对象
private UserRepo userRepo;
public User register(String name, PhoneNumber phone) throws ValidationException {
// 获取用户信息
SalesRep rep = salesRepRepo.findRep(phone.getAreaCode(), phone.getOperatorCode());
// 最后创建用户,落库,然后返回
......
}
}
DP与pojo对比
MVC中pojo只有属性和getter、setter方法
DDD中PhoneNumber 包含 初始化、校验、属性处理 逻辑
>>>新增需求,引入更多的外部依赖
- 在DP案例的基础上 新增需求:
- 引入外部依赖TelcomRealnameService 电话实名查询,返回外部的TelecomDTO对象
- 引入外部依赖 RiskControlService 检查风控
- 增加RewardMapper落库
public class RegistrationServiceImpl implements RegistrationService {
// 外部依赖
private TelcomRealnameService telecomService;//电话实名查询服务
private RiskControlService riskControlService;//风控服务
// 数据库依赖
private SalesRepMapper salesRepDAO;//销售组对象
private UserMapper userDAO;//用户对象
private RewardMapper rewardDAO;//福利对象
public User register(String name, PhoneNumber phone) throws ValidationException {
// 获取用户销售组
salesRepDAO.select(phone.getAreaCode(), phone.getOperatorCode());
// 获取实名信息
TelecomDTO telecomDTO = telecomService.xxx(phone.getNumber());
// 检查用户风控
riskControlService.xxx(telecomDTO.getIdCard, telecomDTO.getLabel);
// 存储用户信息
userDAO.insert(xxx);
// 存储福利信息
rewardDAO.insert(xxx);
}
}
- 现在的问题:
- 依赖外部服务多,依赖外部Service与DTO,耦合非常高。
外部依赖的接口、对象一有变化 整个代码都要改 - 面向数据表编程,依赖DAO和DO。
业务逻辑只面向领域实体,不应该依赖具体的数据库表对象
- 依赖外部服务多,依赖外部Service与DTO,耦合非常高。
- 解决思路:
面向具体实现编程 改为 面向抽象接口编程
当外部依赖的接口、对象有变化时,理论上只需改接口实现类
3. 防腐层接口(解耦 外部依赖)
目的:RegistrationServiceImpl 不直接依赖 外部的 TelcomRealnameService和RiskControlService。
方案:创建中间接口:RegistrationServiceImpl依赖接口,TelcomRealnameService和RiskControlService实现接口。
通过接口隔离依赖 防腐
3.1 外部接口防腐层
- 创建实名服务 TelcomRealnameService的防腐层
// 1. 创建 实名DP
public class RealnameInfo {
private final String idCard; //身份证号
private final String name; //姓名
private final String label; //标签
......
}
// 2. 创建 实名防腐接口
public interface RealnameService {
RealnameInfo get(PhoneNumber phone);
}
// 3. 实现 实名接口依赖
public TelcomRealnameService implements RealnameService {
private TelcomRealnameService telecomService;//电话实名查询服务
@Override
public RealnameInfo get(PhoneNumber phone){
TelecomDTO telecomDto = telecomService.xxx(phone);
return telecomDto2realnameInfo(telecomDto);
}
}
- 原始代码改造为:
此时代码不直接依赖 TelcomRealnameService和TelecomDTO
public class RegistrationServiceImpl implements RegistrationService {
// 外部依赖
private RealnameService realnameService;//=====实名查询服务=====
private RiskControlService riskControlService;//风控服务
// 数据库依赖
private SalesRepMapper salesRepDAO;//销售组对象
private UserMapper userDAO;//用户对象
private RewardMapper rewardDAO;//福利对象
public User register(String name, PhoneNumber phone) throws ValidationException {
// 获取用户销售组
salesRepDAO.select(phone.getAreaCode(), phone.getOperatorCode());
// =====获取实名信息=====
RealnameInfo realnameInfo = realnameService.get(phone.getNumber());
// 检查用户风控
riskControlService.xxx(realnameInfo.getIdCard, realnameInfo.getLabel);
// 存储用户信息
userDAO.insert(xxx);
// 存储福利信息
rewardDAO.insert(xxx);
}
}
同理 改造riskControlService 风控服务依赖
4. Entity 领域实体对象(解耦userDO表对象)
抽象并封装数据库访问
数据库依赖,面向数据表编程(业务逻辑直接依赖DO 和DAO)
和外部依赖防腐接口一样的思路方法。:去依赖自己定义的接口与对象, 拿外部的依赖方法实现自己的接口。起到隔离防腐的作用
- 作用
抽象并封装对象有状态的逻辑
使业务逻辑面向 领域实体Entity,不关心领域实体的落库
用UserGateway防腐接口和User领域实体对象 隔离数据库UserMapper和UserDO表对象
4.1 user 领域实体对象
// 1. 领域实体对象 Entity
public class User {
private PhoneNumber phone; // 用户手机号 DP
private RealnameInfo realnameInfo; // 用户实名 DP
private SalesRepId salesRepId; // 销售组id
private Boolean fresh = false;
// 数据库依赖
private SalesRepMapper salesRepDAO;//销售组对象
// 构造方法
public User(RealnameInfo realnameInfo, PhoneNumber phone){
initRealnameInfo(realnameInfo);
initPhone(phone);
initSalesRepId(phone);
}
private initRealnameInfo(RealnameInfo realnameInfo){
......
}
private initPhone(PhoneNumber phone){
......
}
private initSalesRepId(PhoneNumber phone){
sales = salesRepDAO.select(phone.getAreaCode(), phone.getOperatorCode());
this.salesRepId = sales.getRepId();
}
// 改变状态
public void fresh(){
this.fresh = true;
}
}
Entity与DP对比
DP 无状态,是组成DP的基础类型
Entity 有状态
5. Repository (解耦userMapper数据库对象)
用 Entity 领域实体对象 + 防腐层接口 解耦数据库依赖
5.1 user 数据库防腐接口
// 2. 数据库防腐接口
public interface UserGateway {
User find(PhoneNumber phone);
User save(User user);
}
// 3. 数据库DB实现
public UserMapper implements UserGateway{
private UserMapper userDAO;
@Override
public User find(PhoneNumber phone){
UserDO userDo = userDAO.select(phone);
return userDo2User(userDo);
}
@Override
public User save(User user){
UserDO userDo = User2userDo(user);
userDAO.insert(userDo);
return userDo2User(userDo);
}
}
同样的方法改造 用RewardGateway防腐接口和Reward领域实体对象 隔离数据库RewardMapper和RewardDO表对象
- 原始代码改造为:
此时代码不直接依赖 数据库Mapper 和 表DO对象
public class RegistrationServiceImpl implements RegistrationService {
// 外部防腐接口依赖
private RealnameService realnameService;//实名查询服务
private RiskControlService riskControlService;//风控服务
// 数据防腐接口依赖
private UserGateway userGateway;
private RewardGateway rewardGateway;
public User register(String name, PhoneNumber phone) throws ValidationException {
// 获取实名信息
RealnameInfo realnameInfo = realnameService.get(phone.getNumber());
// ====构造领域对象====
User user = new User(realnameInfo, phone);
Reward reward = Reward(user);
// ====检查用户风控====
if(riskControlService.check(user)){
// 改变user与reward领域对象 状态
user.fresh();
reward.inavailable();
}
// ====存储用户信息====
userGateway.insert(user);
// ====存储福利信息====
rewardGateway.insert(reward);
}
}
- 当前问题
注册域又不纯粹了,耦合了风控和福利代码,涉及到user和reward两个 领域实体对象 Entity - 解决方法
用Domain Service 领域服务 封装多个Entity逻辑
5. Domain Service 领域服务
- 作用
封装 多个Entity对象,跨业务域的逻辑
涉及多个 Entity 改变的服务,被称为 Domain Service
5.1 user 领域服务
public class UserDomainService{
private RiskControlService riskControlService;//风控服务
private RewardGateway rewardGateway;
public void checkAndUpdateUser (User user){
Reward reward = Reward(user);
// ====检查用户风控====
if(riskControlService.check(user)){
// 改变user与reward领域对象 状态
user.fresh();
reward.inavailable();
}
// ====存储福利信息====
rewardGateway.insert(reward);
}
}
- 原始代码改造为:
public class RegistrationServiceImpl implements RegistrationService {
// 外部防腐接口依赖
private RealnameService realnameService;//实名查询服务
// 数据防腐接口依赖
private UserGateway userGateway;
// 领域服务
private UserDomainService userDomainService;
public User register(String name, PhoneNumber phone) throws ValidationException {
// 获取信息
RealnameInfo realnameInfo = realnameService.get(phone.getNumber());
// 构造领域对象
User user = new User(realnameInfo, phone);
// 处理领域对象
userDomainService.checkAndUpdateUser(user);
// 存储信息
userGateway.insert(user);
}
}
对程序员无用的DDD
https://mp.weixin.qq.com/s/sY4tnmINQQ2N3dDcYQLHgQ
讲的很有道理,程序员是搞战术的,你DDD是扯战略忽悠人的、在战术阶段无意义
落到实处了的解耦设计模式、框架一大堆,ddd作用不大