Perché ereditare una classe e non aggiungere proprietà?

36

Ho trovato un albero di ereditarietà nel nostro (piuttosto grande) codice base che va in questo modo:

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

public class OrderDateInfo : NamedEntity { }

Da quello che ho potuto raccogliere, questo è principalmente usato per legare cose sul front-end.

Per me, questo ha senso in quanto dà un nome concreto alla classe, invece di basarsi sul generico NamedEntity . D'altra parte, c'è un certo numero di tali classi che semplicemente non hanno proprietà aggiuntive.

Ci sono dei lati negativi in questo approccio?

    
posta robotron 12.12.2018 - 12:39
fonte

5 risposte

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?

L'approccio non è male, ma ci sono soluzioni migliori disponibili. In breve, un'interfaccia sarebbe una soluzione molto migliore per questo. Il motivo principale per cui le interfacce e l'ereditarietà sono diverse qui è perché puoi ereditare solo da una classe, ma puoi implementare molte interfacce .

Ad esempio, considera di avere un nome entità e entità controllate. Hai diverse entità:

One non è un'entità sottoposta a revisione né un'entità denominata. È semplice:

public class One 
{ }

Two è un'entità denominata ma non un'entità controllata. Questo è essenzialmente ciò che hai ora:

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

public class Two : NamedEntity 
{ }

Three è sia una voce con nome che controllata. Questo è dove si incontra un problema. Puoi creare una classe base AuditedEntity , ma non puoi rendere Three ereditare entrambi AuditedEntity e NamedEntity :

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

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

Tuttavia, potresti pensare a una soluzione alternativa se AuditedEntity eredita da NamedEntity . Questo è un trucco intelligente per garantire che ogni classe debba solo ereditare (direttamente) da un'altra classe.

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

public class Three : AuditedEntity
{ }

Funziona ancora. Ma quello che hai fatto qui è che ogni entità sottoposta a verifica è intrinsecamente anche un'entità denominata . Il che mi porta al mio ultimo esempio. Four è un'entità sottoposta a revisione ma non un'entità denominata. Ma non puoi lasciare che Four erediti da AuditedEntity , altrimenti dovresti renderlo un NamedEntity a causa dell'ereditarietà di AuditedEntity and NamedEntity '.

Utilizzando l'ereditarietà, non c'è modo di far funzionare sia Three che Four a meno che non inizi a duplicare le classi (che apre una serie di problemi completamente nuova).

Utilizzando le interfacce, questo può essere facilmente raggiunto:

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; }
}

L'unico piccolo inconveniente qui è che devi ancora implementare l'interfaccia. Ma ottieni tutti i benefici dall'avere un tipo riutilizzabile comune, senza nessuno degli svantaggi che emergono quando hai bisogno di variazioni su più tipi comuni per una determinata entità.

Ma il tuo polimorfismo rimane intatto:

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.

Questa è una variante del modello di interfaccia marker , in cui si implementa un'interfaccia vuota puramente per poter utilizzare il tipo di interfaccia per verificare se una determinata classe è "contrassegnata" con questa interfaccia.
Stai utilizzando classi ereditate invece di interfacce implementate, ma l'obiettivo è lo stesso, quindi mi riferirò ad esso come una "classe contrassegnata".

Al valore nominale, non c'è niente di sbagliato nelle interfacce / classi di marcatori. Sono sintatticamente e tecnicamente validi e non ci sono inconvenienti intrinseci nell'usarli a condizione che l'indicatore sia universalmente vero (in fase di compilazione) e non condizionale .

Questo è esattamente il modo in cui devi differenziare tra le diverse eccezioni, anche quando quelle eccezioni non hanno effettivamente proprietà / metodi aggiuntivi rispetto al metodo base.

Quindi non c'è nulla di intrinsecamente sbagliato nel farlo, ma ti consiglierei di usare questo cauto, assicurandoti che non stai solo cercando di coprire un errore architettonico esistente con un polimorfismo mal progettato.

    
risposta data 13.12.2018 - 08:14
fonte
62

Questo è qualcosa che uso per prevenire l'uso del polimorfismo.

Supponiamo che tu abbia 15 diverse classi che hanno NamedEntity come una classe base da qualche parte nella loro catena di ereditarietà e stai scrivendo un nuovo metodo che è applicabile solo a OrderDateInfo .

"Potresti" semplicemente scrivere la firma come

void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)

E spera e prega che nessuno abusi del sistema di tipi per spingere un FooBazNamedEntity in là.

Oppure "potresti" scrivere solo void MyMethod(OrderDateInfo foo) . Ora viene applicato dal compilatore. Semplice, elegante e non si basa su persone che non fanno errori.

Inoltre, come sottolineato da @candied_orange , le eccezioni sono un ottimo caso. Molto raramente (e intendo molto, molto, molto raramente) vuoi mai catturare tutto con catch (Exception e) . È più probabile che tu voglia prendere un SqlException o un FileNotFoundException o un'eccezione personalizzata per la tua applicazione. Spesso queste classi non forniscono più dati o funzionalità rispetto alla classe base Exception , ma consentono di differenziare ciò che rappresentano senza doverle ispezionare e controllare un campo tipo o cercare un testo specifico.

Nel complesso, è un trucco per ottenere che il sistema di tipi ti permetta di usare un insieme di tipi più ristretto di quello che potresti se usassi una classe base. Voglio dire, puoi definire tutte le tue variabili e argomenti come aventi il tipo Object , ma questo renderebbe il tuo lavoro più difficile, vero?

    
risposta data 12.12.2018 - 13:53
fonte
27

Questo è il mio uso preferito dell'eredità. Lo uso principalmente per le eccezioni che potrebbero utilizzare nomi migliori, più specifici

Il solito problema dell'ereditariità che porta a catene lunghe e causa il problema dello yo-yo non si applica qui poiché non c'è nulla che ti motiva a catena.

    
risposta data 12.12.2018 - 13:34
fonte
1

IMHO il design della classe è sbagliato. Dovrebbe essere.

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 è una relazione più naturale e crea due classi facili da capire che non avrebbero provocato la domanda originale.

Qualsiasi metodo che ha accettato NamedEntity come parametro avrebbe dovuto essere interessato solo alle proprietà Id e Name , quindi qualsiasi metodo dovrebbe essere modificato per accettare EntityName .

L'unica ragione tecnica che accetterei per il progetto originale è l'associazione di proprietà, che l'OP ha menzionato. Un framework di crap non sarebbe in grado di gestire la proprietà extra e associare a object.Name.Id . Ma se il tuo framework vincolante non è in grado di farcela, allora hai un po 'più di debito tecnologico da aggiungere alla lista.

Vorrei andare con la risposta di @ Flater, ma con molte interfacce contenenti proprietà si finisce per scrivere un sacco di codice boilerplate, anche con le proprietà automatiche di C #. Immagina di farlo in Java!

    
risposta data 13.12.2018 - 16:06
fonte
1

Le classi espongono il comportamento e le strutture dati espongono i dati.

Vedo le parole chiave della classe, ma non vedo alcun comportamento. Ciò significa che vorrei iniziare a vedere questa classe come una struttura di dati. In questo modo, sto riformulando la tua domanda come

Why have a common top level data structure?

Quindi puoi usare il tipo di dati di primo livello. Ciò consente di sfruttare il sistema di tipi per garantire una politica attraverso ampi set di strutture di dati diverse garantendo che tutte le proprietà siano presenti.

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

Quindi puoi usare il tipo di dati di livello inferiore. Ciò consente di inserire suggerimenti nel sistema di battitura per esprimere lo scopo della variabile

Top level data structure - Named
   property: name;

Bottom level data structure - Person

Nella gerarchia di cui sopra, troviamo utile specificare che una Persona è nominata, in modo che le persone possano ottenere e modificare il loro nome. Anche se potrebbe essere ragionevole aggiungere proprietà aggiuntive alla struttura dati Person , il problema che stiamo risolvendo non richiede una perfetta modellazione della Persona, e quindi abbiamo trascurato di aggiungere proprietà comuni come age , ecc.

Quindi è una leva del sistema di battitura per esprimere l'intento di questo oggetto con nome in un modo che non si interrompe con gli aggiornamenti (come la documentazione) e può essere esteso in un secondo momento con facilità (se trovi che hai veramente bisogno il campo age in seguito).

    
risposta data 13.12.2018 - 17:34
fonte

Leggi altre domande sui tag