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

36

Eu encontrei uma árvore de herança em nossa base de código (bastante grande) que é algo assim:

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

public class OrderDateInfo : NamedEntity { }

Pelo que consegui, isso é usado principalmente para vincular as coisas no front-end.

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

Existem desvantagens nessa abordagem?

    
por robotron 12.12.2018 / 12:39
fonte

5 respostas

12

For me, this makes sense as it gives a concrete name to the class, instead of relying on the generic NamedEntity. On the other hand, there is a number of such classes that simply have no additional properties.

Are there any downsides to this approach?

A abordagem não é ruim, mas existem soluções melhores disponíveis. Em suma, 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 você pode implementar muitas interfaces .

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

One nã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ê se depara com um problema. Você pode criar uma classe base AuditedEntity , mas não pode fazer com que Three herde 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 tendo AuditedEntity herdado de NamedEntity . Este é um truque inteligente para garantir que cada classe precise herdar (diretamente) de uma 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 Four herdar de AuditedEntity , já que você também o tornaria NamedEntity devido à herança entre AuditedEntity and NamedEntity '.

Usando herança, não há como fazer com que Three e Four funcionem, a menos que você comece a duplicar 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 desvantagem menor 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);

On the other hand, there is a number of such classes that simply have no additional properties.

Esta é uma variação do padrão de interface do marcador , onde 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 a meta é a mesma, então vou me referir a ela como uma "classe marcada".

No valor de face, não há nada de errado com interfaces / classes de marcadores. Eles são sintática e tecnicamente válidos, e não há desvantagens inerentes em usá-los 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 possuem propriedades / métodos adicionais em comparação com o método base.

Portanto, não há nada de errado em fazer isso, mas eu aconselho usar isso com cautela, certificando-se de que você não esteja apenas tentando encobrir um erro arquitetural existente com um polimorfismo mal projetado.

    
por 13.12.2018 / 08:14
fonte
62

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

Digamos que você tenha 15 classes diferentes que têm NamedEntity como uma classe base em algum lugar de sua cadeia de herança e você está escrevendo um novo método que só é aplicável 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 FooBazNamedEntity para lá.

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 cometerem erros.

Além disso, como @candied_orange apontou , as exceções são um ótimo exemplo disso. Muito raramente (e eu quero dizer muito, muito, muito raramente) você já quer pegar tudo com catch (Exception e) . É mais provável que você queira capturar uma SqlException ou uma FileNotFoundException ou uma exceção personalizada para seu aplicativo. Essas classes muitas vezes não fornecem mais dados ou funcionalidade do que a base Exception class, mas permitem diferenciar o que elas representam sem precisar inspecioná-las e verificar um campo de texto ou pesquisar por um texto específico.

No geral, é um truque fazer com que o sistema de tipos permita usar um conjunto de tipos mais restrito do que se você usasse uma classe base. Quer 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 seria?

    
por 12.12.2018 / 13:53
fonte
27

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

A questão usual de herança que leva a longas cadeias e causa problema do yo-yo não se aplica aqui desde que não há nada para motivá-lo a cadeia.

    
por 12.12.2018 / 13:34
fonte
1

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

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

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

OrderDateInfo HAS 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 que aceitasse NamedEntity como um parâmetro só deveria estar interessado nas propriedades Id e Name , portanto, esses métodos devem ser alterados para aceitar EntityName .

A única razão técnica que eu aceito para o design original é a propriedade, que o OP mencionou. Um framework de porcaria não seria capaz de lidar com a propriedade extra e ligar-se a object.Name.Id . Mas se a sua estrutura de ligação não puder lidar com isso, você terá mais alguma dívida de tecnologia para adicionar à lista.

Eu concordaria com a resposta do @ Flater, mas com muitas interfaces contendo propriedades, você acaba escrevendo muitos códigos clichê, mesmo com as adoráveis propriedades automáticas do C #. Imagine fazer isso em Java!

    
por 13.12.2018 / 16:06
fonte
1

As classes expõem o comportamento e as estruturas de dados expõem os dados.

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

Why have a common top level data structure?

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

Why have a data structure that includes the top level one, but adds nothing?

Assim, você pode usar o tipo de dados de nível inferior. Isso permite colocar sugestões no sistema de digitação para expressar a finalidade da variável

Top level data structure - Named
   property: name;

Bottom level data structure - Person

Na hierarquia acima, achamos conveniente especificar que uma Pessoa é nomeada, para que as pessoas possam obter e alterar seu nome. Embora possa ser razoável adicionar propriedades extras à estrutura de dados Person , o problema que estamos resolvendo não exige uma modelagem perfeita da Person e, portanto, deixamos de adicionar propriedades comuns como age , etc.

Portanto, é uma alavancagem do sistema de digitação para expressar a intenção deste item Nomeado de uma forma que não rompa com as atualizações (como a documentação) e pode ser estendido em uma data posterior com facilidade (se você achar que realmente precisa o campo age depois).

    
por 13.12.2018 / 17:34
fonte