Odore di codice: Abuso di ereditarietà [duplicato]

49

È stato generalmente accettato nella comunità OO che si dovrebbe "favorire la composizione sull'ereditarietà". D'altra parte, l'ereditarietà fornisce sia il polimorfismo sia un modo diretto e teso di delegare tutto a una classe di base, a meno che non sia esplicitamente sovrascritto ed è quindi estremamente conveniente e utile. La delega può spesso (sebbene non sempre) essere prolissa e prolissa.

Il segno più evidente e più evidente dell'abuso di ereditarietà è la violazione del principio di sostituzione di Liskov . Quali sono altri segni che l'ereditarietà sia lo strumento sbagliato per il lavoro, anche se sembra conveniente?

    
posta dsimcha 16.10.2010 - 19:27
fonte

9 risposte

42

Quando si eredita solo per ottenere funzionalità, si sta probabilmente abusando dell'eredità. Questo porta alla creazione di un God Object .

L'ereditarietà di per sé non è un problema fintanto che vedi una relazione reale tra le classi (come gli esempi classici, come Dog extends Animal) e non stai mettendo metodi sulla classe genitore che non ha senso alcuni dei suoi figli (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 inserendo il fornitore di funzionalità nel costruttore?). Se una classe ha bisogno di ESSERE come gli altri, usa l'ereditarietà.

    
risposta data 16.10.2010 - 20:19
fonte
36

Direi che, correndo il rischio di essere abbattuto, quell'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 il riutilizzo del codice)

Avere un unico meccanismo per ottenere entrambi è ciò che porta in primo luogo "l'abuso di ereditarietà", poiché la maggior parte delle persone si aspetta che l'ereditarietà riguardi l'interfaccia, ma potrebbe essere utilizzata per ottenere l'implementazione di default anche da programmatori altrimenti accurati (è solo così facile da trascurare questo ...)

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

Ritorno in pista:

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

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

Quindi il Principio di Liskov di per sé non è abbastanza, quello che devi sapere è se il polimorfismo è usato . Se non lo è, allora non c'era molto da fare ereditando in primo luogo, è un abuso.

Sommario:

  • applica il Liskov Principle
  • verifica che sia effettivamente utilizzato

Un modo per aggirarlo:

Imporre una netta separazione delle preoccupazioni:

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

significa che almeno un paio di sequenze di tasti sono necessarie per ogni metodo, e improvvisamente le persone cominciano a pensare se sia una così grande idea riutilizzare questa classe "grassa" solo per una piccola porzione di esso.

    
risposta data 16.10.2010 - 20:24
fonte
13

È davvero "generalmente accettato"? È un'idea che ho sentito poche volte, ma non è un principio universalmente riconosciuto. Come hai appena sottolineato, l'ereditarietà e il polimorfismo sono i tratti distintivi di OOP. Senza di loro, hai appena ottenuto una programmazione procedurale con sintassi goofy.

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

    
risposta data 16.10.2010 - 19:47
fonte
5

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

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

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

In generale, è preferibile creare classi tramite composizione e quindi convertire in ereditarietà quelle classi con la maggior parte delle loro proprietà in comune, lasciando le differenze come composizioni.

IOW, YAGNI (Non hai bisogno di ereditarietà).

    
risposta data 16.10.2010 - 20:21
fonte
3

"Favorisci la composizione sull'ereditarietà" è la più insignificante delle affermazioni. Sono entrambi elementi essenziali di OOP. È come dire "Prediligi i martelli sulle seghe".

Naturalmente è possibile abusare dell'eredità, si può abusare di qualsiasi caratteristica linguistica, si possono abusare affermazioni condizionali.

    
risposta data 19.03.2011 - 04:47
fonte
1

Forse la cosa più ovvia è quando la classe ereditaria finisce con una pila di metodi / proprietà che non vengono mai usati nell'interfaccia esposta. Nei casi peggiori, in cui la classe base ha membri astratti, troverai implementazioni vuote (o prive di significato) 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 spremute in un'unica implementazione - nonostante queste due cose non siano correlate. Questo tende ad aumentare l'accoppiamento del codice ea districarli quando si scopre che la somiglianza era solo una coincidenza può essere piuttosto dolorosa a meno che non venga individuata in anticipo.

Aggiornato con un paio di esempi: sebbene non sia direttamente collegato alla programmazione in quanto tale, ritengo che l'esempio migliore per questo genere di cose sia la localizzazione / internazionalizzazione. In inglese c'è una parola per "sì". In altre lingue possono essere molte, molte di più per concordare grammaticalmente con la domanda. Qualcuno potrebbe dire, ah, abbiamo una stringa per "sì" e va bene per molte lingue. Poi 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ì" sempre è una coincidenza e il tempo risparmiato solo avere un "sì" è completamente perso nel districare il pasticcio che ne deriva.

Ho visto un esempio particolarmente sgradevole di questo nella nostra base di codice in cui ciò che era in realtà un genitore - > la relazione figlio in cui padre e figlio avevano proprietà con nomi simili era modellata con ereditarietà. Nessuno ha notato questo perché tutto sembrava funzionare, quindi il comportamento del bambino (in realtà base) e genitore (sottoclasse) doveva andare in direzioni diverse. Per fortuna, ciò accadde esattamente nel momento sbagliato in cui una scadenza stava incombendo - nel tentativo di modificare il modello qua e là la complessità (sia in termini di logica che di duplicazione del codice) passò immediatamente sul tetto. Era anche molto difficile ragionare su / lavorare con il codice semplicemente non si riferiva a come 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 simili proprietà sonore con valori che erano uguali rappresentavano lo stesso concetto.

    
risposta data 16.10.2010 - 19:47
fonte
1

Se stai sottoclassi di A in class B ma a nessuno importa se B eredita da A o meno, quindi è il segno che potresti sbagliare.

    
risposta data 17.10.2010 - 18:20
fonte
0

Ci sono situazioni in cui l'ereditarietà dovrebbe essere favorita rispetto alla composizione, e la distinzione è molto più chiara di una questione di stile. Sto parafrasando da Sutter e Alexandrescu gli C ++ Coding Standards qui mentre la mia copia è sul mio scaffale al lavoro al momento. Tra l'altro, la lettura altamente consigliata.

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

Detto questo, l'ereditarietà è incoraggiata quando si desidera che la classe derivata venga riutilizzata in contesti in cui viene attualmente utilizzata la classe base, senza dover modificare il codice in quel contesto. Ad esempio, se hai un codice che sta iniziando ad assomigliare a if (type1) type1.foo(); else if (type2) type2.foo(); , probabilmente hai un buon motivo per esaminare l'ereditarietà.

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à.

    
risposta data 19.03.2011 - 05:31
fonte
0

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

Questa lotta è: quale sarà la quantità minima di codice, per la massima espandibilità nei miglioramenti futuri?

Ecco perché la domanda è così difficile da rispondere. In una lingua potrebbe essere che l'ereditarietà sia più rapida della composizione che in qualche altra lingua, o viceversa.

Oppure potrebbe essere che il problema specifico che stai risolvendo nel codice tragga beneficio più dalla convenienza che da una visione a lungo termine della crescita. Questo è tanto un problema di business quanto di programmazione.

Quindi, per tornare alla domanda, l'ereditarietà potrebbe essere sbagliata se ti sta mettendo in una giacca ad un certo punto nel futuro - o se sta creando un codice che non ha bisogno di essere lì (classi che ereditano dagli altri per no ragione, o peggio ancora classi base che iniziano a fare molti test condizionali per i bambini).

    
risposta data 10.12.2012 - 07:57
fonte