Por que herdar uma classe e não adicionar propriedades?

39

Encontrei uma árvore de herança em nossa base de código (bastante grande) que é mais ou menos assim:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class OrderDateInfo : NamedEntity { }

Pelo que pude entender, isso é usado principalmente para vincular coisas no front-end.

Para mim, isso faz sentido, pois atribui um nome concreto à classe, em vez de depender do genérico NamedEntity. Por outro lado, existem várias dessas classes que simplesmente não têm propriedades adicionais.

Há alguma desvantagem nessa abordagem?

robotron
fonte
25
Upside não mencionados: Você pode distinguir métodos que são relevantes apenas para OrderDateInfos daqueles que são relevantes para outras NamedEntitys
Caleth
8
Pela mesma razão que, se você tiver, digamos, uma Identifierclasse que não tem nada a ver com NamedEntityexigir uma propriedade Ide Name, você não a usaria NamedEntity. O contexto e o uso adequado de uma classe são mais do que apenas as propriedades e métodos que ela contém.
Neil

Respostas:

15

Para mim, isso faz sentido, pois atribui um nome concreto à classe, em vez de confiar no NamedEntity genérico. Por outro lado, existem várias dessas classes que simplesmente não têm propriedades adicionais.

Há alguma desvantagem nessa abordagem?

A abordagem não é ruim, mas existem soluções melhores disponíveis. Em resumo, uma interface seria uma solução muito melhor para isso. A principal razão pela qual interfaces e herança são diferentes aqui é porque você só pode herdar de uma classe, mas pode implementar muitas interfaces .

Por exemplo, considere que você nomeou entidades e entidades auditadas. Você tem várias entidades:

Onenão é uma entidade auditada nem uma entidade nomeada. Isso é simples:

public class One 
{ }

Twoé uma entidade nomeada, mas não uma entidade auditada. Isso é essencialmente o que você tem agora:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Two : NamedEntity 
{ }

Threeé uma entrada nomeada e auditada. É aqui que você encontra um problema. Você pode criar uma AuditedEntityclasse base, mas não pode Threeherdar ambos AuditedEntity e NamedEntity :

public class AuditedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : NamedEntity, AuditedEntity  // <-- Compiler error!
{ }

No entanto, você pode pensar em uma solução alternativa AuditedEntityherdando NamedEntity. Este é um truque inteligente para garantir que toda classe precise herdar (diretamente) de outra classe.

public class AuditedEntity : NamedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : AuditedEntity
{ }

Isso ainda funciona. Mas o que você fez aqui é declarado que toda entidade auditada é inerentemente também uma entidade nomeada . O que me leva ao meu último exemplo. Fouré uma entidade auditada, mas não uma entidade nomeada. Mas você não pode deixar Fourherdar, AuditedEntitypois você também o tornaria NamedEntitydevido à herança entre AuditedEntity andNamedEntity`.

Usando a herança, não há como criar os dois Threee Fourfuncionar, a menos que você comece a duplicar as classes (o que abre um novo conjunto de problemas).

Usando interfaces, isso pode ser facilmente alcançado:

public interface INamedEntity
{
    int Id { get; set; }
    string Name { get; set; }
}

public interface IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class One 
{ }

public class Two : INamedEntity 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Three : INamedEntity, IAuditedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class Four : IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

A única pequena desvantagem aqui é que você ainda precisa implementar a interface. Mas você obtém todos os benefícios de ter um tipo reutilizável comum, sem nenhuma das desvantagens que surgem quando você precisa de variações em vários tipos comuns para uma determinada entidade.

Mas seu polimorfismo permanece intacto:

var one = new One();
var two = new Two();
var three = new Three();
var four = new Four();

public void HandleNamedEntity(INamedEntity namedEntity) {}
public void HandleAuditedEntity(IAuditedEntity auditedEntity) {}

HandleNamedEntity(one);    //Error - not a named entity
HandleNamedEntity(two);
HandleNamedEntity(three);  
HandleNamedEntity(four);   //Error - not a named entity

HandleAuditedEntity(one);    //Error - not an audited entity
HandleAuditedEntity(two);    //Error - not an audited entity
HandleAuditedEntity(three);  
HandleAuditedEntity(four);

Por outro lado, existem várias dessas classes que simplesmente não têm propriedades adicionais.

Essa é uma variação no padrão da interface do marcador , em que você implementa uma interface vazia apenas para poder usar o tipo de interface para verificar se uma determinada classe está "marcada" com essa interface.
Você está usando classes herdadas em vez de interfaces implementadas, mas o objetivo é o mesmo, então vou me referir a ela como uma "classe marcada".

No valor nominal, não há nada de errado com interfaces / classes de marcadores. Eles são sintaticamente e tecnicamente válidos e não há desvantagens inerentes ao seu uso, desde que o marcador seja universalmente verdadeiro (em tempo de compilação) e não condicional .

É exatamente assim que você deve diferenciar entre exceções diferentes, mesmo quando essas exceções não têm realmente propriedades / métodos adicionais comparados ao método base.

Portanto, não há nada de errado em fazê-lo, mas eu aconselho a usá-lo com cautela, certificando-se de que você não esteja apenas tentando encobrir um erro arquitetônico existente com polimorfismo mal projetado.

Flater
fonte
4
"você só pode herdar de uma classe, mas pode implementar muitas interfaces." Isso não é verdade para todas as línguas, no entanto. Alguns idiomas permitem herança de várias classes.
19418 Kenneth K.
Como Kenneth disse, duas linguagens bastante populares têm a capacidade de herança múltipla, C ++ e Python. A classe threeno seu exemplo das armadilhas de não usar interfaces é realmente válida em C ++, por exemplo, na verdade, isso funciona corretamente para todos os quatro exemplos. De fato, tudo isso parece ser uma limitação do C # como uma linguagem, e não uma advertência de não usar herança quando você deveria usar interfaces.
whn
63

Isso é algo que eu uso para impedir que o polimorfismo seja usado.

Digamos que você tenha 15 classes diferentes que tenham NamedEntitycomo classe base em algum lugar da cadeia de herança e que esteja escrevendo um novo método aplicável apenas a OrderDateInfo.

Você "poderia" apenas escrever a assinatura como

void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)

E espere e reze para que ninguém abuse do sistema de tipos para empurrar um para FooBazNamedEntitydentro.

Ou você "poderia" apenas escrever void MyMethod(OrderDateInfo foo). Agora isso é imposto pelo compilador. Simples, elegante e não depende de pessoas que não cometem erros.

Além disso, como apontou @candied_orange , as exceções são um ótimo caso disso. Muito raramente (e eu quero dizer muito, muito, muito raramente) você quer pegar tudo com ele catch (Exception e). O mais provável é que você quer pegar um SqlExceptionou um FileNotFoundExceptionou uma exceção personalizada para a sua aplicação. Muitas vezes, essas classes não fornecem mais dados ou funcionalidade do que a Exceptionclasse base , mas permitem diferenciar o que representam sem ter que inspecioná-las e verificar um campo de tipo ou procurar texto específico.

No geral, é um truque para o sistema de tipos permitir que você use um conjunto mais restrito de tipos do que você usaria se usasse uma classe base. Quero dizer, você poderia definir todas as suas variáveis ​​e argumentos como tendo o tipo Object, mas isso apenas tornaria seu trabalho mais difícil, não é?

Becuzz
fonte
4
Perdoe minha novidade, mas estou curioso por que não seria uma abordagem melhor tornar OrderDateInfo TER um objeto NamedEntity como uma propriedade?
Adam B
9
@AdamB Essa é uma opção de design válida. Se é melhor ou não, depende do motivo pelo qual será usado, entre outras coisas. No caso de exceção, não posso criar uma nova classe com uma propriedade de exceção, pois o idioma (C # para mim) não me permite lançá-la. Pode haver outras considerações de design que impediriam o uso de composição em vez de herança. Muitas vezes a composição é melhor. Depende apenas do seu sistema.
Becuzz
17
@AdamB Pela mesma razão que, quando você herda de uma classe base e adiciona métodos ou propriedades à base, não cria uma nova classe e coloca a base como uma propriedade. Há uma diferença entre um ser humano ser um mamífero e ter um mamífero.
Logarr
8
@Logarr verdade, há uma diferença entre eu ser um mamífero e ter um mamífero. Mas se eu permanecer fiel ao meu mamífero interior, você nunca saberá a diferença. Você pode se perguntar por que posso dar tantos tipos diferentes de leite por capricho.
Candied_orange
5
@ AdamB Você pode fazer isso, e isso é conhecido como composição sobre herança. Mas você renomeará NamedEntitypara isso, por exemplo EntityName, para , para que a composição faça sentido quando descrita.
Sebastian Redl
28

Este é o meu uso favorito de herança. Eu o uso principalmente para exceções que poderiam usar nomes melhores e mais específicos

A questão usual da herança que leva a longas cadeias e causa o problema do ioiô não se aplica aqui, pois não há nada para motivá-lo a encadear.

candied_orange
fonte
1
Hmm, bom ponto re exceções. Eu ia dizer que era uma coisa horrível de se fazer. Mas você me convenceu do contrário com um excelente caso de uso.
David Arno
Yo-Yo Ho e uma garrafa de rum. É raro o orlop ser yar. Muitas vezes vaza por falta de carvalho adequado entre as tábuas. Ao armário de Davey Jones com os lubrificantes terrestres que o construíram. TRADUÇÃO: É raro uma cadeia de herança ser tão boa que a classe base possa ser abstratamente / efetivamente ignorada.
Radarbob 13/12/19
Eu acho que é importante ressaltar que uma nova classe herdar de outro não adicionar uma nova propriedade - o nome da nova classe. É exatamente isso que você usa ao criar exceções com nomes melhores.
Logan Pickup
1

IMHO o design da classe está errado. Deveria ser.

public class EntityName
{
    public int Id { get; set; }
    public string Text { get; set; }
}

public class OrderDateInfo
{
    public EntityName Name { get; set; }
}

OrderDateInfoHAS A Nameé uma relação mais natural e cria duas classes fáceis de entender que não teriam provocado a pergunta original.

Qualquer método aceito NamedEntitycomo parâmetro deveria ter apenas interesse nas propriedades Ide Name, portanto, esses métodos devem ser alterados para serem aceitos EntityName.

A única razão técnica que eu aceitaria para o design original é a vinculação de propriedades, mencionada pelo OP. Uma estrutura de porcaria não seria capaz de lidar com a propriedade extra e vincular-se a object.Name.Id. Mas se sua estrutura vinculativa não puder lidar com isso, você terá mais algumas dívidas de tecnologia para adicionar à lista.

Eu concordaria com a resposta do @ Flater, mas com muitas interfaces contendo propriedades, você acaba escrevendo muito código padrão, mesmo com as adoráveis ​​propriedades automáticas do C #. Imagine fazê-lo em Java!

batwad
fonte
1

Classes expõem o comportamento e Estruturas de dados expõem dados.

Vejo as palavras-chave da classe, mas não vejo nenhum comportamento. Isso significa que eu começaria a visualizar esta classe como uma estrutura de dados. Nesse sentido, vou reformular sua pergunta como

Por que ter uma estrutura de dados comum de nível superior?

Então você pode usar o tipo de dados de nível superior. Isso permite aproveitar o sistema de tipos para garantir uma política entre grandes conjuntos de estruturas de dados diferentes, garantindo que as propriedades estejam todas lá.

Por que ter uma estrutura de dados que inclui a de nível superior, mas não acrescenta nada?

Então você pode usar o tipo de dados de nível inferior. Isso permite colocar dicas no sistema de digitação para expressar o objetivo da variável

Top level data structure - Named
   property: name;

Bottom level data structure - Person

Na hierarquia acima, achamos conveniente especificar que uma Pessoa seja nomeada, para que as pessoas possam obter e alterar seu nome. Embora possa ser razoável adicionar propriedades extras à Personestrutura de dados, o problema que estamos resolvendo não exige uma modelagem perfeita da Pessoa e, portanto, negligenciamos a adição de propriedades comuns como ageetc.

Portanto, é uma alavancagem do sistema de digitação expressar a intenção desse item Nomeado de uma maneira que não seja incompatível com as atualizações (como a documentação) e possa ser estendida em uma data posterior com facilidade (se você realmente precisar do agecampo posteriormente )

Edwin Buck
fonte