Perché ereditare una classe e non aggiungere proprietà?


39

Ho trovato un albero ereditario nella nostra (piuttosto grande) base di codice 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 roba sul front-end.

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

Ci sono aspetti negativi di questo approccio?


25
Upside non menzionato: puoi distinguere metodi che sono rilevanti solo per OrderDateInfos da quelli che sono rilevanti per altri NamedEntitys
Caleth

8
Per lo stesso motivo che, se si è capitato di avere, per esempio, una Identifierclasse che non ha nulla a che fare con NamedEntity, ma che richiedono sia una Ide Nameproprietà, non userebbe NamedEntityinvece. Il contesto e l'uso corretto di una classe sono più che semplici proprietà e metodi che contiene.
Neil,

Risposte:


15

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

Ci sono aspetti negativi di questo approccio?

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 entità nominate e entità controllate. Hai diverse entità:

Onenon è un'entità controllata né un'entità denominata. È semplice:

public class One 
{ }

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

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

public class Two : NamedEntity 
{ }

Threeè sia una voce denominata che controllata. Qui è dove ti imbatti in un problema. Puoi creare una AuditedEntityclasse base, ma non puoi Threeereditare 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 AuditedEntityereditando da NamedEntity. Questo è un trucco intelligente per garantire che ogni classe abbia solo bisogno di 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 ciò che hai fatto qui è dichiarato che ogni entità controllata è intrinsecamente anche un'entità denominata . Il che mi porta al mio ultimo esempio. Fourè un'entità controllata ma non un'entità denominata. Ma non puoi lasciarti Fourereditare da AuditedEntitycome lo faresti anche a NamedEntitycausa dell'eredità tra AuditedEntity andNamedEntity`.

Usando l'ereditarietà, non c'è modo di fare entrambe le cose Threee Fourfunzionare a meno che non si inizi a duplicare le classi (il che apre una nuova serie di problemi).

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 inconveniente minore qui è che devi ancora implementare l'interfaccia. Ma ottieni tutti i vantaggi dall'avere un tipo riutilizzabile comune, senza nessuno degli svantaggi che emergono quando hai bisogno di variazioni su più tipi comuni per una data 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);

D'altra parte, esiste un numero di tali classi che semplicemente non hanno proprietà aggiuntive.

Questa è una variante del modello di interfaccia marcatore , 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 usando classi ereditate invece di interfacce implementate, ma l'obiettivo è lo stesso, quindi mi riferirò ad esso come una "classe contrassegnata".

A prima vista, non c'è niente di sbagliato nelle interfacce / classi dei marker. Sono sintatticamente e tecnicamente validi e non ci sono inconvenienti intrinseci nell'utilizzarli, a condizione che il marker sia universalmente vero (al momento della compilazione) e non condizionale .

Questo è esattamente il modo in cui dovresti distinguere tra diverse eccezioni, anche quando tali eccezioni non hanno effettivamente proprietà / metodi aggiuntivi rispetto al metodo di base.

Quindi non c'è nulla di intrinsecamente sbagliato nel farlo, ma consiglierei di usarlo con cautela, assicurandomi che non stai solo cercando di nascondere un errore architettonico esistente con un polimorfismo mal progettato.


4
"puoi ereditare solo da una classe, ma puoi implementare molte interfacce." Questo non è vero per ogni lingua, però. Alcune lingue consentono l'ereditarietà di più classi.
Kenneth K.,

come ha detto Kenneth, due linguaggi piuttosto popolari hanno la possibilità di ereditarietà multipla, C ++ e Python. la classe threenel tuo esempio delle insidie ​​del non utilizzo delle interfacce è in realtà valida in C ++ per esempio, infatti funziona correttamente per tutti e quattro gli esempi. In realtà, tutto ciò sembra essere una limitazione di C # come linguaggio piuttosto che un racconto cautelativo di non usare l'eredità quando si dovrebbero usare le interfacce.
quando il

63

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

Supponi di avere 15 classi diverse che hanno NamedEntitycome classe base da qualche parte nella loro catena di eredità 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 dei tipi per spingerlo FooBazNamedEntitydentro.

Oppure "potresti" semplicemente scrivere void MyMethod(OrderDateInfo foo). Ora questo è imposto dal compilatore. Semplice, elegante e non si basa sul fatto che le persone non commettano errori.

Inoltre, come sottolineato da @candied_orange , le eccezioni ne sono un ottimo esempio. Molto raramente (e intendo molto, molto, molto raramente) con cui vorresti mai prendere tutto catch (Exception e). Più probabilmente vuoi catturare un'o SqlExceptiono una FileNotFoundExceptiono un'eccezione personalizzata per la tua applicazione. Spesso queste classi non forniscono più dati o funzionalità rispetto alla Exceptionclasse base , ma consentono di differenziare ciò che rappresentano senza doverli ispezionare e controllare un campo di tipo o cercare un testo specifico.

Nel complesso, è un trucco ottenere il sistema di tipi che ti consenta di utilizzare un set di tipi più ristretto di quello che potresti se tu usassi una classe base. Voglio dire, potresti definire tutte le variabili e gli argomenti come aventi il ​​tipo Object, ma ciò renderebbe il tuo lavoro più difficile, no?


4
Perdonate la mia novità, ma sono curioso perché non sarebbe un approccio migliore per rendere OrderDateInfo AVERE un oggetto NamedEntity come proprietà?
Adam B,

9
@AdamB Questa è una scelta di design valida. Se è meglio o no dipende da cosa verrà utilizzato, tra le altre cose. Nel caso dell'eccezione, non riesco a creare una nuova classe con una proprietà dell'eccezione perché il linguaggio (C # per me) non mi consente di lanciarlo. Potrebbero esserci altre considerazioni di progettazione che precludono l'uso della composizione piuttosto che dell'eredità. Molte volte la composizione è migliore. Dipende solo dal tuo sistema.
Becuzz,

17
@AdamB Per lo stesso motivo che quando si eredita da una classe di base e si aggiungono metodi o proprietà alla base manca, non si crea una nuova classe e si inserisce quella di base come proprietà. C'è una differenza tra un ESSERE umano un mammifero e AVERE un mammifero.
Logarr,

8
@ Logog vero, c'è una differenza tra me che sono un mammifero e avere un mammifero. Ma se rimango fedele al mio mammifero interiore non saprai mai la differenza. Potresti chiederti perché posso dare così tanti diversi tipi di latte per capriccio.
candied_orange,

5
@AdamB Puoi farlo, e questo è noto come composizione per ereditarietà. Ma rinomineresti NamedEntityper questo, ad esempio EntityName, in modo che la composizione abbia senso quando viene descritta.
Sebastian Redl il

28

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

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


1
Hmm, buone eccezioni riguardo al punto. Stavo per dire che era una cosa orribile da fare. Ma altrimenti mi hai persuaso con un eccellente caso d'uso.
David Arno,

Yo-Yo Ho e una bottiglia di rum. È raro che l'orlop sia yar. Spesso perde per mancanza di quercia giusta tra le assi. A Davey Jones l'armadietto con i gorgogliatori di terra che cosa lo ha costruito. TRADUZIONE: È raro che una catena di ereditarietà sia così buona che la classe base possa essere ignorata in modo astratto / efficace.
radarbob,

Penso che sia la pena sottolineare che una nuova classe che eredita da un altro non aggiungere una nuova proprietà - il nome della nuova classe. Questo è esattamente ciò che usi quando crei eccezioni con nomi migliori.
Logan Pickup il

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

OrderDateInfoHA A Nameè una relazione più naturale e crea due classi di facile comprensione che non avrebbero provocato la domanda originale.

Qualsiasi metodo accettato NamedEntitycome parametro dovrebbe essere interessato solo alle proprietà Ide Name, pertanto è necessario modificarlo per accettarlo EntityName.

L'unico motivo tecnico che accetterei per il progetto originale è per l'associazione di proprietà, che l'OP ha menzionato. Un quadro di merda non sarebbe in grado di far fronte alla proprietà extra e di legarsi object.Name.Id. Ma se il tuo framework vincolante non riesce a farcela, allora hai qualche debito in più da aggiungere alla lista.

Andrei d'accordo con la risposta di @ Flater, ma con molte interfacce che contengono proprietà finirai per scrivere un sacco di codice del boilerplate, anche con le adorabili proprietà automatiche di C #. Immagina di farlo in Java!


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 visualizzare questa classe come una struttura di dati. In questo senso, ho intenzione di riformulare la tua domanda come

Perché avere una struttura dati comune di alto livello?

Quindi puoi utilizzare il tipo di dati di livello superiore. Ciò consente di sfruttare il sistema dei tipi per garantire una politica attraverso grandi insiemi di diverse strutture di dati garantendo che le proprietà siano tutte presenti.

Perché avere una struttura di dati che include quella di livello superiore, ma non aggiunge nulla?

Quindi è possibile utilizzare 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 sopra, troviamo conveniente specificare che una persona è nominata, in modo che le persone possano ottenere e modificare il loro nome. Mentre potrebbe essere ragionevole aggiungere proprietà extra alla Personstruttura dei dati, il problema che stiamo risolvendo non richiede una modellazione perfetta 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 elemento Named in un modo che non si interrompe con gli aggiornamenti (come la documentazione) e può essere facilmente esteso a quest'ultima data (se trovi che hai davvero bisogno del agecampo in seguito ).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.