DDD5板斧:其一DP
一个简单的业务案例:
假设现在在做一个简单的数据统计系统,
地推员输入客户的姓名和手机号
根据客户手机号的归属地和所属的运营商
将客户群体分组,分配给相应的销售组,
由销售组跟进后续的业务
简单实现方式
我们定义了一个User类,一个注册接口的具体实现类,注册方法中先对参数进行校验,然后通过手机号获取归属地编号和运营商编号,存到user中,
public class RegistrationServiceImpl implements RegistrationService{
private SalesRepRepository salesRepRepo;
private UserRepository userRepo;
public User register(String name,String phone) throws ValidationException{
//第一步进行参数校验
if(name == null || name.length() == 0){
throw new ValidationException("name");
}
if(phone == null || !isValidPhoneNumber(phone)){
throw new ValidationException("phone");
}
//获取手机号归属地编号和运营商编号,然后通过编号找到salesRepo
String areaCode = getAreaCode(phone);
String operatorCode = getOperatorCode(phone);
SalesRep rep = salesRepRepo.findRep(areaCode,operatorCode);
//最后创建用户,落盘然后返回
User user = new User();
user.name = name;
user.phone = phone;
if(rep != null){
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)l
}
private String getAreaCode(String phone){
//获取归属地编号
}
private String getOperatorCode(String phone){
//获取运营商编号
}
上述实现存在的问题
1. 接口语义与参数校验是需要审视的点
接口定义需要明确修改的目标:
- 1.语义明确无歧义
- 2.拓展性强一些
- 3.具有一定的自检性
首先是参数校验部分,如果增加了新的参数校验逻辑,那么就需要修改代码,可以将ValidationUtil封装成类。但是还会出现两个缺点,第一个就是把参数异常和逻辑异常封装在了一起,第二个就是参数类型越来越多,那么工具类中的校验逻辑后随之不断膨胀,后续不太方便维护。
修改目标:
- 接口语义明确,可扩展性强,最好带有自检性
- 参数校验逻辑复用,内聚
- 参数校验异常和业务逻辑解耦。
将Phone封装成PhoneNumber类,这样的话就解决了这个问题
public class RegistrationServiceImpl implements RegistrationService{
private SalesRepRepository salesRepRepo;
private UserRepository userRepo;
public User register(String name,PhoneNumber phone) throws ValidationException{
//获取手机号归属地编号和运营商编号,然后通过编号找到salesRepo
String areaCode = getAreaCode(phone);
String operatorCode = getOperatorCode(phone);
SalesRep rep = salesRepRepo.findRep(areaCode,operatorCode);
//最后创建用户,落盘然后返回
User user = new User();
user.name = name;
user.phone = phone;
if(rep != null){
user.repId = rep.repId;
}
return userRepo.save(user);
}
private String getAreaCode(String phone){
//获取归属地编号
}
private String getOperatorCode(String phone){
//获取运营商编号
}
还有一个问题就是我们注册就是完成注册的,而不是要获取归属地编号和运营商编号,这两个业务也需要分离
改变胶水逻辑 不能进行缝缝补补 对入参进行改变
因此getAreaCode和getOperatorCode也需要拿出来。
public class RegistrationServiceImpl implements RegistrationService{
private SalesRepRepository salesRepRepo;
private UserRepository userRepo;
public User register(String name,PhoneNumber phone) throws ValidationException{
//获取手机号归属地编号和运营商编号,然后通过编号找到salesRepo
SalesRep rep = salesRepRepo.findRep(phoneNumber);
//最后创建用户,落盘然后返回
User user = new User();
user.name = name;
user.phone = phone;
if(rep != null){
user.repId = rep.repId;
}
return userRepo.save(user);
}
没有办法修改胶水逻辑 (不能改变接口)就应该把areaCode 和 operatorCode 也封装到PhoneNumber中
public class PhoneNumber{
private final String number;
private final String pattern = "^0?[1-9]{2,3}-?\\d{8}$";
public String getNumber(){
return number;
}
public PhoneNumber(String number){
if(number == null){
throw new ValidationException("number不能为空");
}else if(!isValid(number)){
throw new ValidationException("Number格式错误");
}
this.number = number;
}
public boolean isValid(String number){
return number.matches(pattern);
}
private String getAreaCode(){
//获取归属地编号
}
private String getOperatorCode(){
//获取运营商编号
}
}
改进后的注册逻辑就变了
public class RegistrationServiceImpl implements RegistrationService{
private SalesRepRepository salesRepRepo;
private UserRepository userRepo;
public User register(String name,PhoneNumber phone) throws ValidationException{
//获取手机号归属地编号和运营商编号,然后通过编号找到salesRepo
SalesRep rep = salesRepRepo.findRep(phone.getAreaCode(),phone.getOperatorCode());
//最后创建用户,落盘然后返回
User user = new User();
user.name = name;
user.phone = phone;
if(rep != null){
user.repId = rep.repId;
}
return userRepo.save(user);
}
private String getAreaCode(String phone){
//获取归属地编号
}
private String getOperatorCode(String phone){
//获取运营商编号
}
思考一个问题
目前获取归属地编号和获取运营商编号方法知识简单的字符串逻辑,没有外部依赖,内聚在PhoneNumber类中恰到好处。假设现在需要对手机号注册时的身份信息和入参中的用户名进行一致性校验,(获取手机号注册时的身份信息是一个外部依赖项)如果在PhoneNumber内部实现,让phoneNumber内依赖其他查询服务似乎是不合适的,那么,这部分逻辑该怎么抽象怎么写?
审视点:单元测试可行性
phoneNumber基本上不变,单元测试也基本不用变。有很强的复用性
DP(Domain primitive)域的基元
最简单的PhoneNumber写法如下
public class PhoneNumber{
private String number;
public String getNumber(){
return number;
}
public void setNumber(String number){
this.number = number;
}
}
这种POJO只包含属性值,属于贫血模型
我们新型的基本单元不仅有属性,还有属性相关的职责,是充血模型的一种,如何把握充血的强度,就需要经验了
public class PhoneNumber{
private final String number;
private final String pattern = "^0?[1-9]{2,3}-?\\d{8}$";
public String getNumber(){
return number;
}
public PhoneNumber(String number){
if(number == null){
throw new ValidationException("number不能为空");
}else if(!isValid(number)){
throw new ValidationException("Number格式错误");
}
this.number = number;
}
public boolean isValid(String number){
return number.matches(pattern);
}
private String getAreaCode(){
//获取归属地编号
}
private String getOperatorCode(){
//获取运营商编号
}
}
DP的定义:在DDD里面DP是一切模型、方法、架构的基础。
它是在特定领域、拥有精准定义、可以自我验证、拥有行为的对象。
可以认为是领域的最小组成部分
DP的三条原则:
- 让隐性的概念显性化
- 让隐形的上下文显性化
- 封装多对象行为