BT

Início Artigos DTO: muito hipster ou depreciado?

DTO: muito hipster ou depreciado?

Favoritos

O Data Transfer Object, conhecido como DTO, é alvo de grandes discussões, quando falamos sobre o desenvolvimento de aplicações Java. O DTO nasceu no mundo Java no EJB2 com dois propósitos: primeiro, contornar o problema de serialização do EJB e; segundo, definir implicitamente uma fase de montagem, na qual todos os dados que serão usados para apresentação passam por uma ordenação antes de irem efetivamente para a camada de apresentação. Porém, atualmente o EJB não é mais utilizado em larga escala, portanto, os DTOs também podem ser descartados? O objetivo desse artigo é falar um pouco sobre a utilidade atual ou posterior do DTO nas aplicações.

Existe uma grande discussão sobre os DTOs, afinal, num ambiente onde existem vários tópicos novos, como cloud e microservices, essa tal camada faz sentido? Quando falamos em uma boa arquitetura de software a resposta é praticamente unânime: depende do quão acoplado desejamos que a entidade esteja com a camada de visualização.

Pensando numa arquitetura básica em camadas e dividindo-a em três partes interconectadas, encontramos o famoso MVC.

Um ponto importante é que a arquitetura da camada MVC não é exclusiva para aplicações Web, como: Spring MVC ou JSF. Por exemplo, em uma aplicação RESTful as informações expostas como JSON funcionam como view, mesmo não tendo uma interface amigável.

Após explicarmos de maneira resumida o MVC, falaremos sobre as vantagens e desvantagens do uso do DTO. Pensando em aplicações em camadas, o DTO tem como objetivo, separar o model da view, sendo assim, as desvantagens do DTO são:

  • Aumento a complexidade;
  • Existe grande possibilidade do código ficar duplicado.

Adicionar uma nova camada causando impacto no layout, deixando-o mais lento, ou seja, cria problemas relacionados a desempenho.

Assim, sistemas simples e dos quais não precisam de um model rico como premissa, não utilizar o DTO traz grandes benefícios para a aplicação. Um ponto interessante é que muitos frameworks de serialização acabam obrigando que os atributos tenham métodos de acesso (getter e setter) sempre presentes e públicos, assim, em algum momento isso significará um impacto no encapsulamento e na segurança da aplicação.

A outra opção está em adicionar a camada DTO garantindo algumas vantagens, como o desacoplamento da view e do model como mencionado anteriormente:

  • Deixa explícito quais campos irão para a camada da view. Sim, existem várias anotações em diversos frameworks que indicam quais campos não irão para a visualização. Porém, caso esqueçamos de fazer as anotações, podemos exportar um campo crítico de maneira acidental como a senha do usuário;
  • Facilita o desenho em relação a orientação de objeto. Um dos pontos que o clean code deixa claro sobre orientação a objetos é que, o POO esconde os dados para expor o comportamento e, o encapsulamento, ajuda com isso;
  • Facilita o atualização do banco de dados. Muitas vezes é importante refatorar ou migrar o banco de dados sem que essa alteração impacte o cliente. Essa separação facilita otimizações, modificações no banco de dados sem que isso impacte a visualização;
  • Versionamento e retrocompatibilidade são pontos importantes, principalmente, quando se tem uma API de uso público e com vários clientes, assim é possível ter um DTO para cada versão e evoluir o modelo de negócio sem se preocupar;
  • Outro benefício é a facilidade de trabalhar com o model rico e na criação de uma API que é a prova de balas. Por exemplo, dentro do model podemos usar uma API do tipo money, porém, dentro da camada da view, exporto como sendo um simples objeto com apenas o valor monetário, ou seja, o bom e velho String no Java;

CQRS: a Segregação de Responsabilidade de Consulta de Comando (Command Query Responsibility Segregation) separa a responsabilidade da escrita e da leitura de dados. Como fazer isso sem os DTOs?

No geral, adicionar uma camada significa desacoplar e facilitar a manutenção em detrimento de ter mais classes e mais complexidade. Uma vez que também temos que pensar na operação de conversão entre essas camadas. Essa é a razão, por exemplo, da existência do MVC, assim, é muito importante entender que tudo se baseia em impacto e tradeoffs em uma determinada aplicação ou situação. A não existência dessas camadas pode ser péssimo, acarretando um padrão Highlander, there can be only one, no qual existe uma classe que detém todas as responsabilidades. Do mesmo jeito que o excesso de camadas torna o padrão similar a uma cebola, onde o desenvolvedor se arrepende ao passar por cada camada.

Uma crítica mais frequente ao DTOs se encontra no trabalho para realizar a conversão. A boa notícia é que existem diversos frameworks especializados neste trabalho, ou seja, não é necessário realizar a mudança manualmente. Nesse artigo escolheremos o framework modelmapper.

O primeiro passo é definir as dependências do projeto, como uma ferramenta de build tudo fica fácil, basta definir a dependência como gostaria, por exemplo, no Maven:

<dependency>

<groupId>org.modelmapper</groupId>

<artifactId>modelmapper</artifactId>

<version>2.3.6</version>

</dependency>

Para ilustrar o conceito do DTO, criaremos uma aplicação utilizando JAX-RS conectado ao MongoDB, tudo isso, graças ao Jakarta EE, utilizando o Payara como servidor. Basicamente, iremos gerenciar um User com nickname, salary, birthday e lista de idiomas que o usuário fala (languages). Como trabalharemos com MongoDB no Jakarta EE, utilizaremos o Jakarta NoSQL.

Para mapear a entidade para persistir no banco de dados, as anotações que o Jakarta NoSQL utiliza é bastante semelhante ao JPA, com exceção do nome do pacote. Assim, temos as anotações `Entity`, `Id`, `Column` para identificar que a classe é uma entidade, o campo é uma chave e os campos que serão persistidos respectivamente.

import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Convert;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;
import my.company.infrastructure.MonetaryAmountAttributeConverter;

import javax.money.MonetaryAmount;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Entity
public class User {

    @Id
    private String nickname;

    @Column
    @Convert(MonetaryAmountAttributeConverter.class)
    private MonetaryAmount salary;

    @Column
    private List<String> languages;

    @Column
    private LocalDate birthday;

    @Column
    private Map<String, String> settings;

   // somente os métodos getter
}

Uma coisa importante é que o MongoDB não tem suporte para persistir o tipo `MonetaryAmount`, assim, criamos também uma classe que realiza a conversão de/para String de modo que criamos o `MonetaryAmountAttributeConverter` para realizar essa conversão de/para o banco MongoDB.

import jakarta.nosql.document.Document;
import jakarta.nosql.mapping.AttributeConverter;
import org.javamoney.moneta.Money;

import javax.money.CurrencyUnit;
import javax.money.Monetary;
import javax.money.MonetaryAmount;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MonetaryAmountAttributeConverter implements AttributeConverter<MonetaryAmount, String> {

    @Override
    public String convertToDatabaseColumn(MonetaryAmount attribute) {
        if (attribute == null) {
            return null;
        }
        return attribute.toString();
    }

    @Override
    public MonetaryAmount convertToEntityAttribute(String dbData) {
        if (dbData == null) {
            return null;
        }
        return Money.parse(dbData);
    }
}

De uma maneira geral, não faz sentido fazer com que as entidades tenham getter e setter para todos os atributos, afinal, isso seria o mesmo que deixar o atributo público de maneira direta. Como o foco do artigo não é sobre DDD ou sobre o model rico, omitiremos os detalhes dessa entidade. Para o DTO, teremos todos os campos que a entidade possui, porém, para o view o `MonetaryAmount` será uma `String` e a birthday seguirá a mesma linha.

import java.util.List;
import java.util.Map;

public class UserDTO {

    private String nickname;

    private String salary;

    private List<String> languages;

    private String birthday;

    private Map<String, String> settings;

    //getter e setter
}

O grande benefício do mapper é que não precisamos nos preocupar em fazer isso manualmente. O único ponto a salientar é que os tipos especiais, por exemplo, o `MonetaryAmount` do money-api precisarão criar um converter para virar `String` e vice-versa.

import org.modelmapper.AbstractConverter;

import javax.money.MonetaryAmount;

public class MonetaryAmountStringConverter extends AbstractConverter<MonetaryAmount, String> {

    @Override
    protected String convert(MonetaryAmount source) {
        if (source == null) {
            return null;
        }
        return source.toString();
    }
}


import org.javamoney.moneta.Money;
import org.modelmapper.AbstractConverter;

import javax.money.MonetaryAmount;

public class StringMonetaryAmountConverter extends AbstractConverter<String, MonetaryAmount> {

    @Override
    protected MonetaryAmount convert(String source) {
        if (source == null) {
            return null;
        }
        return Money.parse(source);
    }
}

Os converters estão prontos, o próximo passo é instanciar a classe que realiza a conversão do `ModelMapper`. A partir de agora, toda a aplicação poderá utilizar o mesmo mapper, sendo necessário utilizar apenas a anotação `Inject` como veremos a frente.

import org.modelmapper.ModelMapper;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import java.util.function.Supplier;

import static org.modelmapper.config.Configuration.AccessLevel.PRIVATE;

@ApplicationScoped
public class MapperProducer implements Supplier<ModelMapper> {
    private ModelMapper mapper;

    @PostConstruct
    public void init() {
        this.mapper = new ModelMapper();
        this.mapper.getConfiguration()
                .setFieldMatchingEnabled(true)
                .setFieldAccessLevel(PRIVATE);
        this.mapper.addConverter(new StringMonetaryAmountConverter());
        this.mapper.addConverter(new MonetaryAmountStringConverter());
        this.mapper.addConverter(new StringLocalDateConverter());
        this.mapper.addConverter(new LocalDateStringConverter());
        this.mapper.addConverter(new UserDTOConverter());
    }

    @Override
    @Produces
    public ModelMapper get() {
        return mapper;
    }
}

Uma das grandes vantagens do uso do Jakarta NoSQL está na facilidade entre integrar o banco de dados. Por exemplo, nesse artigo utilizaremos o conceito de repositório, no qual criaremos uma interface e o Jakarta NoSQL irá se encarregar da implementação.

import jakarta.nosql.mapping.Repository;

import javax.enterprise.context.ApplicationScoped;
import java.util.stream.Stream;

@ApplicationScoped
public interface UserRepository extends Repository<User, String> {
    Stream<User> findAll();
}

Por fim, faremos o recurso com o JAX-RS. Um ponto importantíssimo é a exposição de dados, que será feita a partir do DTO, ou seja, é possível realizar toda a modificação dentro da entidade sem que o cliente saiba, graças ao DTO. Como foi dito anteriormente, o mapper foi injetado e o método `map` facilita bastante essa integração entre o DTO e a entidade, sem necessidade de muito código adicional.

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Path("users")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {

    @Inject
    private UserRepository repository;

    @Inject
    private ModelMapper mapper;

    @GET
    public List<UserDTO> getAll() {
        Stream<User> users = repository.findAll();
        return users.map(u -> mapper.map(u, UserDTO.class))
                .collect(Collectors.toList());
    }

    @POST
    public void insert(UserDTO dto) {
        User map = mapper.map(dto, User.class);
        repository.save(map);
    }

    @POST
    @Path("id")
    public void update(@PathParam("id") String id, UserDTO dto) {
        User user = repository.findById(id).orElseThrow(() ->
                new WebApplicationException(Response.Status.NOT_FOUND));
        User map = mapper.map(dto, User.class);
        user.update(map);
        repository.save(map);
    }

    @DELETE
    @Path("id")
    public void delete(@PathParam("id") String id) {
       repository.deleteById(id);
    }
}

Movendo a aplicação para a nuvem

Gerenciar os bancos de dados, o código e as integrações é sempre difícil, mesmo na nuvem. De fato, o servidor ainda está lá e alguém precisa analisar, executar instalações e os backups, além de manter a integridade como um todo. Existem boas práticas que nos ajudam com isso, como os doze fatores necessitam de uma separação estrita da configuração do código.

Felizmente, o Platform.sh fornece um PaaS que gerencia os serviços, como bancos de dados e filas de mensagens, com suporte a várias linguagens, incluindo o Java. Tudo se baseia no conceito de Infraestrutura como Código (IaC), gerenciando e provisionando os serviços por meio de arquivos YAML.

Nas postagens anteriores, mencionamos como podemos fazer isso no Platform.sh, com três arquivos:

Um para definir os serviços usados ​​pelas aplicações, services.yaml.

mongodb:
  type: mongodb:3.6
  disk: 1024

Um para definir as rotas, routes.yaml.

"https://{default}/":
  type: upstream
  upstream: "app:http"

"https://www.{default}/":
  type: redirect
  to: "https://{default}/"

Um arquivo para definir como criar e executar a aplicação, que será utilizado para criar um container automaticamente.

name: app

type: "java:11"
disk: 1024

hooks:
    build:  mvn clean package payara-micro:bundle

relationships:
    mongodb: 'mongodb:mongodb'

web:
    commands:
        start: |
          export MONGO_PORT=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].port"`
          export MONGO_HOST=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].host"`
          export MONGO_ADDRESS="${MONGO_HOST}:${MONGO_PORT}"
          export MONGO_PASSWORD=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].password"`
          export MONGO_USER=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].username"`
          export MONGO_DATABASE=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].path"`
          java -jar -Xmx1024m -Ddocument.settings.jakarta.nosql.host=$MONGO_ADDRESS \
          -Ddocument.database=$MONGO_DATABASE -Ddocument.settings.jakarta.nosql.user=$MONGO_USER \
          -Ddocument.settings.jakarta.nosql.password=$MONGO_PASSWORD \
          -Ddocument.settings.mongodb.authentication.source=$MONGO_DATABASE \
          target/microprofile-microbundle.jar --port $PORT

Neste artigo, falamos sobre a integração de uma aplicação com o DTO, além das ferramentas para entregar e mapear o DTO com a entidade de uma maneira bastante simples. Houve uma conversa sobre as vantagens e as desvantagens dessa camada. Indiferente do conhecimento técnico e do framework, o bom senso ainda é a melhor ferramenta, tanto para o desenvolvedor, quanto para o arquiteto de software.

Sobre o autor

Otávio Santana é engenheiro de software, com grande experiência em desenvolvimento open source, com diversas contribuições ao JBoss Weld, Hibernate, Apache Commons e outros projetos. Focado em desenvolvimento poliglota e aplicações de alto desempenho, Otávio trabalhou em grandes projetos nas áreas de finanças, governo, mídias sociais e e-commerce. Membro do comitê executivo do JCP e de vários Expert Groups de JSRs, é também Java Champion e recebeu os prêmios JCP Outstanding Award e Duke's Choice Award.

Avalie esse artigo

Relevância
Estilo/Redação

Olá visitante

Você precisa cadastrar-se no InfoQ Brasil ou para enviar comentários. Há muitas vantagens em se cadastrar.

Obtenha o máximo da experiência do InfoQ Brasil.

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Comentários da comunidade

  • problemas

    by fernando zimmermann /

    Seu comentário está aguardando aprovação dos moderadores. Obrigado por participar da discussão!

    ja me incomodei muito com isso, principalmente dependencias ciclicas, vindas do hibernate e ainda com um combo agravante de lazyInitialization;
    no final das contas, fiz meu converter na unha:
    dto.serPropriedate(obj.getPropriedade());

  • Re: problemas

    by Otavio Santana /

    Seu comentário está aguardando aprovação dos moderadores. Obrigado por participar da discussão!

    É sempre uma opção :)
    Quando eu faço isso, geralmente, eu crio uma classe responsável apenas por essa tarefa.

  • Re: problemas

    by fernando zimmermann /

    Seu comentário está aguardando aprovação dos moderadores. Obrigado por participar da discussão!

    exatamente!

  • Performance de serialização.

    by Samuel Santos /

    Seu comentário está aguardando aprovação dos moderadores. Obrigado por participar da discussão!

    Parabéns pelo artigo, colocou de forma clara as vantagens e desvantagens.

    Em relação ao ModelMapper, estive pesquisando estes dias sobre a performance de serialização e achei um artigo sobre e nele mostrava que o MapStruct era mais eficiente, o que você tem a dizer, se já fez algum benchmark em relação a isto?

    Obrigado pela contribuição

  • Re: Performance de serialização.

    by Otavio Santana /

    Seu comentário está aguardando aprovação dos moderadores. Obrigado por participar da discussão!

    O MapStruct me parece muito legal. O meu único pé atrás com relação a ele são as opções de configurações. Nesse sentido, eu sinto que ele está um pouco atrás.
    Mas se ele atende para você não tenho nada contra :)

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

BT

Seu cadastro no InfoQ está atualizado? Poderia rever suas informações?

Nota: se você alterar seu email, receberá uma mensagem de confirmação

Nome da empresa:
Cargo/papel na empresa:
Tamanho da empresa:
País:
Estado:
Você vai receber um email para validação do novo endereço. Esta janela pop-up fechará em instantes.