Quando você está escrevendo código, procurando uma solução elegante, ou não, para algum problema de código, ou de arquitetura, a pergunta que deveria ser feita é: alguém já enfrentou esse problema antes? Existe uma grande chance que sim, que este problema já tenha uma ou mais soluções padronizadas que o resolvam. Como desenvolvedores temos que ser eficientes, e nada melhor que usar o conhecimento alheio 😄
O problema
Você trabalha em uma instituição financeira, que tem um serviço onde pode-se fazer transferências internacionais. Os desenvolvedores do app precisam mostrar a listra de transações realizadas num período, e para cada item da lista você pode clicar e ver mais detalhes daquela transação.
Do lado do backend, você tem as seguintes entidades:
Poderia até ter mais atributos cada uma dessas entidades, mas não precisamos entrar afundo nesse detalhe.
A resposta do endpoint para listar transações será simples, nome do beneficiado, tipo de transação, valor enviado e data. Como vc resolve isso?
Uma possível solução
Existem muitas maneiras de se resolver um problema, pode-se simplesmente trazer os dados do banco, converter-los para seu modelo, se estiver usando algum ORM, e deixar disponível para consumo no formato JSON, vai funcionar, sim, é o ideal? Vamos falar mais sobre.
Essa maneira de resolver traz pelo menos dois potenciais problemas:
O primeiro que é mais fácil de ver, tem muita informação que é irrelevante para aplicação cliente, pelo menos nesse contexto. Então vc pensa: ah é só a aplicação que estiver consumindo ignorar o que não precisa. Eu entendo, mas perceba que vc está desperdiçando recursos, o que vc pede e não usa é desperdício.
Esse talvez também seja fácil de pegar, mas a princípio, talvez não seja importante, dependendo do caso. Expor seus modelos internos de forma pública vai tornar a manutenção ao longo prazo extremamente difícil. E por que? Se precisar mudar a estrutura de dados, como renomear um campo, isso vai gerar uma quebra de contrato com o mundo exterior, quem está consumindo essa estrutura. Aquele campo que tinha um nome X agora é XX, pronto, quebrou as aplicações consumidoras.
Outra maneira é criar um novo modelo que tenha exatamente as informações que são necessárias, soa melhor, não acha?
Então vamos criar uma classe que represente esses dados.
// DTO para listar transações
public record TransacaoListaDTO(
string NomeBeneficiario,
string TipoTransacao,
decimal ValorEnviado,
DateTime Data
);
O que acabamos de criar agora é uma classe que define o que chamamos de Data Transfer Object [Fowler PoEAA], mais conhecido como DTO, que funciona como uma interface de entrada e saída de uma aplicação, Se pensarmos em uma API REST que retorna um JSON, é esse objeto que será serializado na saída e também é usado como dados de entrada, para criar ou atualizar um recurso. Porém DTOs não se limitam a API REST, pode ser usado em qualquer outro tipo de integração.
// Pode-se ter um controller mais ou menos assim
public class TransacoesController : ControllerBase
{
[HttpGet]
public ActionResult<TransacaoListaDTO[]> ListarTransacoes()
{
// Lógica para buscar transações
var transacoes = _repositorio.ObterTransacoes():
var dtos = transacoes.Select(ts =>
{
new TransacaoListaDTO(
NomeBeneficiario: $"{ts.Beneficiario.Nome} {ts.Beneficiario.Sobrenome}",
TipoTransacao: ts.TipoTransacao,
ValorEnviado: ts.Valor,
Data: ts.Data
)
};
return Ok(dtos);
}
}
Pois bem, parece ótimo, então toda vez que eu criar minhas APIs já vou criar um DTO de cara. Dependendo da complexidade do projeto e das entidades, não precisa, se as entidade do projeto são do tipo “anêmicas”, e seu endpoint é um CRUD bem simples é perda de tempo, não adicione complexidade onde não existe complexidade.
Continuando no nosso problema, existe uma funcionalidade para exibir os detalhes de uma transação, então será necessário mais dados do que no atual DTO para listar transações, nesse detalhe as informações necessárias são:
- Quanto foi enviado
- Qual a tarifa pra essa transação
- Nome completo de quem fez a transferência
- Código do banco
- Nome do banco
- Tipo de Conta
Então podemos simplesmente adicionar mais atributos no DTO? Claro… que não. O fato das mesmas entidades estarem envolvidas não significa que o mesmo DTO será utilizado, cada DTO serve para um propósito, o propósito do primeiro é exibir uma lista de transações, entretanto agora o propósito é dar detalhes sobre a transferência. Seria algo parecido com isso:
public record TransacaoDetalhesDTO(
decimal ValorEnviado,
decimal Tarifa,
string NomeCompleto,
string CodigoBanco,
string NomeBanco,
string TipoConta
);
E finalmente eu chego no título do post, existem algumas informações que são iguais, então partindo do princípio DRY, é melhor criar um DTO base e outros DTOs herdarem, certo? Não, heranças parecem boas até que vc precisa mudar algo na classe base efeitos colaterais podem ser espalhados pelas classes. Tente manter os DTOs mais isolados possível, assim garante-se que uma mudança no DTO irá afetar diretamente os consumidores daqueles DTOs, dessa forma há menos dor de cabeça quanto mudanças forem necessárias, e elas sempre aparecem.
Rolou até esse assunto em grupo de desenvolvedores onde participo se isso seria uma duplicação de código. Eu não considero uma duplicação, pra mim, para ser uma duplicação teria que sera mesma classe com os mesmos atributos, no caso apresentado nesse post as classes são parecidas, mas não iguais.
Resumindo
- DTOs (Data Transfer Objects) são usados como interface de entrada e saída em APIs.
- DTOs ajudam a evitar expor modelos internos e desperdiçar recursos.
- Crie DTOs diferentes para propósitos distintos, mesmo que envolvam as mesmas entidades.
- Evite herança entre DTOs para reduzir efeitos colaterais.
- DTOs semelhantes não são considerados duplicação de código se tiverem propósitos diferentes.
- Use DTOs quando necessário, mas evite adicionar complexidade desnecessária em projetos simples.