除了服务之外,还有什么更好的地方可以放置映射器来将实体转换为 DTO,反之亦然?
在工作中,我们总是把Mapper放到Service层,但我不确定这是最好的地方...
该服务包含一堆将 DTO 对象作为参数或返回 DTO 对象的方法。 当在控制器端调用这些方法时,这是很顺利的。 但有时我们在 ServiceA 中有方法调用 ServiceB 的方法,我想避免在不需要的地方转换对象:
示例:
// A SERVICE IMPL
public Integer save(ADto aDto) {
AEntity aEntity = aMapper.toEntity(aDto);
BEntity bEntity = aEntity.getBEntity();
BDto bDto = bMapper.toDto(bEntity);
bService.save(bDto);
aEntity = aRepository.save(aEntity);
return aEntity.getId();
}
// B SERVICE IMPL
public Integer save(BDto bDto) {
BEntity bEntity = bMapper.toEntity(bDto);
bEntity = bRepository.save(bEntity);
return bEntity.getId();
}
在这种情况下,有 2 种转换是可以避免的:
所以我们最终得到了“重复”的方法:
Integer save(Entity entity)
Integer saveFromDto(Dto dto)
Integer save(List<Entity> entities)
Integer saveFromDtos(List<Dto> dtos)
Entity findById(Integer id)
Dto findByIdToDto(Integer id)
List<Entity> findAll()
List<Dto> findAllToDto()
我开始思考如何重构它,我想到了以下可能性:
将Mapper移至Controller层
控制器是负责发送数据作为响应的层,因此该层必须知道必须发送什么,在我看来,知道要发送什么不是服务的责任。
PRO:不需要为每个服务创建新的适配器类
缺点:将转换逻辑添加到控制器中
创建一个服务适配器并将其用于控制器层。
基本上,适配器将负责:
1 - 在调用需要实体作为参数的服务方法之前,调用映射器方法将 Dto 转换为实体。
2 - 在将结果返回到Controller层之前调用mapper方法将实体转换为dto。
PRO:不向控制器添加转换逻辑。
缺点:每个服务都有一个新的适配器类。
此重构将使服务层免于额外的工作,因为它只了解实体类,而不了解 DTO。
对于像这样的设计问题,几乎没有任何不合格的通用答案,但一般来说,数据传输对象的存在是为了模拟应用程序如何与世界其他部分交互:JSON、XML、数据库行等。
在边界上,应用程序不是面向对象的,并且从任何面向对象的意义上来说,DTO 都不是真正合适的对象。正如 Martin Fowler 在 PEAA 中写道:
“数据传输对象是我们母亲告诉我们永远不要写的对象之一。”
DTO 应该特定于外部 API,因此对于同一个域模型,您甚至可能有一个 DTO 用于将实体表示为 JSON,另一个 DTO 用于将其表示为数据库行。
在典型的端口和适配器架构中,DTO 和适配器是在应用程序的边界定义的。
如果您遵循依赖倒置原则,您希望核心领域模型不受“边缘”问题的影响,而“边缘”(或边界)可能依赖于并了解核心领域模型。
根据这一原则,所有涉及 DTO 的映射都必须与 DTO 本身存在于同一“层”。
人们经常发现清洁架构以一种有意义的方式解释了这一点。依赖关系可能只指向内部。
如果我正确理解 OP 的术语,请将映射器放在“控制器层”中,因为这也是 DTO 应该所在的位置,而 DTO 完全是“控制器层”关注的问题。
您不一定要将转换逻辑放入实际的控制器类中,但转换属于该层(例如库或模块)。