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?
object-oriented-design
inheritance
robotron
fonte
fonte
OrderDateInfo
s daqueles que são relevantes para outrasNamedEntity
sIdentifier
classe que não tem nada a ver comNamedEntity
exigir uma propriedadeId
eName
, você não a usariaNamedEntity
. O contexto e o uso adequado de uma classe são mais do que apenas as propriedades e métodos que ela contém.Respostas:
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:
One
não é uma entidade auditada nem uma entidade nomeada. Isso é simples:Two
é uma entidade nomeada, mas não uma entidade auditada. Isso é essencialmente o que você tem agora:Three
é uma entrada nomeada e auditada. É aqui que você encontra um problema. Você pode criar umaAuditedEntity
classe base, mas não podeThree
herdar ambosAuditedEntity
eNamedEntity
:No entanto, você pode pensar em uma solução alternativa
AuditedEntity
herdandoNamedEntity
. Este é um truque inteligente para garantir que toda classe precise herdar (diretamente) de outra classe.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 deixarFour
herdar,AuditedEntity
pois você também o tornariaNamedEntity
devido à herança entre AuditedEntityand
NamedEntity`.Usando a herança, não há como criar os dois
Three
eFour
funcionar, 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:
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:
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.
fonte
three
no 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.Isso é algo que eu uso para impedir que o polimorfismo seja usado.
Digamos que você tenha 15 classes diferentes que tenham
NamedEntity
como classe base em algum lugar da cadeia de herança e que esteja escrevendo um novo método aplicável apenas aOrderDateInfo
.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
FooBazNamedEntity
dentro.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 umSqlException
ou umFileNotFoundException
ou uma exceção personalizada para a sua aplicação. Muitas vezes, essas classes não fornecem mais dados ou funcionalidade do que aException
classe 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 é?fonte
NamedEntity
para isso, por exemploEntityName
, para , para que a composição faça sentido quando descrita.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.
fonte
IMHO o design da classe está errado. Deveria ser.
OrderDateInfo
HAS AName
é 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
NamedEntity
como parâmetro deveria ter apenas interesse nas propriedadesId
eName
, portanto, esses métodos devem ser alterados para serem aceitosEntityName
.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!
fonte
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
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á.
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
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 à
Person
estrutura de dados, o problema que estamos resolvendo não exige uma modelagem perfeita da Pessoa e, portanto, negligenciamos a adição de propriedades comuns comoage
etc.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
age
campo posteriormente )fonte