Perché evitare l'ereditarietà Java "Estende"


40

Disse Jame Gosling

"Dovresti evitare l'ereditarietà dell'implementazione quando possibile."

e invece usa l'ereditarietà dell'interfaccia.

Ma perché? Come possiamo evitare di ereditare la struttura di un oggetto usando la parola chiave "extends" e allo stesso tempo rendere il nostro codice orientato agli oggetti?

Qualcuno potrebbe fornire un esempio orientato agli oggetti che illustra questo concetto in uno scenario come "ordinare un libro in una libreria?"



Il problema è che puoi estendere solo una classe.

Risposte:


38

Gosling non ha detto di evitare l'uso della parola chiave extends ... ha solo detto di non usare l'ereditarietà come mezzo per ottenere il riutilizzo del codice.

L'ereditarietà non è di per sé negativa ed è uno strumento (essenziale) molto potente da utilizzare nella creazione di strutture OO. Tuttavia, quando non viene utilizzato correttamente (ovvero quando viene utilizzato per qualcos'altro rispetto alla creazione di strutture di oggetti), crea un codice strettamente accoppiato e molto difficile da mantenere.

Poiché l'ereditarietà dovrebbe essere utilizzata per la creazione di strutture polimorfiche, può essere eseguita altrettanto facilmente con le interfacce, quindi la dichiarazione di utilizzare interfacce piuttosto che classi durante la progettazione delle strutture degli oggetti. Se ti ritrovi a utilizzare extends come mezzo per evitare di incollare il codice da un oggetto a un altro, forse lo stai usando in modo sbagliato e sarebbe meglio servirlo con una classe specializzata per gestire questa funzionalità e condividere quel codice attraverso la composizione.

Leggi il principio di sostituzione di Liskov e capiscilo. Ogni volta che stai per estendere una classe (o un'interfaccia per quella materia), chiediti se stai violando il principio, se sì, allora molto probabilmente (ab) usi l'eredità in modi innaturali. È facile da fare e spesso sembra la cosa giusta, ma renderà il tuo codice più difficile da mantenere, testare ed evolvere.

--- Modifica Questa risposta è una valida argomentazione per rispondere alla domanda in termini più generali.


3
L'ereditarietà (implementazione) non è essenziale per la creazione di strutture OO. Puoi avere una lingua OO senza di essa.
Andres F.

Potresti fornire un esempio? Non ho mai visto OO senza eredità.
Newtopian,

1
Il problema è che non esiste una definizione accettata di cosa sia OOP. (A parte, anche Alan Kay è venuto a rimpiangere l'interpretazione comune della sua definizione, con la sua attuale enfasi sugli "oggetti" anziché sui "messaggi"). Ecco un tentativo di risposta alla tua domanda . Sono d'accordo che è raro vedere OOP senza eredità; quindi la mia argomentazione era semplicemente che non era essenziale ;)
Andres F.

9

All'inizio della programmazione orientata agli oggetti, l'ereditarietà dell'implementazione era il fulcro del riutilizzo del codice. Se in una classe c'era una funzionalità di cui avevi bisogno, la cosa da fare era ereditare pubblicamente da quella classe (erano i giorni in cui il C ++ era più diffuso di Java) e quella funzionalità era "gratuita".

Tranne che non era davvero gratuito. Il risultato furono gerarchie ereditarie estremamente contorte che erano quasi impossibili da capire, inclusi alberi ereditari a forma di diamante che erano difficili da gestire. Questo è uno dei motivi per cui i progettisti di Java hanno deciso di non supportare l'ereditarietà multipla delle classi.

Il consiglio di Gosling (e altre dichiarazioni) come fosse una medicina forte per una malattia abbastanza grave, ed è possibile che alcuni bambini siano stati buttati fuori con l'acqua del bagno. Non c'è niente di sbagliato nell'usare l'ereditarietà per modellare una relazione tra classi che sia veramente is-a. Ma se stai solo cercando di utilizzare una funzionalità di un'altra classe, ti conviene esaminare prima altre opzioni.


6

L'ereditarietà ha recentemente acquisito una reputazione piuttosto spaventosa. Un motivo per evitarlo è il principio progettuale di "composizione sull'eredità", che sostanzialmente incoraggia le persone a non usare l'ereditarietà laddove questa non è la vera relazione, solo per salvare un po 'di scrittura del codice. Questo tipo di eredità può causare alcuni bug difficili da trovare e difficili da correggere. Il motivo per cui il tuo amico stava probabilmente suggerendo di utilizzare un'interfaccia è perché le interfacce forniscono una soluzione più flessibile e gestibile per le API distribuite. Consentono più spesso di apportare modifiche a un api senza rompere il codice dell'utente, dove esporre le classi spesso interrompe il codice dell'utente. Il miglior esempio di ciò in .Net è la differenza di utilizzo tra List e IList.


5

Perché puoi estendere solo una classe, mentre puoi implementare molte interfacce.

Una volta ho sentito che l'eredità definisce ciò che sei, ma le interfacce definiscono un ruolo che puoi svolgere.

Le interfacce consentono anche un migliore utilizzo del polimorfismo.

Potrebbe essere parte del motivo.


perché il downvote?
Mahmoud Hossam,

6
La risposta mette il carro davanti al cavallo. Java consente solo di estendere una classe a causa del principio articolato Gosling (designer di Java). È come dire che i bambini dovrebbero indossare caschi mentre vanno in bicicletta perché questa è la legge. È vero, ma sia la legge che i consigli derivano dall'evitare lesioni alla testa.
JohnMcG,

@JohnMcG Non sto spiegando la scelta del design fatta da Gosling, sto semplicemente dando consigli a un programmatore, i programmatori di solito non si preoccupano del design del linguaggio.
Mahmoud Hossam,

1

Mi è stato insegnato anche questo e preferisco le interfacce laddove possibile (ovviamente uso ancora l'eredità laddove ha senso).

Una cosa che penso faccia è disaccoppiare il codice da implementazioni specifiche. Supponiamo che io abbia una classe chiamata ConsoleWriter e che viene passata a un metodo per scrivere qualcosa e lo scrive sulla console. Ora diciamo che voglio passare alla stampa in una finestra della GUI. Bene, ora devo modificare il metodo o scriverne uno nuovo che prenda GUIWriter come parametro. Se avessi iniziato definendo un'interfaccia IWriter e avessi preso il metodo in un IWriter, avrei potuto iniziare con ConsoleWriter (che implementerebbe l'interfaccia IWriter) e poi scrivere una nuova classe chiamata GUIWriter (che implementa anche l'interfaccia IWriter) e poi I dovrebbe solo cambiare la classe che viene passata.

Un'altra cosa (che è vero per C #, non sono sicuro di Java) è che puoi estendere solo 1 classe ma implementare molte interfacce. Diciamo che ho tenuto un corso chiamato Teacher, MathTeacher e HistoryTeacher. Ora MathTeacher e HistoryTeacher si estendono dall'Insegnante, ma cosa succede se vogliamo una classe che rappresenti qualcuno che è sia un insegnante di matematica che un insegnante di storia. Può diventare piuttosto disordinato quando si tenta di ereditare da più classi quando è possibile farlo solo uno alla volta (ci sono modi ma non sono esattamente ottimali). Con le interfacce puoi avere 2 interfacce chiamate IMathTeacher e IHistoryTeacher e quindi avere una classe che si estende da Teacher e implementa quelle 2 interfacce.

Uno svantaggio che con l'uso delle interfacce è che a volte vedo persone duplicare il codice (dal momento che devi creare l'implementazione per ogni classe per implementare l'interfaccia), tuttavia esiste una soluzione pulita a questo problema, ad esempio l'uso di cose come i delegati (non sono sicuro qual è l'equivalente Java di quello).

Pensa che il motivo principale per utilizzare le interfacce sulle eredità sia il disaccoppiamento del codice di implementazione, ma non pensare che l'ereditarietà sia malvagia in quanto è ancora molto utile.


2
"puoi estendere solo 1 classe ma implementare molte interfacce" Questo è vero sia per Java che per C #.
FrustratedWithFormsDesigner,

Una possibile soluzione al problema della duplicazione del codice è quella di creare una classe astratta che implementa l'interfaccia e la estenda. Usare con cura, però; ci sono solo alcuni casi che ho visto che ha senso.
Michael K,

0

Se erediti da una classe, prendi una dipendenza non solo da quella classe, ma devi anche onorare qualunque contratto implicito sia stato creato dai clienti di quella classe. Lo zio Bob fornisce un grande esempio di come è facile sottilmente violare il principio di sostituzione Liskov utilizzando la base Piazza si estende rettangolo dilemma. Le interfacce, poiché non hanno implementazioni, non hanno contratti nascosti.


2
Un pignolo: il problema dell'ellisse circolare continuerà a verificarsi anche se vengono utilizzate solo interfacce pure, se gli oggetti devono essere mutabili.
rwong
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.