JavaRush /Java Blog /Random-IT /L'ereditarietà come fenomeno
articles
Livello 15

L'ereditarietà come fenomeno

Pubblicato nel gruppo Random-IT
A dire il vero, inizialmente non avevo pianificato questo articolo. Ho considerato banali, nemmeno degne di menzione, le questioni che voglio trattare qui. Tuttavia, mentre scrivevo articoli per questo sito, ho sollevato una discussione sull'ereditarietà multipla in uno dei forum. Di conseguenza, si è scoperto che la maggior parte degli sviluppatori ha una comprensione molto vaga dell'ereditarietà. E, di conseguenza, commette molti errori. Poiché l'ereditarietà è una delle caratteristiche più importanti dell'OOP (se non la più importante!), ho deciso di dedicare un articolo separato a questo fenomeno. * * * Innanzitutto voglio distinguere tra due concetti: oggetto e classe. Questi concetti sono costantemente confusi. Nel frattempo, sono fondamentali per l'OOP. E, secondo me, è necessario conoscere le differenze tra loro. Quindi, l'oggetto. Fondamentalmente, è qualsiasi cosa. Ecco il cubo. In legno, blu. La lunghezza del bordo è di 5 cm Questo è un oggetto. E c'è una piramide. Plastica, rossa. Costola 10 cm. Anche questo è un oggetto. Cosa hanno in comune? Misure differenti. Forma diversa. Materiale diverso. Tuttavia, hanno qualcosa in comune. Innanzitutto sia il cubo che la piramide sono poliedri regolari. Quelli. la somma del numero di vertici e del numero di facce è 2 in più rispetto al numero di spigoli. Ulteriore. Entrambe le forme hanno facce, bordi e vertici. Entrambe le figure hanno una caratteristica come la dimensione delle costole. Entrambe le forme possono essere ruotate. Entrambe le figure possono essere disegnate. Le ultime due proprietà sono già comportamenti. E così via. La pratica della programmazione mostra che è molto più facile operare con oggetti omogenei che con oggetti eterogenei. E poiché c'è ancora qualcosa in comune tra queste figure, c'è il desiderio di evidenziare in qualche modo questa comunanza. È qui che entra in gioco il concetto di classe. Quindi, la definizione.
Una classe è un descrittore delle proprietà comuni di un gruppo di oggetti. Queste proprietà possono essere sia caratteristiche degli oggetti (dimensione, peso, colore, ecc.), sia comportamenti, ruoli, ecc.
Commento. La parola “tutto” (descrittore di tutte le proprietà) non è stata pronunciata. Ciò significa che qualsiasi oggetto può appartenere a diverse classi. L'ereditarietà come fenomeno - 1 Prendiamo lo stesso esempio con forme geometriche come base. La descrizione più generale è poliedro regolare . Indipendentemente dalla dimensione del bordo, dal numero di facce e dai vertici. L'unica cosa che sappiamo è che questa figura ha vertici, bordi e facce e che le lunghezze dei bordi sono uguali. Ulteriore. Possiamo rendere la descrizione più specifica. Diciamo che vogliamo disegnare questo poliedro . Introduciamo un concetto come un poliedro regolare disegnato . Di cosa abbiamo bisogno per disegnare? Descrizione di un metodo di disegno generale che non dipende da coordinate specifiche del vertice. Forse il colore dell'oggetto. Ora introduciamo le classi Cubo e Tetraedro . Gli oggetti appartenenti a queste classi sono certamente poliedri regolari. L'unica differenza è che i numeri di vertici, spigoli e facce sono già rigorosamente fissati per ciascuna delle nuove classi. Inoltre, conoscendo il tipo di una figura specifica, possiamo fornire una descrizione del metodo di disegno. Ciò significa che qualsiasi oggetto delle classi Cubo o Tetraedro è anche un oggetto della classe Poliedro regolare disegnato . C'è una gerarchia di classi. In questa gerarchia si scende dalla descrizione più generale a quella più specifica. Si noti che un oggetto di qualsiasi classe si adatta anche alla descrizione di qualsiasi classe più generale nella gerarchia. Questa relazione di classe è chiamata ereditarietà . Ogni classe figlia eredita tutte le proprietà della classe genitore, più generale, e (possibilmente) ne aggiunge alcune delle proprie a queste proprietà. Oppure sovrascrive alcune proprietà della classe genitore. Qui voglio citare il classico libro di Gradi Bucha sul design orientato agli oggetti:
L'ereditarietà, quindi, definisce una gerarchia "è una" tra classi, in cui la sottoclasse eredita da una o più superclassi. Questa è infatti la cartina di tornasole per l'ereditarietà. Date le classi A e B, se A "non è un" tipo di B, allora A non dovrebbe essere una sottoclasse di B.
Tradotto suona così:
L'ereditarietà definisce quindi una gerarchia "è" tra le classi, in cui una sottoclasse eredita da una o più superclassi. Questo è, infatti, il test determinante (letteralmente, una cartina di tornasole, mia nota) per l'ereditarietà. Se abbiamo le classi A e B, e se la classe A "non è" una variante della classe B, allora A non deve essere una sottoclasse di B.
Chi ha letto fin qui probabilmente si volterà il dito contro la tempia perplesso. Il primo pensiero è che tutto questo è banale! Questo è vero. Ma se sapessi quante gerarchie pazzesche di eredità ho visto! In quella discussione di cui ho parlato all'inizio, uno dei partecipanti ha ereditato seriamente un carro armato da... una mitragliatrice!!! Per il semplice motivo che il carro armato HA una mitragliatrice. E questo è l'errore più comune. L'ereditarietà viene confusa con l'aggregazione, ovvero l'inclusione di un oggetto all'interno di un altro. Il carro armato non è una mitragliatrice, ne contiene una. E a causa di questo errore, molto spesso si desidera utilizzare l'ereditarietà multipla. Passiamo ora direttamente a Java. Cosa c'è in termini di eredità? Esistono due tipi di classi nel linguaggio: quelle in grado di contenere un'implementazione e quelle incapaci di farlo. Queste ultime sono chiamate interfacce, anche se in sostanza sono classi completamente astratte. L'ereditarietà come fenomeno - 2 Pertanto, il linguaggio ti consente di ereditare una classe da un'altra classe che potenzialmente contiene un'implementazione. MA SOLO DA UNO! Lasciatemi spiegare perché è stato fatto ciò. Il punto è che ogni implementazione può occuparsi solo della propria parte, ovvero delle variabili e dei metodi di cui è a conoscenza. E anche se ereditiamo la classe C da A e B , allora il metodo processA , ereditato dalla classe A, può funzionare solo con la variabile interna a , perché non sa nulla di b , così come non sa nulla di c e del metodo processC . Allo stesso modo il metodo processB può funzionare solo con la variabile b. Cioè, in sostanza, le parti ereditate sono isolate. La Classe C può certamente funzionare con essi, ma può altrettanto bene funzionare con queste parti se vengono semplicemente incluse come parte di essa anziché ereditate. Tuttavia, qui c'è un altro fastidio, ovvero la sovrapposizione dei nomi. Se i metodi processA e processB avessero lo stesso nome, ad esempio process, quale effetto avrebbe la chiamata al metodo process della classe C ? Quale dei due metodi verrebbe chiamato? Naturalmente il C++ ha il controllo in questa situazione, ma questo non aggiunge armonia al linguaggio. Pertanto, l'ereditarietà dell'implementazione non offre vantaggi, ma presenta degli svantaggi. Per questo motivo questa eredità implementativa in Java è stata abbandonata. Tuttavia, agli sviluppatori è rimasta una tale opzione per l'ereditarietà multipla come eredità da un'interfaccia. In termini Java, un'implementazione dell'interfaccia. Qual è l'interfaccia? Una serie di metodi. (Attualmente non stiamo considerando la definizione di costanti nelle interfacce; maggiori informazioni qui .) Cos'è un metodo? E un metodo, nella sua essenza, determina il comportamento di un oggetto. Non è una coincidenza che il nome di quasi tutti i metodi contenga un'azione: getXXX , drawXXX , countXXX , ecc. E poiché un'interfaccia è una raccolta di metodi, l'interfaccia è, di fatto, un determinante del comportamento . Un'altra opzione per utilizzare un'interfaccia è definire il ruolo di un oggetto. Osservatore, ascoltatore , ecc. In questo caso, il metodo è in realtà l'incarnazione di una reazione a qualche evento esterno. Questo è, ancora una volta, un comportamento. Un oggetto può ovviamente avere diversi comportamenti. Se deve essere renderizzato, viene renderizzato. Se ha bisogno di essere salvato, viene salvato. Bene, ecc. Di conseguenza, la capacità di ereditare dalle classi che definiscono il comportamento è molto, molto utile. Allo stesso modo, un oggetto può avere diversi ruoli. Tuttavia, l'implementazioneil comportamento dipende interamente dalla coscienza della classe infantile. L'ereditarietà da un'interfaccia (la sua implementazione) dice che un oggetto di questa classe dovrebbe essere in grado di fare questo e quello. E COME lo fa è determinato indipendentemente da ciascuna classe che implementa l'interfaccia. Torniamo agli errori nell'ereditarietà. La mia esperienza nello sviluppo di vari sistemi mostra che avendo l'ereditarietà dalle interfacce, è possibile implementare qualsiasi sistema senza utilizzare l'ereditarietà di implementazioni multiple. E quindi, quando incontro lamentele sulla mancanza di ereditarietà multipla nella forma in cui esiste in C++, per me questo è un sicuro segno di progettazione errata. L'errore più comune commesso è quello che ho già menzionato: l'ereditarietà viene confusa con l'aggregazione. A volte ciò accade a causa di presupposti errati. Quelli. Prendiamo, ad esempio, un tachimetro, si sostiene che la velocità può essere misurata solo misurando la distanza e il tempo, dopodiché il tachimetro viene ereditato con successo dal righello e dall'orologio, diventando così il righello e l'orologio, secondo la definizione di eredità. (Le mie richieste di misurare il tempo con un tachimetro di solito venivano risposte con battute. Oppure non rispondevano affatto.) Qual è l'errore qui? Nella premessa. Il fatto è che il tachimetro non misura il tempo. E anche le distanze, tra l'altro. Il contachilometri, presente in ogni tachimetro, è un classico esempio di secondo dispositivo nello stesso alloggiamento, ovvero. aggregazione. Non è necessario per misurare la velocità. Può essere rimosso del tutto: ciò non influenzerà in alcun modo la misurazione della velocità. A volte tali errori vengono commessi deliberatamente. Questo è molto peggio. “Sì, lo so, è sbagliato, ma per me è più conveniente”. A cosa potrebbe portare questo? Ma ecco cosa: erediteremo un carro armato da un cannone e una mitragliatrice. È più conveniente così. Di conseguenza, il carro armato diventa un cannone e una mitragliatrice. Successivamente equipaggeremo l'aereo con due mitragliatrici e un cannone. Cosa otteniamo? Un aereo con armi sospese sotto forma di tre carri armati! Perché c'è SICURAMENTE una persona che, senza capirlo, usa un carro armato come una mitragliatrice. Esclusivamente secondo la gerarchia ereditaria. E avrà assolutamente ragione, perché colui che ha progettato una simile gerarchia ha commesso un errore.
In generale non capisco bene l’approccio “ per me è più conveniente così”. È conveniente scrivere come un ascoltatore e quelli che dicono la grammatica di base sono kazly. Sto esagerando, ovviamente, ma l'idea principale rimane: oltre alla comodità momentanea, esiste una cosa chiamata alfabetizzazione. Questo concetto è definito sulla base dell'esperienza di un gran numero di persone. In effetti, questa è quella che in inglese viene chiamata “best practice”: la soluzione migliore. E il più delle volte, le soluzioni che sembrano più semplici portano molti problemi in futuro.
Questo esempio, ovviamente, è molto esagerato e quindi assurdo. Tuttavia, ci sono casi meno evidenti che portano comunque a conseguenze catastrofiche. Ereditando da un oggetto, invece di aggregarlo, lo sviluppatore offre a chiunque la possibilità di utilizzare direttamente la funzionalità dell'oggetto genitore. Con tutto ciò che implica. Immagina di avere una classe che funziona con un database, DBManager . Crei un'altra classe che funzionerà con i tuoi dati utilizzando DBManager - DataManager . Questa classe eseguirà il controllo dei dati, trasformazioni, azioni aggiuntive, ecc. In generale, uno strato intermedio tra il livello aziendale e il livello di base. Se erediti DataManager da DBManager, chiunque lo utilizzi avrà accesso direttamente al database. E, quindi, sarà in grado di eseguire qualsiasi azione aggirando il controllo, le trasformazioni, ecc. Ok, supponiamo che nessuno voglia causare danni intenzionali e che le azioni dirette siano competenti. Ma! Supponiamo che la base sia cambiata. Voglio dire, alcuni principi di controllo o trasformazione sono cambiati. DataManager è stato modificato. Ma il codice che in precedenza funzionava direttamente con il database continuerà a funzionare. Molto probabilmente non lo ricorderanno. Il risultato sarà un errore di tale classe che chi lo cercherà diventerà grigio. A nessuno verrebbe mai in mente di lavorare con il database bypassando DataManager. A proposito, un esempio dalla vita reale. Ci è voluto MOLTO tempo per trovare l'errore. Infine, lo dirò di nuovo. L'ereditarietà deve essere utilizzata SOLO quando esiste una relazione "è". Perché questa è l'essenza stessa dell'ereditarietà: la capacità di utilizzare oggetti di una classe figlia come oggetti di una classe base. Se non esiste una relazione “è” tra le classi, NON DOVREBBE esserci ereditarietà!!! Mai e in nessun caso. E ancora di più, proprio perché è così conveniente. Link alla fonte originale: http://www.skipy.ru/philosophy/inheritance.html
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION