Vai al contenuto

La programmazione ad oggetti spiegata a mio figlio

Quali sono i principi fondamentali della programmazione orientata agli oggetti? È uno di quegli argomenti che viene chiesto così spesso che un informatico non può permettersi di non sapere. Come le classiche domande banali in un colloquio, anche questa serve principalmente a scoprire il grado di preparazione di un candidato o il livello di comprensione di un argomento.

Ma torniamo a noi… Quattro principi basilari: incapsulamento, astrazione, eredità e polimorfismo.

Queste parole possono sembrare spaventose per uno sviluppatore junior. E le complesse spiegazioni eccessivamente lunghe di Wikipedia a volte non aiutano ma raddoppiano la confusione. Per questo motivo, questo articolo è dedicato ad una spiegazione semplice, breve e chiara per ciascuno dei 4 concetti, come se davanti a me ci fosse mio figlio.

Incapsulamento

Supponiamo di avere un programma che possiede alcuni oggetti “logicamente” diversi che comunicano tra loro – secondo le regole definite nel programma.

L’incapsulamento si ha quando l’oggetto mantiene il suo stato privato all’interno della classe. Altri oggetti non hanno accesso diretto a questo stato. Invece, possono solo chiamare un elenco di funzioni pubbliche, chiamate metodi. Quindi, l’oggetto gestisce il proprio stato tramite i metodi e nessun’altra classe può modificare lo stato se non esplicitamente consentito. Se si desidera comunicare con l’oggetto, è necessario utilizzare i metodi pubblici forniti. Ma (per default), non è possibile modificare lo stato.

Supponiamo di voler creare un gioco del tipo Sims usando la programmazione ad oggetti. Nel nostro mondo Sims, convivono tra gli altri persone e gatti. Persone e gatti devono in qualche modo comunicare tra di loro. Vogliamo applicare l’incapsulamento e quindi incapsuliamo tutte le logiche “feline” in una classe Gatto. Potrebbe assomigliare a qualcosa del genere:

Puoi dare da mangiare al gatto ma non puoi cambiare direttamente lo stato di quanto sia affamato il gatto.

Nella classe Gatto, lo stato è rappresentato da 3 variabili private: umore, affamato ed energia. Viene implementato anche un metodo privato miao() per far miagolare il gatto. Tale metodo viene dichiarato privato perché le altre classi non possono dire al gatto quando miagolare. Quello che invece possono fare è definito nei metodi pubblici dorme(), gioca(), mangia(). Ognuno di questi metodi modifica lo stato interno dell’oggetto modificando le variabili private e possono invocare il metodo miao(). In questo modo viene effettuato un legame tra lo stato, che è privato, e i metodi pubblici. Questo è l’incapsulamento.

Astrazione

L’astrazione può essere pensata come un’estensione naturale dell’incapsulamento. Nella progettazione orientata agli oggetti, i programmi sono spesso estremamente grandi. E gli oggetti separati comunicano molto tra loro. Quindi manutenere per anni questo codice, con tutti i cambiamenti e gli aggiornamenti del caso, diventa difficile.

L’astrazione è un processo che punta a semplificare un problema complesso. 

Per risolvere un problema, non ci si deve perdere in ogni dettaglio, bensì prestare attenzione solo a quelli che portano alla sua soluzione. L’astrazione in pratica consente di trovare una soluzione generale e riutilizzabile in tutte le situazioni riconducibili a una certa casistica. Un soluzione quindi che dovrebbe essere innanzitutto facile da utilizzare (per i successivi riutilizzi) e che raramente dovrebbe cambiare nel tempo.

Facciamo alcuni esempi concreti. Immaginiamo una coda di persone in un bancomat che attendono di dover prelevare. La persona che arriva prima, preleva per prima secondo un concetto molto caro agli informatici chiamato FIFO (first in, first out). Immaginiamo adesso una catena di montaggio di automobili. Le vetture vengono assemblate una dopo l’altra e la prima vettura ad entrare nella catena sarà la prima ad uscire. Lo stesso concetto si può anche applicare alle comande di un ristorante e cosi via.

Sebbene questi casi potrebbero sembrare molto diversi tra di loro, c’è una cosa che li accomuna: la coda. In questo caso l’astrazione ci ha consentito di trovare un modello generale per risolvere i casi particolari. Nella coda FIFO non ci interessa il tipo di elemento che sta al suo interno, ma solo che l’oggetto che entra per primo, sarà l’oggetto che uscirà per primo.

Perchè astrazione e incapsulamento sono concetti legati tra di loro? L’astrazione è una caratteristica per rendere efficace l’incapsulamento

Prendiamo ad esempio, la macchina per il caffè. Fa un sacco di cose e fa rumori bizzarri all’interno, ma tutto quello che bisogna fare per fare una tazzina di caffè è mettere la capsula e premere un pulsante. Quindi pensiamo all’astrazione come un piccolo insieme di metodi pubblici che qualsiasi altra classe può chiamare senza “sapere” come funzionano all’interno (se l’acqua della macchinetta viene riscaldata da una resistenza elettrica o da un mini reattore nucleare a noi poco importa, l’importante è che la macchinetta svolga il suo compito)

Facciamo un altro esempio di astrazione della vita reale: lo smartphone

I moderni smartphone sono complessi ma usarli è molto semplice. Interagiamo con il tuo telefono usando solo pochi pulsanti. Cosa sta succedendo all’interno non ci interessa saperlo. Abbiamo solo bisogno di conoscere un breve insieme di azioni.

Riconoscere l’occasione in cui è opportuno utilizzare l’astrazione è un compito che spetta al programmatore. Non è sempre necessario ricorrere all’astrazione per risolvere il problema, anzi alcune volte potrebbe essere controproducente pensare ad un’astrazione forzandola ai limiti dell’immaginabile. Quindi il consiglio è quello di valutare caso per caso.

Ereditarietà

Qual è un altro problema comune nel design OOP? Gli oggetti sono spesso molto simili, condividono una logica comune (per logica intendo metodi e funzioni) ma non sono completamente uguali. Quindi, come possiamo riutilizzare la logica comune ed estrarre la logica univoca in una classe separata?

Un modo per ottenere questo è l’ereditarietà. In parole povere si crea una classe (sottoclasse) che “eredita” da un’altra classe (superclasse) i metodi comuni. In questo modo, formiamo una gerarchia. La classe figlio riutilizza tutti i campi e i metodi della classe genitore (parte comune) e può implementare una propria logica unica.

Per esempio:

Un insegnante privato è un tipo di insegnante. E ogni insegnante è un tipo di persona. Se il nostro programma ha bisogno di gestire insegnanti pubblici e privati, ma anche altri tipi di persone come gli studenti, possiamo implementare questa gerarchia di classi. In questo modo, ogni classe aggiunge solo ciò che è necessario mentre riusa la logica comune delle classi genitori.

Polimorfismo

Polimorfismo, dal greco “molte forme”, è la capacità di assumere più forme. Dopo aver definito l’ereditarietà e incapsulamento ci poniamo un problema con un esempio del mondo reale: supponiamo di avere un uomo, una scimmia e un leone (non è una barzelletta) che eseguono l’azione di camminare. Pur facendo parte di una classe genitore che chiameremo EsseriViventi, ciascuno cammina a modo proprio: l’uomo a due gambe, la scimmia in maniera più goffa e il leone a quattro zampe. La classe EsseriViventi diventerà un’interfaccia e le classi figlio implementeranno i metodi (chiamati anche firme) in essa contenuti.

Facciamo un esempio: nella figura in basso implementiamo un’interfaccia comune che chiameremo FigureGeometriche per effettuare il calcolo di una non precisata figura geometrica. In questa classe interfaccia definiamo solamente i nomi dei metodi, senza specificarne il contenuto, ad esempio calcolaArea() e calcolaPerimetro()

Le classi Triangolo, Cerchio e Rettangolo implementano ciò che è stato definito in FormeGeometriche. Cioè tutte le classi avranno al loro interno due metodi chiamati calcolaArea() e calcolaPerimetro() ma con logiche di programmazione completamente diverse, perchè, come sappiamo,  l’area del triangolo non si calcola allo stesso modo dell’area del cerchio…

All’interno del codice del nostro programma potremmo poi definire 3 oggetti di tipo FormeGeometriche e inizializzarli con la parola chiave new rispettivamente come Triangolo, Cerchio o Rettangolo. Quando si tenta di calcolare l’area di una figura (quindi di un corrispondente oggetto di uno dei tre tipi menzionati), entra in gioco il polimorfismo, che stabilisce in maniera del tutto autonoma quale metodo di quale classe chiamare. Semplice, no?