防腐层
1.为什么需要
在实际开发中,本地系统通常需要与外部系统进行交互。这些外部系统可能是遗留系统,其他团队开发的系统或第三方服务。外部系统具有自己独立的数据结构和操作方式,与本地上下文的领域模型存在差异。如果直接将外部系统的数据结构和操作方式引入领域模型,往往会导致许多问题,例如命名冲突,数据类型不匹配,业务逻辑不一致,以及外部上下文模型的变更对本地上下文模型的改动等
举个例子,在支付系统中存在支付与退款的业务逻辑,这两个业务逻辑都要使用支付单这个模型,在传统的开发中这两个业务逻辑会共享一个支付单模型这会导致单个模型的复杂度直线上升(因为这个模型要满足很多业务逻辑),而且当业务逻辑产生差异时,这种单模型问题会更多,在DDD中推崇模型分解的方式,不同的问题域应当使用不同的模型
2.介绍
防腐层(Anti-Corruption Layer,ACL)是DDD中的一个重要概念,它是一种用于隔离外部上下文的方法,可以保持本地上下文的领域模型的独立性和纯净性
多个上下文一般不共享一个模型,如果要使用其它上下文的模型,请使用防腐层进行转换,把外部模型转换成本地模型
3.作用
防腐层的作用是将外部上下文接口返回的模型转换为本地上下文定义的领域模型,并将本地上下文的操作转换为对外部上下文的操作。防腐层可以有效地隔离外部上下文的领域模型,避免外部上下文对本地上下文领域模型的影响
3.例子理解没有防腐层的问题
背景
B上下文对外提供的RPC接口BRpcQueryService相关的伪代码如下:
/**
* B上下文的开放主机服务,提供查询服务
*/
public interface BRpcQueryService {
/**
* 查询B上下文数据
*/
BResponse<BView> query(BQuery query);
}
public class BQuery {
private Integer property1;
private String property2;
// 省略其他属性和方法
}
public class BView {
private Integer property1;
private String property2;
// 省略其他属性和方法
}
当A上下文调用B上下文的BRpcQueryService接口服务下的query方法时,将得到BView类型的查询结果。如果A上下文的领域模型中直接引用了BView,那么将导致A自己的上下文被污染。这会引发一系列问题
类级别的问题
随着B上下文的迭代,BView类的路径,名称,属性名可能都会被改变
举个例子,B上下文可能会进行系统重构,重构后的新服务需要通过新的jar包进行调用,B的开发团队要求调用方切换到新的jar包上。在新的jar包中,类与方法与包名等都发生了变化,比如做出了如下调整:
// 比如包名变了
/**
* 原BRpcQueryService
*/
public interface BQueryServiceProvider {
/**
* 方法整体也变了
*/
BResponse<BView> queryOne(BQuery query);
}
// 比如包名变了
public class BQuery {
private Integer property1;
private String property2;
// 省略其他属性和方法
}
// 比如包名变了
public class BView {
private Integer property1;
private String property2;
// 省略其他属性和方法
}
可以看到,如果直接将BQuery和BView引入本地上下文,一旦BQuery和BView发生变化,A上下文的领域模型将需要大量的改动,并且需要大量的回归测试才能确保切换无风险
属性级别的问题
- BView中的某个属性与A上下文中同名的字段代表的业务含义相同,但有可能BView中的某个属性的类型与A上下文中对应的属性类型不一致,因而使用时必须进行强转
- BView中某个字段的名称与A上下文某个字段的名称相同,但表示含义南辕北辙,调用时容易引起歧义,容易混淆
4.实现方案
介绍
通常使用设计模式中的适配器模式实现防腐层
适配器模式
介绍
适配器模式是一种常见的设计模式,它主要用于将一个类的接口转换成客户端所期望的另一种接口,从而使得原本由于接口不兼容而无法一起工作的类可以协同工作。适配器模式属于结构型模式,它通过引入一个适配器类来完成不兼容接口之间的转换
类适配器方式实现
类适配器是通过继承来实现适配器的,适配器继承自需要适配的类,并实现目标接口。需要注意的是,这种实现方法需要重写需要适配的类中的方法,类图如下:
伪代码如下:
/**
* 被适配者
*/
public class Adaptee {
public void specificRequest () {
System.out.println("Adaptee#specificRequest.");
}
}
/**
* 目标接口
*/
public interface Target {
/**
* 目标方法
*/
void request();
}
/**
* 类适配器
*/
public class Adapter extends Adaptee implements Target {
@Override
public void request () {
// 执行Adaptee中的specificRequest方法
specificRequest();
}
}
public class Client {
public static void main(String[] args) {
// 创建适配器对象,声明为目标接口类型
Target target = new Adapter();
// 执行目标方法
target.request();
}
}
对象适配器方式实现
对象适配器是通过组合来实现适配器的。适配器包含需要适配的类的一个对象,并且实现目标接口。这种实现方法不需要重写所有适配的类中的方法
/**
* 对象适配器
*/
public class Adapter implements Target {
/**
* 可以通过多种方式注入适配器对象
*/
private Adaptee adaptee = new Adaptee();
@Override
public void request () {
// 执行Adaptee中的specificRequest方法
this.adaptee.specificRequest();
}
}
实现步骤
- 在领域层定义一个服务网关接口
- 在基础设施层实现领域层定义的网关接口
- 完成外部上下文的调用适配
- 应用层使用
在领域层定义一个服务网关接口
在领域层定义一个服务网关接口,在该网关中定义了本地上下文中期望的行为
假设需要对B上下文进行读和写,该网关接口BContextGateway的伪代码如下:
/**
* 调用B限界上下文的网关
*/
public interface BContextGateway {
/**
* 查询某个值
*/
ValueObject queryMethod(SomeValue someValue);
/**
* 向B上下文发起某个命令操作
*/
void commandMethod(SomeValue someValue);
}
在基础设施层实现领域层定义的网关接口
在领域层定义好外部服务网关接口后,需要在基础设施层提供该网关接口的实现类
完成外部上下文的调用适配
完成外部上下文的调用适配,在BContextGateway的实现类BContextGatewayImpl中,需要完成对B上下文的调用适配,伪代码如下:
/**
* 网关实现类
*/
@Component
public class BContextGatewayImpl implements BContextGateway {
@Resource
private BContextServiceProvider serviceProvider;
/**
* 参数一般是值对象或基本数据类型
* 一般按需传递,不会直接将整个聚合根作为参数
*/
@Override
public ValueObject query(SomeValue someValue) {
// 1.拼装调用B上下文开放主机服务的请求报文,有的还需要做请求签名
QueryRequest req = this.createQueryRequest(someValue);
QueryResponse<BView> res = null;
try {
// 2.调用B上下文查询服务,得到查询结果
res = serviceProvider.queryOne(req);
} catch (Exception e) {
// 3.捕获一些框架或者调用一场,将其转换为本地的业务异常
throw new CustomRuntimeException(e);
}
// 4.如果调用的状态码不是成功,抛出自定义异常,此处假设响应码code为0是成功
if(res.getCode()!=0) {
throw new CustomRuntimeException();
}
// 5.解析出数据对象
BView bView = res.getBView();
// 根据数据对象,创建本地上下文的值对象并返回
ValueObject valueObject = new ValueObject(bView.getProperty1());
return valueObject;
}
/**
* 参数一般是值对象或基本数据类型
* 一般按需传递,不会直接将整个聚合根作为参数
*/
@Override
public void command(SomeValue someValue) {
// 1.拼装调用B上下文开放主机服务的请求报文,有的还需要做请求签名
CommandRequest req = this.createCommandRequest(someValue);
CommandResponse res = null;
try {
// 2.调用B上下文执行命令请求,得到结果
res = this.createCommandRequest(someValue);
}catch(Exception e) {
// 3.捕获一些框架或者调用一场,将其转换为本地的业务异常
throw new CustomRuntimeException(e);
}
// 4.如果调用的状态码不是成功,抛出自定义异常,此处假设响应码code为0是成功
if(res.getCode()!=0) {
throw new CustomRuntimeException();
}
}
}
应用层使用
防腐层实现后,将在应用层进行使用,应用层调用防腐层的伪代码如下:
public class AContextApplicationService {
@Resource
private ADomainRepository domainRepository;
@Resource
private BContextGateway bContextGateway;
/**
* 演示查询外部上下文数据
*/
public void mehtod1 (Command1 command1) {
// 根据实际情况,SomeValue有几种可能的类型
// 1.基本数据类型:从参数中获得
// 2,值对象:从入参中获得的数据之后创建
// 3.值对象:加载聚合根之后,是聚合根的某个字段
SomeValue someValue = command.getSomeValue();
// 从B上下文查询数据,并在防腐层封装为值对象
ValueObject valueObject = bContextGateway.queryMethod(someValue);
// 加载聚合根
String eid = command.getEntityId();
Entity entity = domainRepository.load(new EntityId(eid));
// 聚合根执行业务操作
entity.doMethod1(someValue);
// 保存聚合根
domainRepository.save(entity);
}
/**
* 演示对外部上下文发起命令操作
* 注意:这种情况存在分布式事务问题
*/
public void method2 (Command2 command) {
// 根据实际情况,SomeValue有几种可能的类型
// 1.基本数据类型:从参数中获得
// 2,值对象:从入参中获得的数据之后创建
// 3.值对象:加载聚合根之后,是聚合根的某个字段
SomeValue someValue = command.getSomeValue();
// 加载聚合根
Strng eid = command.getEntityId();
EntityId entityId = new EntityId(eid);
Entity entity = domainRepository.load(entityId);
// 聚合根执行业务操作
entity.doMehtod1(someValue);
// 保存聚合根
domainRepository.save(eneity);
// 从聚合根中获得某些值
someValue = entity.getSomeValue();
// 根据执行结果,更新B上下文,有分布式事务问题,正常应该使用领域事件通知B
bContextGateway.commandMethod(someValue);
}
}
5.总结实现要点
封装技术细节
防腐层应尽可能隐藏与外部系统或服务进行交互的技术细节
当调用外部系统时,通常需要组装请求对象。此组装对象的过程应放在防腐层。并对上层调用者透明。某些外部服务要求调用方对请求进行加密或签名,这种处理也应该放在防腐层中
尽量简单且稳定
防腐层应只关注与外部系统进行交互的技术细节,例如数据转换和接口适配等,并将外部系统的数据转换为本地上下文值对象或基本数据类型。防腐层不应该包含领域层的业务逻辑
防腐层需要遵循单一职责原则,确保每个防腐层只负责一与一个外部系统进行交互。如果需要通过多个防腐层进行数据拼接以生成一个值对象,则应该由工厂完成
/**
* 演示多个防腐层的使用
*/
@Component
public class ValueObjectFactoryImpl implements ValueObjectFactory {
@Resource
public BContextGateway bContexteway;
@Resource
public CContextGateway cContexteway;
/**
* 演示使用多个防腐层的执行结果创建值对象
*/
public ValueObject newInstance (String someValue) {
// 调用B
ValueObject1 valueObject1 = bContexteway.queryOne(someValue);
// 调用C
ValueObject2 valueObject2 = cContexteway.queryOne(someValue);
// 创建新的值对象
return new ValueObject(valueObject1,valueObject2);
}
}
入参和出参为本地上下文值对象或基本数据类型
防腐层方法返回值必须是本地上下文的值对象或者基本数据类型,不得返回外部上下文的模型
/**
* 演示返回本地上下文的领域模型
*/
public class BContextGateway {
@Resoruce
private BRpcServiceProvider bRpc;
public ValueObject query (SomeValue someValue) {
// 封装查询报文
Query query = this.createQueryRequest(someValue);
// 执行查询
Reponse<BView> bResponse = bRpc.query(query);
// 忽略判空,查询失败等逻辑
BView bView = bResponse.getData();
// 重点:封装成本地上下文的值对象进行返回
return new SomeValue(bView.getProperty1());
}
}
将外部异常转换为本地异常
防腐层在处理外部异常时,要捕获外部异常并抛出本地上下文自定义的异常
/**
* 演示框架和外部异常转换为本地异常
*/
public class BContextGateway {
@Resoruce
private BRpcServiceProvider bRpc;
public ValueObject query (SomeValue someValue) {
// 封装查询报文
Query query = this.createQueryRequest(someValue);
// 执行查询
Reponse<BView> bResponse = null;
try{
// 查询结果
bResponse = bRpc.query(query);
}catch(Exception e) {
// 重点:捕获异常,转换成本地异常
throw new QueryBContextException(e);
}
// 忽略其他逻辑
}
}
将外部错误码转换为本地异常
外部上下文返回的错误码,应该转换成本地异常进行抛出,不应该将错误码返回给上层
/**
* 演示将外部错误码转换为本地异常
*/
public class BContextGateway {
@Resoruce
private BRpcServiceProvider bRpc;
public ValueObject query (SomeValue someValue) {
// 封装查询报文
Query query = this.createQueryRequest(someValue);
// 执行查询
Reponse<BView> bResponse = null;
try{
// 查询结果
bResponse = bRpc.query(query);
}catch(Exception e) {
// 捕获异常,转换成本地异常
throw new QueryBContextException(e);
}
// 重点:根据错误码抛出本地异常
if("1".equals(bResponse.getCode())) {
throw new QueryBContextException();
}
// 忽略其他逻辑
}
}
按需返回
只返回本地上下文关注的信息,只返回需要的字段。举个例子,外部上下文可能返回字符串的0和1,代表false和true,但是本地上下文使用boolean类型,因此需要在防腐层转换后进行再返回
/**
* 演示将外部执行结果转为本地上下文期待的类型
*/
public class BContextGateway {
@Resoruce
private BRpcServiceProvider bRpc;
public ValueObject query (SomeValue someValue) {
// 封装查询报文
Query query = this.createQueryRequest(someValue);
// 执行查询
Reponse<BView> bResponse = null;
bResponse = bRpc.query(query);
// 重点:转换类型
return "1".equals(bResponse.getData().getCheckResult());
}
}
不在实体和值对象中调用防腐层
防腐层也是一种基础设施,因此,不应该在实体和值对象中直接调用防腐层,否则会导致实体和值对象的业务方法承担过多职责。试想:如果需要调用多个防腐层,那么本该是充血模型的实体和值对象岂不是又和贫血模型的Service一样了?
一般在应用层的应用服务,领域层的领域服务中调用防腐层,并将防腐层执行的结果交给实体和值对象来完成业务操作
/**
* 本地上下文的应用层
*/
public class AContextApplicationService {
@Resource
private ADomainRepository domainRepository;
@Resource
private BContextGateway bContextGateway;
public void method1(Command command) {
SomeValue someValue = command.getSomeValue();
// 防腐层执行,得到执行结果
ValueObject valueObject = bContextGateway.query(someValue);
// 加载聚合根
String eid = command.getEntityId();
Entity entity = domainRepository.load(new EntityId(eid));
// 聚合根使用防腐层的执行结果完成业务操作
entity.doMehtod1(valueObject);
// 保存聚合根
domainRepository.save(entity);
}
}