Entendendo e aplicando o padrão DTO

Entendendo e aplicando o padrão DTO

Olá pessoal! Bem-vindos ao artigo desta semana.
Você pode conferir os artigos anteriores aqui, e o código está disponível no meu GitHub, basta acessar aqui.


Um DTO (Data Transfer Object) é um padrão de projeto utilizado para transferir dados entre diferentes camadas ou componentes de uma aplicação. Nas APIs, ele é amplamente empregado para receber e enviar informações de forma organizada, segura e eficiente, principalmente em operações de entrada (requests) e saída (responses).

Por que usar DTOs?

Imagine uma API para gerenciar usuários. Ao consultar as informações de um usuário, não faz sentido retornar todos os dados armazenados no banco, como senha ou configurações internas. Nesse cenário, um DTO é útil para filtrar e organizar apenas as informações relevantes para a resposta, como nome, e-mail e data de cadastro.

Características de um DTO

  1. Estrutura simples: Um DTO é uma classe que geralmente contém somente atributos e métodos de acesso (getters e setters). Ela não inclui regras de negócio nem lógica complexa.

  2. Propósito específico: O objetivo do DTO é facilitar a comunicação entre as camadas do sistema, como a camada de serviço e a de apresentação, transferindo apenas os dados necessários.

Benefícios do uso de DTOs

  1. Segurança: Com o DTO é possível controlar quais informações serão expostas pela API, evitando a divulgação acidental de dados sensíveis, como senhas ou tokens de autenticação.

  2. Desacoplamento: O uso de DTOs ajuda a separar as responsabilidades entre a camada de apresentação (que lida com a entrada e saída de dados) e a camada de domínio (onde ficam as regras de negócio). Isso torna o sistema mais modular e facilita a manutenção.

  3. Otimização de desempenho: Transferir apenas os dados necessários reduz o tamanho das respostas da API, diminuindo o tráfego de rede e aumentando a performance, especialmente em sistemas que lidam com grande volume de requisições.

  4. Flexibilidade: Permite que o modelo de dados seja alterado sem quebrar ou impactar os clientes que consomem a API, pois o contrato de comunicação permanece estável.

Vamos voltar para o código e aplicar na prática?

Hoje, a rota GET /series retorna todas as informações salvas no banco. No entanto, em muitos casos, não precisamos de todos esses detalhes, o que acaba gerando um tráfego de dados maior do que o necessário e pode impactar a performance da aplicação.

Para otimizar isso, vamos alterar a rota para devolver apenas o nome, o ID, o número de temporadas e a situação da série. Se for preciso consultar todos os dados de uma série específica, basta chamar GET /series/{id}, que continuará retornando todas as informações completas.

Pode parecer algo simples em uma API pequena, mas, em sistemas mais robustos e acessados constantemente, essa redução de dados pode fazer a diferença.

Como fica nossa classe SeriesSummaryDTO

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SeriesSummaryDTO {
    private String id;
    private String name;
    private int numberOfSeasons;
    private boolean completed;
}

Outro ponto de alteração é na classe SeriesController, antes o método getAllSeries retornava diretamente a classe Series, mas agora precisa retornar a nossa DTO recém-criada. Com isso, a resposta da API passa a conter somente os campos desejados.

    @GetMapping
    public ResponseEntity<List<SeriesSummaryDTO>> getAllSeries() {
        List<Series> seriesList = seriesService.listAllSeries();
        List<SeriesSummaryDTO> seriesSummaryDTOs = seriesList.stream()
                .map(seriesMapper::toSeriesSummaryDTO)
                .toList();

        return ResponseEntity.ok(seriesSummaryDTOs);
    }

Por fim, um ponto fundamental do nosso projeto é a criação de uma classe Mapper, que tem a responsabilidade de converter a entidade que vem do banco de dados no nosso DTO (e vice-versa). Para ilustrar melhor o processo de mapeamento, optei por criar essa classe manual mas, no dia a dia, é bem comum utilizar bibliotecas que automatizam esse trabalho, como o MapStruct ou ModelMapper (posso falar mais sobre elas depois!).

Abaixo está a classe SeriesMapper, que realiza exatamente esse trabalho de conversão.

@Component
public class SeriesMapper {

    public Series toEntity(SeriesDTO dto) {
        if (dto == null) return null;

        return Series.builder()
                .id(dto.getId())
                .name(dto.getName())
                .genre(dto.getGenre())
                .releaseYear(dto.getReleaseYear())
                .seasons(dto.getSeasons() != null ?
                        dto.getSeasons().stream()
                                .map(this::seasonToEntity)
                                .toList() :
                        null)
                .completed(dto.isCompleted())
                .build();
    }

    public SeriesDTO toDTO(Series entity) {
        if (entity == null) return null;

        return SeriesDTO.builder()
                .id(entity.getId())
                .name(entity.getName())
                .genre(entity.getGenre())
                .releaseYear(entity.getReleaseYear())
                .seasons(entity.getSeasons() != null ?
                        entity.getSeasons().stream()
                                .map(this::seasonToDTO)
                                .toList() :
                        null)
                .completed(entity.isCompleted())
                .build();
    }

    private Season seasonToEntity(SeasonDTO dto) {
        if (dto == null) return null;

        return Season.builder()
                .seasonNumber(dto.getSeasonNumber())
                .episodes(dto.getEpisodes())
                .build();
    }

    private SeasonDTO seasonToDTO(Season entity) {
        if (entity == null) return null;

        return SeasonDTO.builder()
                .seasonNumber(entity.getSeasonNumber())
                .episodes(entity.getEpisodes())
                .build();
    }

    public SeriesSummaryDTO toSeriesSummaryDTO(Series entity) {
        if (entity == null) return null;

        return SeriesSummaryDTO.builder()
                .id(entity.getId())
                .name(entity.getName())
                .numberOfSeasons(entity.getSeasons().size())
                .completed(entity.isCompleted())
                .build();
    }
}

Ficou com alguma dúvida? Deixe sua pergunta nos comentários que vou tentar ajudar da melhor forma possível!
Se quiser consultar a alteração das demais operações, visite o projeto no Github, por lá você também pode baixar a Collection do Postman mais atualizada.

Até a próxima 🚀