Usar composição e herança para DTOs


13

Temos uma API Web do ASP.NET que fornece uma API REST para nosso aplicativo de página única. Usamos DTOs / POCOs para transmitir dados por essa API.

O problema agora é que esses DTOs estão aumentando ao longo do tempo, então agora queremos refatorar os DTOs.

Estou procurando "práticas recomendadas" como criar um DTO: Atualmente, temos pequenos DTOs que consistem apenas em campos de valor, por exemplo:

public class UserDto
{
    public int Id { get; set; }

    public string Name { get; set; }
}

Outros DTOs usam esse UserDto por composição, por exemplo:

public class TaskDto
{
    public int Id { get; set; }

    public UserDto AssignedTo { get; set; }
}

Além disso, existem alguns DTOs estendidos que são definidos pela herança de outros, por exemplo:

public class TaskDetailDto : TaskDto
{
    // some more fields
}

Como alguns DTOs foram usados ​​para vários pontos de extremidade / métodos (por exemplo, GET e PUT), eles foram estendidos de forma incremental por alguns campos ao longo do tempo. E devido à herança e composição, outros DTOs também ficaram maiores.

Minha pergunta agora é: herança e composição não são boas práticas? Mas quando não as reutilizamos, parece que escrevemos o mesmo código várias vezes. É uma má prática usar um DTO para vários pontos de extremidade / métodos ou deve haver DTOs diferentes, que diferem apenas em algumas nuances?


6
Não sei dizer o que você deve fazer, mas, de acordo com minha experiência em trabalhar com DTO, a herança e a composição, mais cedo ou mais tarde, caçam você como um café ruim. Eu nunca vou reutilizar o DTO novamente. Sempre. Além disso, não considero DTOs semelhantes como uma violação DRY. Dois pontos de extremidade que retornam a mesma representação podem reutilizar os mesmos DTOs. Dois pontos de extremidade que retornam representações semelhantes não estão retornando os mesmos DTOs, então eu faço DTOs específicos para cada um . Se eu fosse forçado a escolher, a composição é a menos problemática a longo prazo.
21417 Laiv

@ Laiv esta é a resposta correta para a pergunta, apenas não. Não sei por que você o colocou como comentário #
TheCatWhisperer 17/17/17

2
@Laiv: O que você usa em vez disso? Na minha experiência, as pessoas que lutam com isso simplesmente estão pensando demais. Um DTO é apenas um contêiner de dados, e isso é tudo.
Robert Harvey

TheCatWhisperer porque meus argumentos seriam principalmente baseados em opiniões. Ainda estou tentando resolver esse tipo de problema em meus projetos. @RobertHarvey é verdade, não sei por que costumo ver as coisas mais do que realmente são. Ainda estou trabalhando na solução. Eu estava bastante convencido de que o HAL era o modelo destinado a resolver esses problemas, mas, ao ler sua resposta, percebi que também faço meu DTO granular. Então, colocarei em prática sua abordagem primeiro. As mudanças serão menos dramáticas do que mudar completamente para o HATEOAS.
21417 Laiv

Tente isto para uma boa discussão que podem ajudá-lo: stackoverflow.com/questions/6297322/...
johnny

Respostas:


9

Como prática recomendada, tente tornar seus DTOs o mais conciso possível. Retorne apenas o que você precisa retornar. Use apenas o que você precisa usar. Se isso significa alguns DTOs extras, que assim seja.

No seu exemplo, uma tarefa contém um usuário. Provavelmente não é necessário um objeto de usuário completo, talvez apenas o nome do usuário ao qual a tarefa foi atribuída. Não precisamos do restante das propriedades do usuário.

Digamos que você queira reatribuir uma tarefa a um usuário diferente; pode-se tentar passar um objeto de usuário completo e um objeto de tarefa completa durante a postagem de reatribuição. Mas, o que é realmente necessário é apenas o ID da tarefa e o ID do usuário. Essas são as únicas duas informações necessárias para reatribuir uma tarefa; portanto, modele o DTO como tal. Isso geralmente significa que há um DTO separado para cada chamada de descanso, se você estiver buscando um modelo DTO enxuto.

Além disso, às vezes pode-se precisar de herança / composição. Digamos que alguém tenha um emprego. Um trabalho tem várias tarefas. Nesse caso, conseguir um emprego também pode retornar a lista de tarefas para o trabalho. Portanto, não há regra contra a composição. Depende do que está sendo modelado.


O mais conciso possível também significa que eu deveria recriar DTOs se eles diferirem em alguns detalhes - não é?
oficial

1
@officer - Em geral, sim.
Jon Raynor

2

A menos que seu sistema seja estritamente baseado em operações CRUD, seus DTOs são muito granulares. Tente criar terminais que incorporem processos de negócios ou artefatos. Essa abordagem é bem mapeada para uma camada de lógica de negócios e para o "design controlado por domínio" de Eric Evans.

Por exemplo, digamos que você tenha um ponto de extremidade que retorna dados para uma fatura para que ela possa ser exibida em uma tela ou formulário para o usuário final. Em um modelo CRUD, você precisaria de várias chamadas para seus terminais para reunir as informações necessárias: nome, endereço de cobrança, endereço de entrega, itens de linha. Em um contexto de transação comercial, um único DTO de um único terminal pode retornar todas essas informações de uma só vez.


Robert, você quer dizer com uma raiz agregada aqui?
21917 johnny

Atualmente, nossos DTOs são projetados para fornecer todos os dados de um formulário, por exemplo, ao exibir uma lista de tarefas, o TodoListDTO possui uma Lista de TaskDTOs. Mas como estamos reutilizando o TaskDTO, cada um deles também possui um UserDTO. Portanto, o problema é a grande quantidade de dados consultados no back-end e enviados por fio.
oficial

Todos os dados são necessários?
Robert Harvey

1

É difícil estabelecer práticas recomendadas para algo tão "flexível" ou abstrato como um DTO. Essencialmente, os DTOs são apenas objetos para transferência de dados, mas dependendo do destino ou do motivo da transferência, convém aplicar diferentes "práticas recomendadas".

Eu recomendo a leitura de Patterns of Enterprise Application Architecture de Martin Fowler . Há um capítulo inteiro dedicado aos padrões, onde os DTOs recebem uma seção realmente detalhada.

Originalmente, eles foram "projetados" para serem usados ​​em chamadas remotas caras, nas quais você provavelmente precisaria de muitos dados de diferentes partes da sua lógica; Os DTOs realizariam a transferência de dados em uma única chamada.

Segundo o autor, os DTOs não foram projetados para uso em ambientes locais, mas algumas pessoas encontraram um uso para eles. Geralmente, eles são usados ​​para reunir informações de diferentes POCOs em uma única entidade para GUIs, APIs ou camadas diferentes.

Agora, com a herança, a reutilização de código é mais um efeito colateral da herança do que seu principal objetivo; a composição, por outro lado, é implementada com a reutilização de código como objetivo principal.

Algumas pessoas recomendam o uso da composição e da herança juntos, usando os pontos fortes de ambos e tentando mitigar suas fraquezas. A seguir, parte do meu processo mental ao escolher ou criar novos DTOs ou qualquer nova classe / objeto para esse assunto:

  • Uso herança com DTOs dentro da mesma camada ou mesmo contexto. Um DTO nunca herdará de um POCO, um DLL BLL nunca herdará de um DAL DTO, etc.
  • Se eu estiver tentando esconder um campo de um DTO, refatorarei e talvez use a composição.
  • Se forem necessários muito poucos campos diferentes de um DTO base, eu os colocarei em um DTO universal. DTOs universais são usados ​​apenas internamente.
  • Um POCO / DTO básico quase nunca será usado para qualquer lógica, dessa forma a base responde apenas às necessidades de seus filhos. Se eu precisar usar a base, evito adicionar qualquer novo campo que seus filhos nunca usem.

Algumas delas talvez não sejam as "melhores práticas", elas funcionam muito bem para os projetos nos quais estou trabalhando, mas é preciso lembrar que nenhum tamanho serve para todos. No caso do DTO universal, você deve ter cuidado, minhas assinaturas de métodos são assim:

public void DoSomething(BaseDTO base) {
    //Some code 
}

Se algum dos métodos precisar de seu próprio DTO, eu herdo a herança e, geralmente, a única alteração que preciso fazer é o parâmetro, embora às vezes precise aprofundar-me em casos específicos.

Pelos seus comentários, percebo que você está usando DTOs aninhados. Se seus DTOs aninhados consistem apenas em uma lista de outros DTOs, acho que a melhor coisa a fazer é desembrulhar a lista.

Dependendo da quantidade de dados que você precisa exibir ou trabalhar, pode ser uma boa ideia criar novos DTOs que limitem os dados; por exemplo, se o UserDTO tiver muitos campos e você precisar apenas de 1 ou 2, talvez seja melhor ter um DTO apenas com esses campos. Definir a camada, contexto, uso e utilidade de um DTO ajudará muito ao projetá-lo.


Não consegui adicionar mais de dois links na minha resposta. Aqui estão mais algumas informações sobre DTOs locais. Estou ciente de que são informações muito antigas, mas acho que algumas delas talvez ainda sejam relevantes.
IvanGrasp

1

Usar composição em DTOs é uma prática perfeitamente adequada.

A herança entre tipos concretos de DTOs é uma má prática.

Por um lado, em um idioma como C #, uma propriedade auto-implementada tem muito pouca sobrecarga de manutenção, portanto, duplicá-las (eu realmente odeio a duplicação) não é tão prejudicial quanto costuma ser.

Um motivo não para usar a herança concreta entre os DTOs é que certas ferramentas os mapeiam com prazer para os tipos errados.

Por exemplo, se você usar um utilitário de banco de dados como o Dapper (eu não recomendo, mas é popular) que executa class name -> table name inferência, você pode facilmente salvar um tipo derivado como um tipo de base concreto em algum lugar da hierarquia e, assim, perder dados ou pior .

Um motivo mais profundo para não usar herança entre DTOs é que ele não deve ser usado para compartilhar implementações entre tipos que não possuem um relacionamento óbvio "é um". Na minha opinião, TaskDetailnão soa como um subtipo de Task. Poderia facilmente ser uma propriedade de um Taskou, pior, poderia ser um supertipo de Task.

Agora, uma coisa com a qual você pode se preocupar é manter a consistência entre os nomes e os tipos das propriedades de vários DTOs relacionados.

Embora a herança de tipos concretos, como conseqüência, ajude a garantir essa consistência, é muito melhor usar interfaces (ou classes básicas virtuais puras em C ++) para manter esse tipo de consistência.

Considere os seguintes DTOs

interface IIdentity
{
    int Id { get; set; }
}

interface INamed
{
    string Name { get; set; }
}

public class UserDto: IIdentity, INamed
{
    public int Id { get; set; }

    public string Name { get; set; }

    // User specific properties
}

public class TaskDto: IIdentity
{
    public int Id { get; set; }

    // Task specific properties
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.