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.
problemas
by fernando zimmermann,
Re: problemas
by Otavio Santana,
Re: problemas
by fernando zimmermann,
Performance de serialização.
by Samuel Santos,
Re: Performance de serialização.
by Otavio Santana,
DTO entre Camada Física ( TIER ) x Camada Lógica ( Layer )
by Edison Martins,
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 :)
DTO entre Camada Física ( TIER ) x Camada Lógica ( Layer )
by Edison Martins,
Seu comentário está aguardando aprovação dos moderadores. Obrigado por participar da discussão!
Em resumo:
Sou muito a favor de usar DTO entre camadas física.
Entre a camada lógica, aumenta a complexidade sem necessidade.