it-swarm.it

Odore di codice: abuso di ereditarietà

È stato generalmente accettato nella comunità OO che si dovrebbe "favorire la composizione rispetto all'eredità". D'altra parte, l'ereditarietà fornisce sia il polimorfismo sia un modo semplice e conciso di delegare tutto a una classe base se non esplicitamente ignorato ed è quindi estremamente conveniente e utile. La delega può spesso (sebbene non sempre) essere verbosa e fragile.

Il segno più evidente e più sicuro di IMHO di abuso ereditario è la violazione del Principio di sostituzione di Liskov . Quali sono alcuni altri segni che l'ereditarietà è lo strumento sbagliato per il lavoro anche se sembra conveniente?

52
dsimcha

Quando erediti solo per ottenere funzionalità, stai probabilmente abusando dell'ereditarietà. Questo porta alla creazione di un God Object .

L'ereditarietà in sé non è un problema fintanto che vedi una vera relazione tra le classi (come gli esempi classici, come Dog extends Animal) e non stai inserendo metodi nella classe genitore che non ha senso su alcuni di essi bambini (ad esempio, l'aggiunta di un metodo Bark () nella classe Animal non avrebbe alcun senso in una classe Fish che estende Animal).

Se una classe ha bisogno di funzionalità, usa la composizione (forse iniettando il provider di funzionalità nel costruttore?). Se una classe ha bisogno di ESSERE come le altre, usa l'ereditarietà.

48
Fernando

Direi, correndo il rischio di essere abbattuto, che l'ereditarietà è un odore di codice in sé :)

Il problema con l'ereditarietà è che può essere utilizzato per due scopi ortogonali:

  • interfaccia (per polimorfismo)
  • implementazione (per riutilizzo del codice)

Avere un singolo meccanismo per ottenere entrambi è ciò che porta in primo luogo all'abuso dell'ereditarietà, poiché la maggior parte delle persone si aspetta che l'ereditarietà riguardi l'interfaccia, ma potrebbe essere utilizzata per ottenere l'implementazione predefinita anche da programmatori altrimenti attenti (è così facile trascuri questo ...)

In effetti, i linguaggi moderni come Haskell o Go hanno abbandonato l'eredità per separare entrambe le preoccupazioni.

Ritorno in pista:

Una violazione del principio di Liskov è quindi il segno più sicuro, poiché significa che la parte "interfaccia" del contratto non è rispettata.

Tuttavia, anche quando l'interfaccia viene rispettata, potresti avere oggetti che ereditano da classi di base "fat" solo perché uno dei metodi è stato ritenuto utile.

Pertanto il Principio di Liskov in sé non è sufficiente, ciò che devi sapere è se il polimorfismo è sato. Se non lo è, allora non ha molto senso ereditare in primo luogo, è un abuso.

Sommario:

  • imporre il Liskov Principle
  • controlla che sia effettivamente utilizzato

Un modo per aggirarlo:

Imporre una chiara separazione delle preoccupazioni:

  • eredita solo dalle interfacce
  • usa la composizione per delegare l'implementazione

significa che sono necessari almeno un paio di battiture per ciascun metodo, e all'improvviso le persone iniziano a pensare se è una grande idea riutilizzare questa classe "grassa" solo per una piccola parte di essa.

39
Matthieu M.

È davvero "generalmente accettato"? È un'idea che ho sentito alcune volte, ma difficilmente è un principio universalmente riconosciuto. Come hai appena sottolineato, eredità e polimorfismo sono i tratti distintivi di OOP. Senza di essi, hai appena una programmazione procedurale con sintassi sciocca.

La composizione è uno strumento per costruire oggetti in un certo modo. L'ereditarietà è uno strumento per costruire oggetti in modo diverso. Non c'è niente di sbagliato in nessuno dei due; devi solo sapere come funzionano entrambi e quando è appropriato usare ogni stile.

14
Mason Wheeler

La forza dell'eredità è la riusabilità. Di interfaccia, dati, comportamento, collaborazioni. È un modello utile da cui trasmettere attributi a nuove classi.

Lo svantaggio dell'utilizzo dell'ereditarietà è la riusabilità. L'interfaccia, i dati e il comportamento sono incapsulati e trasmessi in massa, rendendola una proposta del tutto o niente. I problemi diventano particolarmente evidenti man mano che le gerarchie diventano più profonde, mentre le proprietà che potrebbero essere utili in astratto diminuiscono e iniziano a oscurare le proprietà a livello locale.

Ogni classe che utilizza l'oggetto composizione dovrà più o meno ri-implementare lo stesso codice per interagire con esso. Mentre questa duplicazione richiede una certa violazione di DRY, offre ai progettisti una maggiore libertà di scegliere le proprietà di una classe più significative per il loro dominio. Ciò è particolarmente vantaggioso quando le classi diventano più specializzate.

In generale, dovrebbe essere preferito creare classi tramite la composizione e quindi convertire in eredità quelle classi con la maggior parte delle loro proprietà in comune, lasciando le differenze come composizioni.

IOW, YAGNI (non avrai bisogno di ereditarietà).

5
Huperniketes

Se stai eseguendo la sottoclasse di A in classe B ma a nessuno importa se B eredita da A o no, quindi questo è il segno che potresti aver sbagliato .

3
tia

"Favorire la composizione sull'eredità" è la più sciocca delle affermazioni. Sono entrambi elementi essenziali di OOP. È come dire "Favor martelli sulle seghe".

Naturalmente l'ereditarietà può essere abusata, qualsiasi caratteristica del linguaggio può essere abusata, le dichiarazioni condizionali possono essere abusate.

2
Chuck Stephanski

Forse il più ovvio è quando la classe ereditaria finisce con un mucchio di metodi/proprietà che non sono mai usati nell'interfaccia esposta. Nei casi peggiori, dove la classe base ha membri astratti, troverai implementazioni vuote (o insignificanti) dei membri astratti solo per "riempire gli spazi vuoti" per così dire.

Più sottilmente, a volte vedi dove, nel tentativo di aumentare il riutilizzo del codice, due cose che hanno implementazioni simili sono state compresse in un'unica implementazione - nonostante quelle due cose non siano correlate. Questo tende ad aumentare l'accoppiamento del codice e districarli quando si scopre che la somiglianza era solo una coincidenza può essere abbastanza dolorosa a meno che non venga individuata in anticipo.

Aggiornato con un paio di esempi: sebbene non abbia direttamente a che fare con la programmazione in quanto tale, penso che l'esempio migliore per questo genere di cose sia con la localizzazione/internazionalizzazione. In inglese c'è una parola per "sì". In altre lingue possono essere molte, molte altre per concordare grammaticalmente con la domanda. Qualcuno potrebbe dire, ah, abbiamo una stringa per "sì" e va bene per molte lingue. Quindi si rendono conto che la stringa "sì" in realtà non corrisponde a "sì", ma in realtà il concetto di "risposta affermativa a questa domanda" - il fatto che sia "sì" tutto il tempo è una coincidenza e il tempo risparmiato solo avere un "sì" è completamente perso nel districare il disordine risultante.

Ho visto un esempio particolarmente brutto di questo nella nostra base di codice in cui quella che era in realtà una relazione padre - figlio in cui padre e figlio avevano proprietà con nomi simili era modellata con eredità. Nessuno lo notò poiché tutto sembrava funzionare, quindi il comportamento del bambino (in realtà base) e del genitore (sottoclasse) doveva andare in direzioni diverse. Per fortuna, questo è accaduto esattamente nel momento sbagliato in cui si profilava una scadenza - nel tentativo di modificare il modello qua e là la complessità (sia in termini di logica che di duplicazione del codice) è passata immediatamente attraverso il tetto. Era anche molto difficile ragionare/lavorare con il fatto che il codice semplicemente non si riferiva al modo in cui il business pensava o parlava dei concetti rappresentati da queste classi. Alla fine abbiamo dovuto mordere il proiettile e fare alcuni importanti refactoring che avrebbero potuto essere completamente evitati se il ragazzo originale non avesse deciso che un paio di proprietà dal suono simile con valori che erano identici rappresentavano lo stesso concetto.

0
FinnNk

Penso che battaglie come eredità e composizione siano in realtà solo aspetti di una battaglia più profonda.

Quella lotta è: cosa comporterà la minima quantità di codice, per la massima espandibilità nei miglioramenti futuri?

Ecco perché alla domanda è così difficile rispondere. In una lingua potrebbe essere che l'ereditarietà venga messa in atto più rapidamente della composizione rispetto ad altre lingue, o viceversa.

Oppure potrebbe essere il problema specifico che stai risolvendo nel codice beneficia più della convenienza che di una visione a lungo termine della crescita. È un problema di business tanto quanto di programmazione.

Quindi, per tornare alla domanda, l'eredità potrebbe essere sbagliata se ti metterà in una giacca dritta ad un certo punto in futuro - o se sta creando codice che non ha bisogno di essere lì (classi che ereditano da altri senza motivo, o peggio ancora classi di base che iniziano a dover fare molti test condizionali per i bambini).

Ci sono situazioni in cui l'ereditarietà dovrebbe essere privilegiata rispetto alla composizione e la distinzione è molto più chiara rispetto a una questione di stile. Sto parafrasando da Sutter e Alexandrescu's C++ Coding Standards qui come la mia copia è sulla mia libreria al lavoro al momento. A proposito, lettura altamente raccomandata.

Prima di tutto, l'ereditarietà è scoraggiata quando non è necessaria perché crea una relazione molto intima e strettamente accoppiata tra classi di base e classi derivate che può causare mal di testa lungo la strada per la manutenzione. Non è solo l'opinione di un ragazzo che la sintassi dell'eredità rende più difficile la lettura o qualcosa del genere.

Detto questo, l'ereditarietà è incoraggiata quando si desidera che la classe derivata stessa venga riutilizzata in contesti in cui la classe base è attualmente in uso, senza dover modificare il codice in quel contesto. Ad esempio, se si dispone di codice che inizia a somigliare a if (type1) type1.foo(); else if (type2) type2.foo();, è probabile che si tratti di un ottimo caso per esaminare l'eredità.

In breve, se vuoi solo riutilizzare i metodi della classe base, usa la composizione. Se si desidera utilizzare una classe derivata nel codice che attualmente utilizza una classe base, utilizzare l'ereditarietà.

0
Karl Bielefeldt