DTO 或数据传输对象是在进程之间传输数据以减少方法调用次数的对象。该模式最初是由 Martin Fowler 在他的著作EAA中介绍的。
Fowler 解释说,该模式的主要目的是通过在一次调用中批量处理多个参数来减少到服务器的往返次数。这减少了此类远程操作中的网络开销。
另一个好处是封装了序列化的逻辑(将对象结构和数据转换为可以存储和传输的特定格式的机制)。它把序列化细节实现放在一个集中地方,便于修改。它还将领域模型与表示层分离,允许两者独立更改。
DTO 通常被创建为POJO。它们是不包含业务逻辑的平面数据结构。它们只包含与序列化或反序列化相关的用于存储、访问器的方法。
数据从领域模型映射到 DTO,通常通过表示层或外观层中的映射器组件(Mapper component。
下图说明了组件之间的交互:
DTO 在具有远程调用的系统中派上用场,因为它们有助于减少远程调用的数量。
当领域模型由许多不同的对象组成并且展现层模型一次需要它们的所有数据时,DTO 也很有帮助,或者它们甚至可以减少客户端和服务器之间的往返调用。
使用 DTO,我们可以从我们的领域模型构建不同的视图,允许我们创建同一领域的其他表示,但在不影响我们的领域设计的情况下根据客户的需求对其进行优化。这种灵活性是解决复杂问题的有力工具。也即是不改变领域模型的情况下,设计不同的DTO来满足展现层的需求。
下面用一个具有两个主要领域模型的简单应用程序演示该模式的实现,这两个领域模型分别为User和Role。为了专注于该模式,让我们看一下与用户管理功能相关的两个场景——用户检索和新用户的创建。
定义两个领域模型:User和Role。
public class User {
private String id;
private String name;
private String password;
private List roles;
public User(String name, String password, List roles) {
this.name = Objects.requireNonNull(name);
this.password = this.encrypt(password);
this.roles = Objects.requireNonNull(roles);
}
// Getters and Setters
String encrypt(String password) {
// encryption logic
}
}
public class Role {
private String id;
private String name;
// Constructors, getters and setters
}
现在让我们看一下 DTO,以便我们可以将它们与领域模型进行比较。
此时,重要的是要注意 DTO 表示从 API 客户端发送或发送给 API 客户端的模型。
因此,小的区别要么是将发送到服务器的请求包装在一起,要么优化客户端的响应:
public class UserDTO {
private String name;
private List roles;
// standard getters and setters
}
上面的 DTO 仅向客户端提供相关信息,隐藏密码,例如出于安全原因。
下一个 DTO 将创建用户所需的所有数据分组并在单个请求中将其发送到服务器,从而优化与 API 的交互:
public class UserCreationDTO {
private String name;
private String password;
private List roles;
// standard getters and setters
}
接下来,连接两个类的层使用映射器组件将数据从一侧传递到另一侧,反之亦然。
这通常发生在表示层:
@RestController
@RequestMapping("/users")
class UserController {
private UserService userService;
private RoleService roleService;
private Mapper mapper;
// Constructor
@GetMapping
@ResponseBody
public List getUsers() {
return userService.getAll()
.stream()
.map(mapper::toDto)
.collect(toList());
}
@PostMapping
@ResponseBody
public UserIdDTO create(@RequestBody UserCreationDTO userDTO) {
User user = mapper.toUser(userDTO);
userDTO.getRoles()
.stream()
.map(role -> roleService.getOrCreate(role))
.forEach(user::addRole);
userService.save(user);
return new UserIdDTO(user.getId());
}
}
最后,我们有传输数据的Mapper组件,确保 DTO 和领域模型不需要相互了解:
@Component
class Mapper {
public UserDTO toDto(User user) {
String name = user.getName();
List roles = user
.getRoles()
.stream()
.map(Role::getName)
.collect(toList());
return new UserDTO(name, roles);
}
public User toUser(UserCreationDTO userDTO) {
return new User(userDTO.getName(), userDTO.getPassword(), new ArrayList<>());
}
}
尽管 DTO 模式是一种简单的设计模式,但我们在实现该技术的应用程序中可能会犯一些错误。
第一个错误是为每个场合创建不同的 DTO。这将增加我们需要维护的类和映射器的数量。尽量保持简洁,并评估添加一个或重用现有一个的权衡。
我们还希望避免在许多场景中尝试使用单个类。这种做法可能会导致很多属性经常不使用的大合同。
另一个常见的错误是向这些类添加业务逻辑,这是不应该发生的。该模式的目的是优化数据传输和合约结构。因此,所有业务逻辑都应该存在于领域层中。
最后,我们有所谓的LocalDTO,这些 DTO 跨领域传递数据。问题是所有这些映射都有其维护成本。
支持这种方法的最常见论点之一是领域模型的封装。但这里的问题是让我们的领域模型与持久性模型耦合。通过将它们解耦,暴露领域模型的风险几乎消失了。
其他模式也有类似的结果,但它们通常用于更复杂的场景,例如CQRS 、Data Mappers、CommandQuerySeparation等。