É 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.