MokaByte Numero 03 - Dicembre 1996

 
La Programmazione
ad oggetti
Terza parte


 

di
Giovanni Puliti
 

 


Ci siamo lasciati un po' di tempo fa dopo aver affrontato i concetti iniziali della OOP. Abbiamo parlato di classi e della relazione di ereditarietà. Oggi affrontiamo gli altri concetti basilari della programmazione ad oggetti: saranno esposti i concetti di polimorfismo e la relazione tutto parti. In abbinamento ai concetti esposti vedremo di analizzare alcune semplici regole che aiutino nella architettazione di un progetto OO.

Vediamo quindi cosa sia il polimorfismo: con tale parola si indica in genere la possibilità che una entità possa assumere molte forme. Semplificando molto le cose tale caratteristica permette di fare riferimento ad oggetti di classi diverse mediante la stessa entità, e di svolgere la stessa azione in modi diversi a seconda della particolare istanziazione su cui si sta lavorando. Per poter utilizzare il polimorfismo non si devono introdurre particolari costrutti sintattici come invece si è fatto quando abbiamo parlato di ereditarietà. I concetti introdotti fin'ora sono infatti sufficienti per poter parlare di polimorfismo ed analizzare le conseguenza ad esso legato.

Invece di prolungare oltre la trattazione teorica con concetti vaghi, preferisco affrontare un esempio e successivamente da esso estrapolare i concetti teorici. Consideriamo ad esempio una gerarchia fra classi presa dal mondo animale: a partire dal concetto (classe) di mammifero possiamo considerare molte sottospecie e quindi definire altrettanti classi. Supponiamo di esaminare la classe balena, la classe cane, la classe leone ed infine la classe uomo che nel corso dei millenni si è evoluto a tal punto da farci oggi spendere del tempo per discutere sulla programmazione ad oggetti e su Java: questo però è un altro aspetto della questione che affronteremo in un'altra rivista :). Ognuno dei mammiferi elencati poco sopra avrà sicuramente fra i metodi disponibili, cioè fra le sue capacità, una che si riferisce al concetto di nutrimento. Visto che tale metodo è comune a tutti i discendenti della gerarchia potremmo darne una definizione generica nella classe mammiferi, ed inoltre tale classe potrebbe essere definita astratta. Ricordiamo che una classe astratta non può essere istanziata, cioè non è possibile lavorare con un oggetto di quella classe. Per chi già' ha familiarità con questo costrutto potremmo giustificare questa scelta col fatto che nessuno ha mai visto, nemmeno nei documentari di Pero Angela, un mammifero in forma indefinita, ma si sono sempre visti immagini e reportage che parlavano di balene, cani, leoni e uomini (uno o più anche in studio...). Invece chi non ha mai approfondito il perché si debba fare uso di classi astratte, si chiederà anche in questo caso del motivo che ci spinge a fare tale scelta. Inoltre, anche se per motivi ignoti ai più si desse per scontato che la definizione di una classe astratta sia cosa giusta e buona, perché insistere su questa strada definendo un metodo generico come nutrirsi visto poi che per ogni sottoclasse dovremo ridefinire i dettagli implementativi? La risposta a tali angoscianti quesiti risiede nella parola genericità alla quale la OOP si lega in senso stretto. Diciamo che, in prima analisi, il poter definire una classe astratta avente metodi generici ci permette di fissare le linee generali del progetto ponendo le basi per uno sviluppo omogeneo ed elegante di tutto il sistema. Si pensi ad esempio al concetto di mangiare , o meglio al più generico nutrirsi: se volessimo definire un metodo corrispondente per la classe mammifero, potremmo definirlo decidendo che esso riceve come input un valore intero esprimente le calorie equivalenti al nutrimento ingerito dall'animale in funzione sia del metabolismo dell'animale sia del tipo di cibo ingerito. In tale modo questa definizione generica risulta valida per ogni classe discendente da mammifero. Una mucca ad esempio deve spendere molto tempo, e poca fatica per ingerire tantissima erba per un equivalente di 1000 calorie rispetto ad un leone che dovrà correre molto per mangiare un po' di carne. Il nostro metodo %public void Nutrirsi(int calorie ) sarà quindi generico per ogni sottospecie della classe mammifero, e la diversa implementazione verrà lasciata al caso in esame.

Quindi avendo dato una seppur breve giustificazione empirica della necessità di lavorare con classi e metodi generici, possiamo riprendere e capire meglio la definizione data all'inizio dell'articolo dove parlavamo di entità e della possibilità di riferirsi a classi diverse o di svolgere azioni diverse a seconda dell'entità presa in considerazione. Il polimorfismo in sintesi è questo: come accennato in precedenza e come si può dedurre dal breve esempio riportato, la sua utilizzazione non prevede l'uso di particolari elementi sintattici. Esso è al tempo stesso molto facile a spiegarsi ma non altrettanto nella sua assimilazione e nell'uso.

Come dicevo in genere una buona implementazione del polimorfismo si accompagna di pari passo ad una buona progettazione della struttura gerarchica. E' per questo che dopo aver parlato della relazione di ereditarietà, prima di affrontare quella di tutto parti, vorrei esporre alcune semplici regole che ci possono aiutare nella definizione di una gerarchia di classi. Infatti anche se è intuitivo capire quale è il legame che unisce due classi, più complicato è capire come strutturare una gerarchia composta da un grande numero. Vediamo quindi cosa suggeriscono gli esperti e la pratica di lavoro.

Relazione Tutto-Parti

Abbiamo fin'ora parlato (fino alla nausea) di relazione di ereditarietà e del concetto di generalizzazione-specializzazione. Vediamo adesso di un altro tipo di relazione che si instaura fra classe, quella detta di %tutto-parti. Pensiamo ad esempio alla classe personal computer che si specializza da un'altra detta computer. Come possiamo allora oopizzare il concetto che la classe pc ha un case, un alimentatore, un hard disk e così via? Questo tipo di problema di relazione fra oggetti si implementa specificando che una classe ne %possiede delle altre e che tale appartenenza serve per definire completamente la classe proprietaria. In questo caso si dice che si passa non da una entità generica ad una più specifica, ma da una globale che ne contiene altre più elementari: dal tutto, il computer assemblato, si passa a considerare le parti, i vari componenti. Anche se considerare anche la relazione tutto parti ci permette maggior potenza espressiva, l'introduzione di questo nuovo concetto, porta ad un problema delicato. Quando dobbiamo scegliere una relazione tutto parti e quando invece una generalizzazione-specializzazione? In genere le cose si evolvono in maniera quasi automatica e facendosi qualche domanda sulla struttura del sistema che stiamo analizzando si riesce a venire a capo di questo rompicapo. Spesso per chiarire tali dubbi si può utilizzare semplici regole, come quella dell'essere o avere che in genere elimina ogni dubbio. Come si usa tale metodo? Vediamo subito: se vi chiedessi se una ruota è una parte di un camion o una sua specializzazione, cioè se un camion %è una ruota, o se il camion %possiede la ruota, voi cosa rispondereste? La risposta verrà lasciata per esercizio al lettore. Vorrei far notare che la relazione tutto parti porta a definire un componente internamente ad una certa classe, e che questo è molto simile a definire per quella un attributo aggiuntivo. Per questo una delle difficoltà nel gestire il tutto-parti deriva dal dover scegliere se definire un attributo o una classe vera e propria. Poter effettuare la scelta giusta non sempre è semplice anche se nella maggior parte dei casi il tutto si risolve con una analisi approfondita del problema (come per quanto detto sopra, non vorrei essere ripetitivo).

Ricordiamo però che Murphi è sempre in agguato e con l'evolversi del progetto si finisce spesso per trovarsi in situazioni imbarazzanti, in cui la scelta non risulta essere così ovvia. Diciamo subito che questo è un argomento molto vasto e affrontarlo in maniera completa richiederebbe una serie di riviste dedicate allo scopo: poiché temo invece che la redazione di MokaByte non intenda dar luogo ad una rivista parallela od ospitare le mie digressioni per i prossimi 100 numeri, cercherò di essere sintetico.

Per chiarire quali possano essere le ambiguità che sorgono supponiamo di considerare ad esempio la classe autovettura: quand'è che si deve specificare tale classe ne possiede un'altra detta motore con tutte le sue caratteristiche oppure semplicemente considerare il motore come un attributo riferendosi al modello o alla cilindrata o ad altro. Chiaramente in questo caso se pensiamo al fatto che un motore può effettuare delle operazioni (funzionare, aumentare di giri,...) e possiede delle caratteristiche (cilindrata, potenza, peso, modello,....) allora risulta abbastanza automatico considerarlo come una istanza della classe motore. Non sempre le cose sono così semplici, anche in funzione dell'ambito in cui si lavora. Se ad esempio il motore ci interessa solo marginalmente e solo per poter tenere memoria della cilindrata dell'autovettura, allora forse non ha senso definire una classe apposita. Questo aspetto di indeterminazione ha maggior peso in un ambito ibrido come quello del C++, mentre in Java si tende a considerare tutto come un oggetto, come in ogni linguaggio purista ad oggetti. Un programmatore di Smalltalk in genere inorridisce al pensiero che si possa fare a meno di definire un oggetto per poter anche fa riferimento ad un'attributo. Anche in Java le cose seguono questa sana abitudine, anche se niente, nemmeno il JDK, ci proibisce di definire semplici attributi. Java con le sue regole sintattico-semantiche ci forza in un certo modo ad usare al massimo la filosofia ad oggetti: se notate quasi tutto in Java è considerato come un oggetto, e solo i tipi semplici (interi, float, double,...) sono implementati non come oggetti, anche se esistono parallelamente le classi Integer, Float, Double,...

Per non perdersi quindi nei meandri di queste problematiche potrei passare velocemente a proporre una regola empirica basata sull'analisi grammaticale: anche se forse molti fra voi che bazzicate fra bit e byte non hanno buoni ricordi con questa materia forse è bene ingoiare il boccone amaro e rispolverare i sostantivi, gli aggettivi, ed i predicati verbali. Partiamo dal analizzare il problema che si deve OOpizzare: tralascio la raccomandazione che una buona riuscita del lavoro richiede una ottima conoscenza in ogni dettaglio del dominio del problema in esame.

L'uso della analisi grammaticale consiste nell'eseguire una raccolta dei termini che compaiono nella relazione del sistema: avremo gruppi di sostantivi, di aggettivi, di avverbi, e di predicati verbali. La prima separazione da luogo all'insieme dei nomi, aggettivi da una parte e a quello dei verbi dall'altra. In genere un nome corrisponde o ad una classe o ad un oggetto componente della classe, mentre un aggettivo o un avverbio ci permette di individuare un attributo, anche se ancora non siamo in grado di decidere se e come definire le classi e le relazioni fra esse. La parte più semplice è quella riguardante i verbi. Qui sicuramente abbiamo a che fare con una azione e quindi con un metodo di una classe. Ricordo che metodi sono funzioni, ma non è possibile definire in OO (e quindi neanche in Java) funzioni a se stanti. Per stabilire quali siano le classi quali gli attributi e quali i componenti, ci si affida in prima istanza alla logica pensando al caso reale. In genere in questa fase si riesce ad assestare le cose quasi completamente. Dove rimanga un dubbio in genere si utilizza l'insieme dei verbi: da tale gruppo si prelevano quei verbi che corrispondono ad azioni eseguibili da entità discrete. Si possono così individuato le classi che compongono il nostro progetto. Inoltre tale operazione di raccolta ci permette anche di individuare la presenza di classi la cui presenza non ci era sembrata necessaria, ma che risulta essere poi indispensabile per completare il quadro degli elementi facenti parte dell'ambiente in esame. Viceversa gli aggettivi potranno essere considerati o come semplici attributi di una qualche classe o come classi autonome legate da qualche relazione tutto parti. Anche in questo caso l'ausilio della analisi delle azioni eseguite o eseguibili nel sistema, ci permette di operare la scelta esatta.

Spesso se il progetto rimane di dimensioni ridotte le due politiche di lavoro, definire classi in ogni caso o invece introdurre dei semplici attributi, sono equivalenti: nel primo caso, si otterrà del codice più elegante, maggiormente comprensibile, più facile da correggere, e più semplice da gestire e manutenere. Il prezzo che in genere si paga è un codice piuttosto grosso, e parallelamente lento in esecuzione in rapporto ad una politica di lavoro non puramente ad oggetti. Non si dimentichi che la OOP non è la panacea di tutti i mali, ed anzi, in certi ambiti di lavoro, viene addirittura considerata deleteria, come ad esempio in certi sistemi di calcolo ottimizzati per la velocità.

Concludendo questo breve corso di introduzione alla OOP spero di aver almeno fatto capire quelle che sono le problematiche di base che si incontrano quando si inizia a confrontarsi con tale tecnica di programmazione. Non sono entrato di proposito nei dettagli tecnici di Java o del C/C++ per i quali si possono avere esaurienti chiarimenti su ogni manuale dedicato a tali linguaggi. Non dimenticate a tal proposito gli articoli scritti sulle pagine di MokaByte da Fabrizio Giudici su Java e C++. Come ultimo consiglio vorrei suggerire a chi volesse dedicare delle energie e del tempo per approfondire argomenti nuovi, di spendere del tempo nello studio della teoria OO, piuttosto che non nella assimilazione dei dettagli sintattici di Java o di un altro linguaggio.
 
  

 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it