在本教程中,我们将讨论DTO 模式、它是什么以及如何以及何时使用它。到最后,我们将知道如何正确使用它。
DTO 或数据传输对象是在进程之间传输数据以减少方法调用次数的对象。马丁·福勒(Martin Fowler)在他的《EAA》一书中首次引入了这种模式。
Fowler 解释说,该模式的主要目的是通过在单个调用中批处理多个参数来减少到服务器的往返。这减少了此类远程操作中的网络开销。
另一个好处是序列化逻辑的封装(将对象结构和数据转换为可以存储和传输的特定格式的机制)。它提供了序列化细微差别的单点更改。它还将域模型与表示层分离,允许两者独立更改。
DTO 通常创建为POJO。它们是不包含业务逻辑的平面数据结构。它们仅包含存储、访问器以及与序列化或分析相关的最终方法。
数据通常通过表示或立面层中的映射器组件从域模型映射到 DTO。
下图说明了组件之间的交互:
DTO 在具有远程呼叫的系统中派上用场,因为它们有助于减少远程呼叫的数量。
当域模型由许多不同的对象组成并且表示模型需要同时使用其所有数据时,DTO 也会有所帮助,或者它们甚至可以减少客户端和服务器之间的往返。
使用 DTO,我们可以从我们的领域模型中构建不同的视图,允许我们创建同一领域的其他表示形式,但根据客户的需求对其进行优化,而不会影响我们的领域设计。这种灵活性是解决复杂问题的有力工具。
为了演示该模式的实现,我们将使用一个简单的应用程序,其中包含两个主要域模型,在本例中为“用户”和“角色”。为了关注模式,让我们看两个功能示例 — 用户检索和创建新用户。
以下是两种模型的定义:
- 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());
- }
- }
最后,我们有传输数据的映射器组件,确保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,数据映射器,命令查询分离等。
在本文中,我们看到了DTO模式的定义,它存在的原因以及如何实现它。
我们还看到了一些与实施相关的常见错误以及避免这些错误的方法。
像往常一样,该示例的源代码可在GitHub 上找到。