The DTO Pattern (Data Transfer Object) – DTO模式(数据传输对象)

最后修改: 2021年 8月 29日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll discuss the DTO pattern, what it is, and how and when to use it. By the end, we’ll know how to use it properly.

在本教程中,我们将讨论DTO模式,它是什么,以及如何和何时使用它。到最后,我们将知道如何正确使用它。

2. The Pattern

2.模式

DTOs or Data Transfer Objects are objects that carry data between processes in order to reduce the number of methods calls. The pattern was first introduced by Martin Fowler in his book EAA.

DTOs或数据传输对象是在进程之间携带数据的对象,以减少方法调用的数量。该模式是由Martin Fowler在他的书EAA中首次提出。

Fowler explained that the pattern’s main purpose is to reduce roundtrips to the server by batching up multiple parameters in a single call. This reduces the network overhead in such remote operations.

Fowler解释说,该模式的主要目的是通过在一次调用中批处理多个参数来减少往返服务器的次数。这就减少了这种远程操作的网络开销。

Another benefit is the encapsulation of the serialization’s logic (the mechanism that translates the object structure and data to a specific format that can be stored and transferred). It provides a single point of change in the serialization nuances. It also decouples the domain models from the presentation layer, allowing both to change independently.

另一个好处是对序列化的逻辑(将对象结构和数据转换为可以存储和传输的特定格式的机制)进行了封装。它在序列化的细微差别中提供了一个单一的变化点。它还将领域模型与表现层解耦,允许两者独立变化。

3. How to Use It?

3.如何使用它?

DTOs normally are created as POJOs. They are flat data structures that contain no business logic. They only contain storage, accessors and eventually methods related to serialization or parsing.

DTO通常被创建为POJO。它们是扁平的数据结构,不包含业务逻辑。它们只包含存储、访问器和最终与序列化或解析有关的方法。

The data is mapped from the domain models to the DTOs, normally through a mapper component in the presentation or facade layer.

数据从域模型映射到DTO,通常是通过表现层或门面层中的映射器组件来实现。

The image below illustrates the interaction between the components:

下面的图片说明了这些组件之间的互动。

4. When to Use It?

4.何时使用?

DTOs come in handy in systems with remote calls, as they help to reduce the number of them.

在有远程调用的系统中,DTOs很方便,因为它们有助于减少远程调用的数量。

DTOs also help when the domain model is composed of many different objects and the presentation model needs all their data at once, or they can even reduce roundtrip between client and server.

当领域模型由许多不同的对象组成,而表现模型需要它们的所有数据时,DTOs也会有所帮助,或者它们甚至可以减少客户端和服务器之间的往返。

With DTOs, we can build different views from our domain models, allowing us to create other representations of the same domain but optimizing them to the clients’ needs without affecting our domain design. Such flexibility is a powerful tool to solve complex problems.

通过DTO,我们可以从我们的领域模型中构建不同的视图,允许我们创建同一领域的其他表现形式,但根据客户的需求进行优化,而不影响我们的领域设计。这种灵活性是解决复杂问题的有力工具。

5. Use Case

5.使用案例

To demonstrate the implementation of the pattern, we’ll use a simple application with two main domain models, in this case, User and Role. To focus on the pattern, let’s look at two examples of functionality — user retrieval and the creation of new users.

为了演示该模式的实现,我们将使用一个具有两个主要领域模型的简单应用程序,在这种情况下,UserRole。为了专注于该模式,让我们看看两个功能的例子–用户检索和创建新用户。

5.1. DTO vs Domain

5.1.DTO与域名

Below is the definition of both models:

以下是这两种模式的定义。

public class User {

    private String id;
    private String name;
    private String password;
    private List<Role> roles;

    public User(String name, String password, List<Role> 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
}

Now let’s look at the DTOs so that we can compare them with the Domain models.

现在让我们看看DTO,这样我们就可以将它们与领域模型进行比较。

At this moment, it’s important to notice that the DTO represents the model sent from or to the API client.

此时此刻,需要注意的是,DTO代表了从API客户端发送或发送到API客户端的模型。

Therefore, the small differences are either to pack together the request sent to the server or optimize the response of the client:

因此,这些微小的差异要么是为了将发送到服务器的请求打包在一起,要么是为了优化客户端的响应。

public class UserDTO {
    private String name;
    private List<String> roles;
    
    // standard getters and setters
}

The DTO above provides only the relevant information to the client, hiding the password, for example, for security reasons.

上面的DTO只向客户提供相关信息,例如出于安全考虑,隐藏了密码。

The next DTO groups all the data necessary to create a user and sends it to the server in a single request, which optimizes the interactions with the API:

下一个DTO将创建一个用户所需的所有数据分组,并在一个单一的请求中将其发送到服务器,这优化了与API的交互。

public class UserCreationDTO {

    private String name;
    private String password;
    private List<String> roles;

    // standard getters and setters
}

5.2. Connecting Both Sides

5.2.连接两边

Next, the layer that ties both classes uses a mapper component to pass the data from one side to the other and vice versa.

接下来,连接两个类的层使用一个映射器组件将数据从一边传到另一边,反之亦然。

This normally happens in the presentation layer:

这通常发生在演示层。

@RestController
@RequestMapping("/users")
class UserController {

    private UserService userService;
    private RoleService roleService;
    private Mapper mapper;

    // Constructor

    @GetMapping
    @ResponseBody
    public List<UserDTO> 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());
    }

}

Last, we have the Mapper component that transfers the data, making sure that both DTO and domain model don’t need to know about each other:

最后,我们有传输数据的Mapper组件,确保DTO和领域模型都不需要知道对方的情况

@Component
class Mapper {
    public UserDTO toDto(User user) {
        String name = user.getName();
        List<String> 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<>());
    }
}

6. Common Mistakes

6.常见错误

Although the DTO pattern is a simple design pattern, we can make a few mistakes in applications implementing this technique.

尽管DTO模式是一个简单的设计模式,但我们在实现这一技术的应用中会犯一些错误。

The first mistake is to create different DTOs for every occasion. That will increase the number of classes and mappers we need to maintain. Try to keep them concise and evaluate the trade-offs of adding one or reusing an existing one.

第一个错误是为每个场合创建不同的DTO。这将增加我们需要维护的类和映射器的数量。尽量保持它们的简洁,并评估增加一个或重用一个现有的DTO的利弊。

We also want to avoid trying to use a single class for many scenarios. This practice may lead to big contracts where many attributes are frequently not used.

我们也想避免在许多情况下使用一个单一的类。这种做法可能会导致许多属性经常不被使用的大合同。

Another common mistake is to add business logic to those classes, which should not happen. The purpose of the pattern is to optimize the data transfer and the structure of the contracts. Therefore, all business logic should live in the domain layer.

另一个常见的错误是向这些类添加业务逻辑,这是不应该发生的。该模式的目的是为了优化数据传输和合同的结构。因此,所有的业务逻辑都应该生活在领域层中。

Last, we have the so-called LocalDTOs, where DTOs pass data across domains. The problem once again is the cost of maintenance of all the mapping.

最后,我们有所谓的LocalDTOs,其中DTOs跨域传递数据。问题还是在于所有映射的维护成本。

One of the most common arguments in favor of this approach is the encapsulation of the domain model. But the problem here is to have our domain model coupled with the persistence model. By decoupling them, the risk to expose the domain model almost disappears.

支持这种方法的最常见的论据之一是对领域模型的封装。但这里的问题是,我们的领域模型与持久化模型相耦合。通过将它们解耦,暴露领域模型的风险几乎消失了。

Other patterns reach a similar outcome, but they usually are used in more complex scenarios, such as CQRS, Data Mappers, CommandQuerySeparation, etc.

其他模式也会达到类似的结果,但它们通常用于更复杂的场景,如CQRSData MappersCommandQuerySeparation等。

7. Conclusion

7.结语

In this article, we saw the definition of the DTO Pattern, why it exists and how to implement it.

在这篇文章中,我们看到了DTO模式的定义,它为什么存在以及如何实现它。

We also saw some of the common mistakes related to its implementation and ways to avoid them.

我们还看到了与它的实施有关的一些常见错误和避免它们的方法。

As usual, the source code of the example is available over on GitHub.

像往常一样,这个例子的源代码可以在GitHub上找到over