L'evoluzione della programmazione: dagli Spaghetti Code agli Oggetti.


In questa terza ed ultima puntata non ci resta che analizzare i limiti della Programmazione Modulare, e la sua lenta, ma inesorabile, evoluzione nella Programmazione ad Oggetti.

Ciò che ha spinto ad abbandonare il precedente paradigma, o meglio a farlo evolvere trasformandolo lentamente nella Programmazione ad Oggetti , è la scarsa flessibilità nella gestione dei tipi definiti dall'utente, quando questi sono una variante di un tipo base. Anche qui è bene ricorrere ad un esempio pratico. Supponiamo di dover scrivere un mini-cad, dotato delle seguenti primitive geometriche: punto, linea, quadrato, cerchio. Ciascuna primitiva può essere rappresentata da una apposita struttura dati, e con le sue procedure venir racchiusa in un modulo.
Se le guardiamo bene notiamo la presenza di alcune caratteristiche comuni, che potremmo astrarre e raccogliere in una nuova struttura dati rappresentante una primitiva base, che denoteremo come forma base, con la quale realizzare tutte le altre, semplicemente aggiungendo i dati necessari e modificando solo le procedure specifiche. Per esempio tutte le primitive avranno una procedura Draw(), che provvederà a disegnare sullo schermo la figura in questione, ovviamente ciascuna primitiva avrà una sua procedura specifica, ma per noi, grazie all'astrazione, è come se ne esistesse una sola. Fino qui la Programmazione Modulare non mostra alcun limite, però proseguendo nella progettazione del mini-cad ci accorgiamo subito, che qualcosa non va come dovrebbe, e ci costringe a fare salti mortali.
Supponiamo che l'utente possa assemblare più primitive, ed in genere questo è il loro scopo, per realizzare un disegno più complesso. Come rappresentiamo il disegno? Definiamo un nuovo tipo di dato che chiamiamo progetto, anch'esso sarà una evoluzione, in un certo senso, delle primitive e quindi della forma base. La sua struttura dati, a differenza delle altre, dovrà anche contenere l'elenco, mediante una lista, di tutte le primitive che lo compongono.
Anche il modulo di progetto dovrà contenere la procedura Draw(), che a differenza delle altre non lo disegnerà direttamente, ma si limiterà ad invocare per ciascuna primitiva il relativo Draw(). Come fa a sapere quale procedura Draw() chiamare? La soluzione pur essendo abbastanza lineare ed intuitiva è poco pratica. La struttura dati del progetto contiene l'elenco delle primitive, o meglio contiene gli handler di ciascuna primitiva, a questo punto dall'handler deve risalire al tipo della primitiva, oppure nella lista memorizzare anche il tipo. Noto il tipo si può invocare la procedura corretta. Questo lavoro extra è affidato al programmatore. Ogni volta che si modifica il numero di primitive presenti nel mini-cad è necessario aggiornare anche il modulo relativo al progetto, ma ogni modifica può far sorgere errori in altre parti, o rendere necessario alterare anche altre parti del codice, e tutto ciò obbliga ad aver accesso ai sorgenti di ciascun modulo, e non sempre la cosa è possibile, specialmente se si acquistano librerie da terzi. Addio riusabilità del codice.

Negli anni '90, c'è stato il boom dei Personal Computer, e conseguentemente dell'industria legata allo sviluppo del software. Tutti i produttori si sono appoggiati a strumenti e linguaggi Orientati agli
Oggetti, nel tentativo di colmare la grave lacuna della Programmazione Modulare, prima illustrata.
Nonostante un tardivo successo, i primi germi della programmazione ad oggetti furono gettati a cavallo degli anni '60 e '70, grazie a due rivoluzionari (allora) linguaggi: Simula 67, e SmalTalk.

Nella programmazione ad oggetti non vi è una sostanziale differenza tra tipi predefiniti del linguaggio e quelli definiti dall'utente, e quindi i compilatori possono effettuare dei controlli più severi sulla correttezza dei dati da passare o ricevuti, e possono segnalare al programmatore ogni ambiguità, e quando ciò non è possibile il controllo è eseguito a run-time, dando al programmatore opportuni strumenti per intercettare queste anomalie e comportarsi di conseguenza, senza necessariamente bloccare l'elaborazione o mandare in crash il sistema ospite.

Il problema fondamentale della Programmazione è quello di trascrivere in un linguaggio comprensibile alla macchina un elenco di passi che portano alla soluzione del problema, in altre parole si cerca di trasferire le conoscenze dal dominio del problema nel dominio dell'elaboratore. La OOP cerca di fornire tutti gli strumenti necessari per rendere questa transizione il più intuitiva possibile, anche se all'inizio tutto sembra tranne che intuitiva.

Lo scopo principale della OOP è quello di estendere il linguaggio di programmazione in modo da adattarlo alla terminologia del problema, in questo modo il codice che descrive la soluzione è del tutto simile alla descrizione che daremmo parlando usando i termini del problema, ovvero la OOP ci consete di descrivere l'algoritmo usando i termini del problema, piuttosto che quelli del linguaggio di programmazione utilizzato, e questo è un modo più naturale e più flessibile dei precedenti, e rappresenta la migliore forma di astrazione che oggi possediamo. In altre parole potremmo dire che un linguaggio orientato agli oggetti è un meta-linguaggio, ovvero un linguaggio per definire altri linguaggi.

L'estensione del linguaggio, praticamente, si basa sulla definizione di strutture dati, dette Oggetti, che riproducono i dettagli degli Oggetti Reali presenti nello spazio del problema, e dotandoli di comportamenti, mediante la scrittura di procedure ad hoc, dette Metodi, del tutto simili a quelli degli Oggetti Reali.
Strutture dati e procedure ancora una volta sono racchiusi in moduli, questa volta detti Classi, in termini tecnici si parla di Incapsulamento dei dati e dei metodi nella classe. Per esempio una lampadina, nel mondo reale, può essere: buona, rotta, accesa, spenta. Il corrispondente oggetto avrà degli attributi che memorizzeranno il suo stato, e dei metodi per manipolare lo stato, per esempio Accendi(), Spegni(), o per controllare Rotta(), Accesa(), oltre a due fondamentali detti Constructor() e Destructor(). Il primo serve per allocare ed inizializzare la struttura dati, e ne restituisce l'handler, mentre il secondo come è intuitivo, si occupa del compito contrario. Prima di riferirsi ad un oggetto è necessario crearlo mediante la chiamata del costruttore, e se l'operazione ha avuto esito positivo si parla di istanza dell'oggetto, e non più di oggetto, termine con il quale ci si riferisce all'entità astratta. In questo modo il programmatore inizializza correttamente ed automaticamente l'oggetto non appena lo istanzia.

I linguaggi orientati agli oggetti forniscono un importantissimo strumento per la modellazione degli oggetti, e si tratta dell'ereditarietà, ovvero la possibilità di definire un nuovo oggetto, detto figlio, partendo dalla definizione di un altro detto genitore.
Il figlio eredita dal genitore tutti gli attributi ed i metodi, consentendo di aggiungere nuovi attributi e metodi, o di alterare il comportamento di alcuni o di tutti i metodi ereditati dal genitore. In questo modo si specializza il figlio, che a sua volta potrà servire per definire un nuovo oggetto ancora più specializzato, e così via all'infinito.

Questo modo di procedere rappresenta la soluzione ottimale al problema del mini-cad.


La forma base rappresenta il genitore, dal quale man mano saranno derivati nuovi tipi come il punto, la linea, ecc... Per ciascuna nuova classe ci occupiamo solo di aggiungere gli attributi specifici necessari per la sua visualizzazione sullo schermo, e di ridefinire il metodo Draw().
L'oggetto progetto, questa volta sarà costituito da una semplice lista di forme base, per il linguaggio, i figli sono del tutto equivalenti al genitore, essendone solo una estensione. Il metodo Draw(), in precedenza era il punto critico del progetto, mentre ora è di una banalità disarmante, deve limitarsi a recuperare dalla sua lista di definizione l'handler, e di questo invocare il metodo Draw(). Essendo una lista di forme base il programmatore invocherà sempre lo stesso metodo Draw(), ma il codice eseguito sarà quello specifico della figura in questione, e non un altro. Questa peculiarità è detta Polimorfismo.

Qualcuno potrebbe obbiettare: "se la forma base non viene mai tracciata sul video, perché si deve definire il metodo Draw()? Non è uno spreco di tempo?". In poche parole, se tale oggetto non fosse dotato del metodo Draw() non si potrebbe sfruttare il polimorfismo. Se due o più figli di uno stesso genitore possiedono un comportamento comune, ma il genitore non lo possiede, quando si usano i figli in luogo del genitore, è possibile invocare soltanto i metodi del genitore, gli altri genereranno una segnalazione di errore, in quanto specifici e non appartenenti alla famiglia. E' possibile indicare il possesso di un metodo da parte di un oggetto, senza che questo sia effettivamente definito, e ciò torna utile proprio per sfruttare a pieno il polimorfismo. Per consentire di dichiarare, senza definire, un metodo di un particolare oggetto, i linguaggi orientati agli oggetti possiedono la direttiva Virtual o similare, che serve solo per specificare l'interfaccia del metodo.
Tale direttiva avvisa il compilatore che i figli dovranno ridefinire il metodo, ed informa il programmatore che dovrà sempre usare uno dei figli in luogo del genitore, altrimenti incorrerà nella segnalazione di errore.

I vantaggi offerti dal paradigma OO, sono tanti ed innegabili, ma hanno un costo. Causa l'eccessiva astrazione, il programma OO è più lento dell'equivalente programma scritto seguendo il paradigma procedurale, ma data la rapidità con cui cresce la potenza di calcolo dei computer, questo overhead è abbastanza trascurabile. La fase di progettazione diventa critica per la buona riuscita del progetto, e quasi l'80% del lavoro complessivo spetta proprio a questa fase. Durante la progettazione delle classi, si deve analizzare il problema con la logica Top-Down, partendo dal problema generale si giunge alla soluzione, attraverso fasi successive, man mano sempre più particolari e dettagliate, in modo da far emergere gli elementi atomici del problema che costituiranno le specifiche per la realizzazione delle classi. Una volta realizzate e testate le classi si deve costruire il programma seguendo una logica Bottom-Up, ovvero dal livello più basso si sale a quello del programma completo, assemblando via via tutti i pezzi. Tutto ciò può disorientare i programmatori poco esperti, o che si sono avvicinati da poco alla OOP, perchè non riescono a comprendere bene che la fase critica è proprio quella della progettazione delle classi, e non l'ottimizzazione del codice al loro interno, cosa che può benissimo avvenire in un secondo tempo, se la natura dell'elaborazione lo richiederà.

Prima di concludere è bene puntualizzare che man mano che le tecniche si sono raffinate, si è guadagnato in affidabilità e leggibilità del sorgente, ma si è pagato il vantaggio innegabile sia in termini di dimensioni, come si evince dai due esempi in Fortran ed E, che illustrano brevemente i paradigmi
Spaghetti Code e Programmazione Procedurale-Strutturata, e sia in termini di velocità di esecuzione, in quanto il codice extra produce comunque un maggior numero di operazioni che il processore deve eseguire. La soluzione migliore consiste nel saper scegliere la tecnica migliore per risolvere il problema assegnato.


Per saperne di più:

[1] Alessandro De Simone "Programmazione Modulare" Commodore Computer Club Nº34.

[2] Bjarne Stroustrup "Linguaggio C++" 2nd ed. Addison-Wesley.

[3] Manuel Lemos "Objection: A portable Object Oriented Programming support system" Atti IPISA '95.

[4] Bruce Eckel "Thinking in C++" 2nd ed.

[5] Bruce Eckel "Thinking in Java" Prentice Hall.

[6] Fiorentino, Laganà, Romani, Turini "Pascal. Laboratorio di programmazione" McGraw-HILL

[7] Fiorentino, Laganà, Romani, Turini "C & Java. Laboratorio di programmazione" McGraw-HILL

[8] Fuggetta, Ghezzi, Morasca, Morzenti, Pezzè "Ingegneria del software" Mondadori Informatica.

[9] Vincenzo Gervasi "Amiga E: programmazione ad oggetti" Amigamagazine N°84.


Francesco De Napoli