DDD入门

简介

整理至: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) {
    	......
    }
}
  • 此时的问题:

    1. register入参不明确,都是string,编译后三方系统调用不知道参数含义
    2. register可扩展性不强,增加入参就得新写方法
    3. 校验参数 与 业务实现 耦合在一块
  • 解决方案:创建 入参的领域模型,用 领域模型 构建合法的入参对象

1. DP(Domain Primitive) 领域基础类型

  • 作用:
    抽象并自检一些隐形属性的计算逻辑,属性无状态

自定义类型PhoneNumber,将校验逻辑放入PhoneNumber,让PhoneNumber构建合法的入参

  • 作用:
    1. 接口语义清晰
    2. 参数校验异常 与 业务逻辑异常分离
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;
    }
}
  • 作用
    1. 隐形概念显性化
    2. 隐形上下文显性化
    3. 封装域方法

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案例的基础上 新增需求:
    1. 引入外部依赖TelcomRealnameService 电话实名查询,返回外部的TelecomDTO对象
    2. 引入外部依赖 RiskControlService 检查风控
    3. 增加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);
	}
}
  • 现在的问题:
    1. 依赖外部服务多,依赖外部Service与DTO,耦合非常高。
      外部依赖的接口、对象一有变化 整个代码都要改
    2. 面向数据表编程,依赖DAO和DO。
      业务逻辑只面向领域实体,不应该依赖具体的数据库表对象
  • 解决思路:
    面向具体实现编程 改为 面向抽象接口编程
    当外部依赖的接口、对象有变化时,理论上只需改接口实现类

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作用不大

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值