Ereditarietà usando una classe base non astratta

6

Questo post è basato sulla domanda link .

Alcuni hanno detto - controlla se esiste una relazione "è-a". Se è lì usa l'ereditarietà. Quello che penso è che ci dovrebbe essere un secondo controllo per l'utilizzo dell'ereditarietà. Usa l'ereditarietà solo se la classe base è astratta. Se la classe base deve essere istanziata, utilizzare la composizione; non ereditarietà.

E.g 1. Accountant è un dipendente. Ma non userò l'ereditarietà perché un oggetto Employee può essere istanziato. (Quando parli con gli uomini d'affari, loro ti diranno che - Accountant è un dipendente, ma in OO world Accountant non è (non dovrebbe essere) un dipendente)

E.g 2. Book is a SellingItem. Un oggetto SellingItem non può essere istanziato - è un concetto astratto. Quindi userò l'ereditarietà. Il SellingItem è una classe base astratta (o interfaccia in C #)

Quello che sto cercando qui è un esempio che metterà alla prova la mia argomentazione -che è uno scenario in cui l'ereditarietà è migliore della composizione anche se la classe base non è astratta (la classe base può essere istanziata). Potete fornire un esempio del genere?

Nota: sarebbe più utile se è possibile fornire uno scenario basato su dominio Bank, dominio delle risorse umane, dominio Retail o qualsiasi altro dominio popolare.

Nota: nel design, si supponga di avere il pieno controllo del codice. Questo è che non stiamo usando alcuna API esterna. Inoltre stiamo iniziando dall'inizio.

Contrassegnerò come risposta se c'è un esempio che spiega uno scenario che dimostra che la mia argomentazione è sbagliata. Non è una semplice discussione sull'uso di Composizione o Ereditarietà.

Supporto @anon answer in link

The main reason for using inheritance is not as a form of composition - it is so you can get polymorphic behaviour. If you don't need polymorphism, you probably should not be using inheritance.

@MatthieuM. dice in Odore di codice: abuso di ereditarietà

The issue with inheritance is that it can be used for two orthogonal purposes:

interface (for polymorphism)

implementation (for code reuse)

RIFERIMENTO

  1. Esempio di impiegato - Accountant link
  2. Esempio di articolo di vendita - link
  3. Programmazione dei principi SOLID
  4. Odore di codice: abuso di eredità
posta Lijo 02.08.2012 - 11:05
fonte

7 risposte

2

Sebbene tu abbia menzionato in un commento non si parla di "composizione vs eredità", devo tornare ad esso. Perché la composizione è migliore dell'ereditarietà Se assumiamo che il disaccoppiamento sia sempre una buona cosa ( Preferisci la composizione all'ereditarietà? ).

In realtà non è sempre il caso. Anche se vedi che "è un" può essere sostituito con "ha un" o "ha un" sembra più promettente, "è un" ha delle proprietà automatiche molto belle.

Considera qualsiasi gerarchia di widget. Di solito tutto ciò che riguarda la dimensione è sepolto in profondità alla radice. E non importa per quanto tempo il percorso di ereditarietà, puoi sempre chiamare direttamente SetBounds dell'istanza e passare l'istanza a una procedura che si aspetta l'oggetto di base, il compilatore lo accetterà perché da "è una" viene la conversione e l'accesso di tipo automatico .

Ma se si converte la gerarchia dell'ereditarietà in gerarchia di composizione (tecnicamente è sempre possibile), si perderà la convenienza in entrambi i casi, perché si dovrebbe indicare il pezzo concreto dell'albero della composizione per cambiare la dimensione o fornire una procedura con particolare tipo di classe. Confronta

 MyForm.SetBounds(...  // easy because of an "is a"-tree
 MyForm.ScrollingWinControl.WinControl.Control.SetBounds  //complex because of a "has a"-tree 

o

 GetSomeInfoOfControl(MyForm) // easy ...
 GetSomeInfoOfControl(MyForm.ScrollingWinControl.WinControl.Control) //complex ...

Quindi, nonostante il prezzo di un accoppiamento più strong, l'ereditarietà a volte è molto facile da gestire e astratta.

    
risposta data 03.08.2012 - 08:38
fonte
7

L'intero 'è un' vs 'ha un' discussione non ha mai avuto senso per me. Esiste già una buona definizione su quando utilizzare l'ereditarietà, il cosiddetto "principio di sostituzione di Liskov", che afferma:

Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

La prima cosa che noterai è che in genere è una cattiva idea lasciare che le persone con un dottorato di ricerca spieghino un semplice problema.

In secondo luogo, questo indica esattamente quando utilizzare l'ereditarietà. Se posso usare articolo di Wikipedia e applicarlo all'esempio del tuo commercialista / dipendente:

Liskov's notion of a behavioral subtype defines a notion of substitutability for mutable objects; that is, if Accountant is a subtype of Employee, then objects of type Employee in a program may be replaced with objects of type Accountant without altering any of the desirable properties of that program (e.g., correctness).

L'intero paragrafo Principi di Wikipedia contiene alcuni principi più interessanti.

Apparentemente la maggior parte delle risposte si riferisce a LSP in un modo o nell'altro. Baqueta l'ha astutamente camuffato come un link facile da smascherare e Geek ha modificato il suo post per includere LSP secondi prima di pubblicare la mia risposta. Quindi, LSP sembra essere il modo più ragionevole per creare la tua gerarchia.

Alcuni esempi di dove il tuo principio non regge:

In WPF, il concreto Sistema. Windows.Controls.Button ha le seguenti sottoclassi:

In Java e C #, tutto si estende direttamente o indirettamente da object , che non è astratto.

Anche in WPF: DatePickerTextBox è una sottoclasse di (concreto) TextBox. Ci sono molti altri esempi. Questi rispetti LSP e la classe base non sono astratti.

Come nota aggiuntiva. Penso che la ragione per cui hai trovato questo principio sia perché è generalmente una buona idea mantenere la tua gerarchia molto piatta, perché l'ereditarietà introduce l'accoppiamento. Uno o due livelli in profondità è più che sufficiente. Le classi astratte saranno (quasi) sempre sottoclasse da qualche parte, poiché questo è lo scopo di fare un abstract di classe, in modo che altri possano renderlo specializzato. La maggior parte delle classi concrete non ha bisogno di essere specializzata in alcun modo. Questo potrebbe essere il motivo per cui non hai ancora trovato un esempio.

    
risposta data 02.08.2012 - 13:29
fonte
1

Penso che in realtà ci sia una buona regola per scegliere di fare un abstract di classe: se ci sono (o si può prevedere la necessità di) varianti di una classe con un comportamento leggermente diverso, allora è spesso un buona idea per creare una classe base astratta.

In conclusione, secondo la mia esperienza: finché la relazione con cui si ha a che fare è veramente is-a (ad es. l'esempio Accountant-Employee è più adatto alla composizione che all'eredità, come questa risposta ) quindi è molto raramente dannoso / problematico ereditare da classi non astratte.

Un paio di esempi quando potresti volere / dover ignorare la tua regola. Entrambi supponiamo che tu abbia bisogno di Poodle per essere sostituibile per Dog :

  1. Dog è una classe in un'API esterna e non esiste un'interfaccia IDog per Poodle da implementare.
  2. Dog è già istanziato in dozzine di luoghi diversi nel tuo codice (già testato)

Certamente, questi sono entrambi i casi in cui un design originale migliore potrebbe aver aiutato, ma nel mondo reale i design non sono mai perfetti!

    
risposta data 02.08.2012 - 11:46
fonte
1

Penso che ereditare la classe concreta sia OK e in realtà una caratteristica abbastanza preziosa.

La regola "is-a" è valida solo per modelli e domini estremamente semplici. Il modo in cui lo vedo, è che le classi rappresentano il comportamento e incapsulano lo stato in cui lo stato può essere un'altra classe. Che tipo di comportamento Accountant, Employee, Book and Selling hanno in relazione al nostro modello? Questa è la domanda che dovresti fare. E in base a tale uso l'ereditarietà della composizione.

Inoltre, molti oggetti del mondo reale non sono dei buoni esempi, perché sono solitamente compositi di comportamenti diversi, che possono essere condivisi tra diversi oggetti del mondo reale. Molte specie animali camminano su tutto 4. Questo è un buon esempio di comportamento.

Estendere il comportamento già esistente per ereditarietà è un ottimo modo per semplificare e modulare il modello del dominio. E per il nostro esempio di comportamento di camminata, possiamo estendere la classe WoundedWalk, che fa esattamente quello che dice il suo nome.

    
risposta data 02.08.2012 - 11:50
fonte
1

Non penso ci sia alcuna necessità di un esempio impegnativo. Quello che dici è un buon principio di progettazione che si chiama inversione di dipendenza . Cioè, i componenti più alti non dovrebbero mai dipendere da componenti inferiori. I componenti più bassi dovrebbero dipendere solo da componenti più alti.

Come ogni regola, anche questa potrebbe avere delle eccezioni. Ma non ci sono regole senza eccezioni!

    
risposta data 02.08.2012 - 11:16
fonte
1

Usi l'ereditarietà sulla composizione quando hai bisogno del riutilizzo del codice. L'ereditarietà aiuta a evolvere velocemente le classi. Tuttavia, si dovrebbe fare attenzione a non esagerare. usalo giudiziosamente solo quando ha senso (cioè quando sei assolutamente sicuro che esista una relazione "IS-A" tra tutti gli oggetti di entrambe le classi. Devi assicurarti che gli oggetti sottoclassi rispettino il Principio di sostituzione di Liskov . Anche allora potrebbe non essere una buona idea ogni volta che aggiungi un componente di valore a una sottoclasse e la super classe ha anche la nozione di componente del valore. Da Java efficace:

There is no way to extend an instantiable class and add a value component while preserving the equals contract, unless you are willing to forgo the benefits of object-oriented abstraction.

D'altro canto la composizione ha molti vantaggi rispetto all'ereditarietà e quindi la maggior parte delle volte è preferibile all'ereditarietà, ma quella non era la tua domanda. Il principio di iniezione * di di Dipendenza * di Spring si basa principalmente su questa premessa.

    
risposta data 02.08.2012 - 13:17
fonte
1

What I am looking for here is an example that will challenge my argument

Esempi abbondanti:

  • Pulsanti in molti framework GUI, View non è una classe astratta: puoi creare e spesso creare viste vanilla. Ma ci sono anche molte sottoclassi di View, come Button.

  • Oggetti mutevoli. alcuni framework, ad es. Cacao, hanno classi di contenitori di dati che sono immutabili (NSArray, NSString, NSDictionary, ecc.) E anche sottoclassi mutabili di tali (NSMutableArray, NSMutableString, ecc.). È possibile eseguire qualsiasi operazione su un'istanza di NSMutableArray che è possibile eseguire su un'istanza di NSArray e quindi alcuni.

  • Dipendente. Prendendo a prestito dal tuo esempio Accountant, è probabile che una classe Employee possa essere una sottoclasse di Person, ma non c'è ragione per cui Person debba essere astratto.

  • Accountant. L'esempio viene spesso utilizzato per illustrare come la composizione può / deve essere utilizzata al posto dell'ereditarietà, ma ciò non significa che la composizione sia sempre la scelta migliore in questo caso . Sì, è possibile creare un contabile separando gli aspetti "lavoro" dal "dipendente" e quindi configurando un'istanza di Dipendente con un lavoro particolare. Ma ci possono essere buoni motivi per usare l'ereditarietà. Un ragioniere non è solo un impiegato che capita di scricchiolare i numeri; un contabile è un particolare tipo di dipendente, e potresti voler usare l'ereditarietà per riflettere questo. Ad esempio, è possibile che la classe Accountant implementa un'interfaccia ProfessionalStanding o che includa funzionalità che consentono di rilevare i conflitti di interesse. Inoltre, la creazione di una classe Accountant consente di utilizzare il sistema di tipi della lingua per aiutare a impedire che dipendenti che non sono ragionieri vengano utilizzati in ruoli in cui dovrebbe essere utilizzato solo un contabile: Project::addAuditor(Accountant& a);

La decisione di utilizzare o non utilizzare l'ereditarietà ha poco a che fare con il fatto che una classe sia astratta. È il contrario, in realtà: le classi astratte vengono utilizzate per costringerti a creare una sottoclasse, generalmente per garantire che vengano fornite importanti funzionalità mancanti. Il fatto che puoi creare un'istanza di una classe non significa certamente che devi evitare la sottoclasse.

    
risposta data 03.08.2012 - 10:13
fonte