Durante l'apprendimento della programmazione orientata agli oggetti e dopo aver acquisito le nozioni di base su oggetti, campi e metodi, la maggior parte del tempo viene dedicata all'ereditarietà. Ereditare significa acquisire una parte dell'implementazione da una classe base. È sufficiente creare una sottoclasse di una classe base per ereditare ogni campo e metodo non privato.
Un'auto e un aereo sono veicoli, quindi è ovvio che entrambe queste classi debbano essere espanse dalla loro classe base comune, chiamata Veicolo. Questo è un tipico esempio accademico, ma mentre decidiamo di legare queste classi con la relazione di ereditarietà, dobbiamo essere consapevoli di alcune conseguenze.
Fig. 1 Implementazione della relazione di eredità.
In questo caso, le classi sono strettamente collegate tra loro: ciò significa che le modifiche al comportamento di ogni classe possono essere ottenute apportando modifiche alla classe base. codice. Questo può essere un vantaggio o uno svantaggio: dipende dal tipo di comportamento che ci si aspetta. Se l'ereditarietà viene applicata nel momento sbagliato, il processo di aggiunta di una nuova funzione può incontrare alcune difficoltà di implementazione, perché non si adatta al modello di classe creato. Dovremo scegliere tra la duplicazione del codice e la riorganizzazione del modello, un processo che può richiedere molto tempo. Possiamo definire il codice che esegue la relazione di ereditarietà come "aperto-chiuso": ciò significa che è aperto per le estensioni, ma chiuso per le modifiche. Supponendo che nella classe Veicolo ci sia un funzionamento generale e definito del motore di ogni veicolo, nel momento in cui volessimo aggiungere un modello di veicolo senza motore (ad esempio una bicicletta) alla nostra gerarchia di classi, dovremmo apportare alcune serie modifiche alle nostre classi.
classe Veicolo
def start_engine
fine
def stop_motore
fine
fine
classe Aereo < Veicolo
def muovere
avvia_motore
...
stop_motore
fine
fine
Composizione
Se siamo interessati solo a una parte del comportamento della classe esistente, una buona alternativa all'ereditarietà è l'uso della composizione. Invece di creare sottoclassi che ereditano tutti i comportamenti (quelli che ci servono e quelli che non ci servono affatto), possiamo isolare le funzioni di cui abbiamo bisogno e dotare i nostri oggetti di riferimenti ad esse. In questo modo, rinunciamo al pensiero che l'oggetto sia un tipo di un oggetto base, a favore dell'affermazione che contiene solo alcune parti delle sue proprietà.
Fig. 2 Utilizzo della composizione
Seguendo questo approccio, possiamo isolare il codice responsabile del funzionamento del motore nella classe autonoma chiamata Motore e fare riferimento ad essa solo nelle classi che rappresentano i veicoli con motore. L'isolamento delle funzioni con l'uso della composizione renderà più semplice la struttura della classe Veicolo e rafforzerà l'incapsulamento delle singole classi. Ora, l'unico modo in cui i veicoli possono avere un effetto sul motore è usare la sua interfaccia pubblica, perché non avranno più informazioni sulla sua implementazione. Inoltre, consentirà di utilizzare diversi tipi di motori in diversi veicoli e persino di scambiarli durante l'esecuzione del programma. Naturalmente, l'uso della composizione non è impeccabile: stiamo creando un insieme di classi non collegate tra loro, che può essere facilmente esteso e aperto alle modifiche. Allo stesso tempo, però, ogni classe è collegata a molte altre e deve avere informazioni sulle loro interfacce.
classe Veicolo
fine
classe Motore
def inizio
fine
def stop
fine
fine
classe Aereo < Veicolo
def initialize
@engine = Engine.new
fine
def spostare
@engine.start
@engine.stop
fine
def change_engine(new_engine)
@engine = nuovo_motore
fine
fine
La scelta
Entrambi gli approcci descritti hanno vantaggi e svantaggi, quindi come scegliere tra loro? L'ereditarietà è una specializzazione, quindi è meglio applicarla solo ai problemi in cui esistono relazioni di tipo "is-a", in modo da avere a che fare con la vera gerarchia dei tipi. Poiché l'ereditarietà lega strettamente le classi tra loro, per prima cosa dovremmo sempre considerare se usare la composizione o meno. La composizione dovrebbe essere applicata ai problemi in cui ci sono relazioni di tipo "has-a" - quindi la classe ha molte parti, ma è qualcosa di più di un insieme di classi. Un aereo è composto da parti, ma da solo è qualcosa di più: ha capacità aggiuntive, come il volo. Proseguendo con questo esempio, le singole parti possono essere presenti in diverse varianti specializzate, e allora è un buon momento per usare l'ereditarietà.
L'ereditarietà e la composizione sono solo strumenti che i programmatori hanno a disposizione, quindi scegliere lo strumento giusto per un particolare problema richiede esperienza. Quindi, facciamo pratica e impariamo dai nostri errori 🙂