Basi di OOP e sue implementazioni in AmigaE


Overview

La Programmazione Orientata agli Oggetti (OOP) rappresenta la naturale evoluzione di altre tecniche di programmazione, come la programmazione strutturata e quella procedurale. Le varie metodologie di programmazione evolutesi in questi anni, hanno sempre cercato di incapsulare i dati non essenziali al programmatore in strutture invisibili in modo da ridurre al minimo la possibilità di errore. Facendo un esempio, una procedura o una funzione chiamata Test() potrebbe richiedere in input alcuni parametri, e dovrebbe essere compito della procedura stessa di assicurare un corretto funzionamento anche in situazioni critiche (ad es. parametri non validi, mancanza di memoria, ecc...).

Caratteristica della programmazione procedurale è la relativa semplicità del debug: se i risulati ottenuti dalla procedura Test() non sono corretti, il problema risiede solo() nella procedura Test() stessa. Quindi un programmatore è in grado di correggere facilmente eventuali errori. A patto che non scriva un intero programma in una sola volta e poi tenti di debuggarlo.

La OOP rappresenta la naturale evoluzione della programmazione procedurale. Anche se quello che sto per dire non è del tutto corretto, un oggetto potrebbe essere visto come una struttura di dati (le STRUCT del C, gli OBJECTs dell'E, i NEWTYPE del BLITZ) associata ad una serie di funzioni mirate alla manipolazione dei dati contenuti nella struttura stessa. La mia definizione di cosa sia un oggetto è sicuramente troppo semplicistica ma, in definitiva, la programmazione a oggetti si limita a questo. I vantaggi però non sono così semplicistici...

Dal punto di vista dei programmatore, un oggetto può essere visto come un vero e proprio programma a sè stante, con le sue variabili globali e locali (le prime incluse nella definizione dell'oggetto, le altre nelle varie funzioni/procedure di manipolazione dei dati dell'oggetto stesso), le sue funzioni e le sue routine di inizializzazione e distruzione (la main() del C e dell'E). Scrivendo oggetto, il programmatore si comporta esattamente come se stesse scrivendo un programma. Tutti i dati verranno manipolati all'interno dell'oggetto stesso, e sta al programmatore decidere cosa rendere visibile, e cosa invece tenere invisibile all'interno dell'oggetto.

Fondamenti di OOP

Un oggetto, come abbiamo già detto, è formato da una parte strutturale di dati (le variabili) ed un'altra di funzioni legate all'oggetto stesso, che permettono la manipolazione dei dati. Sia le variabili all'interno dell'oggetto che le funzioni stesse, possono essere rese visibili o invisibili dall'implementatore dell'oggetto ai futuri utilizzatori dell'oggetto stesso: in definitìva, è chi crea l'oggetto che decide se determinate caratteristiche dell'oggetto devono essere sfruttabili da altri, oppure rimanere interne all'oggetto stesso. Questa caratteristicha della OOP, permette di creare delle vere e proprie "scatole nere" (gli oggetti), che svolgono determinate funzioni correttamente e che possono (e devono) essere utilizzate da altri programmatori senza preoccuparsi molto di come siano effettivamente state create.

Personalmente, ho scritto molti oggetti che mi permettono operazioni particolari sui dati o su particolari caratteristiche del Sistema Operativo di Amiga... e di alcuni non so più con precisione cosa facciano. Più o meno è così che dovrebbe essere per tutti: un oggetto deve essere chiuso in sè stesso ed assicurare funzionalità.

Uno dei principi più importanti della programmazione a oggetti e' di cercare di creare oggetti piccoli, compatti e specializzati. Ereditare un oggetto con queste caratteristiche sarà sempre facile e fruttuoso. Gli oggetti creati dovranno essere il più specializzati possibile: se create un oggetto "disco" che si occupa dell' input/output su di un disco, cercate di prevedere tutte le funzionalità che un programmatore potrebbe richiedere e fate anche in modo che l'oggetto creato sia testato in tutte le condizioni, in modo da assicurare stabilità anche se il programmatore che si accinge ad utilizzare il vostro oggetto commette degli errori.

La OOP ha delle caratteristiche esaltanti e riesce a mettere nelle mani dei programmatore una serie di strumenti per lo sviluppo rapidissimo di applicazioni complesse. Tutto questo ha però un prezzo: bisogna pianificare accuratamente le caratteristiche e i comportamenti dell'oggetto PRIMA di iniziare a scrivere del codice. Questo accorgimento, che di per sè potrebbe sembrare banale, riesce invece ad assicurare un notevole risparmio di tempo nella stesura definitiva e permetterà poi di ereditare l'oggetto proficuamente.

Un primo esempio


Iniziamo adesso a scrivere il nostro primo oggetto in AmigaE, per quello che riguarda la parte teorica della programmazione a oggetti, ci occuperemo dei vari aspetti man mano che li incontreremo.

Il nostro primo oggetto sarà molto semplice e sicuramente non molto utile, ma servirà ad introdurci alla programmazione a oggetti. Ci occuperemo di scrivere un oggetto chiamato "person" che conterrà il nome, il cognome e il numero di telefono di una persona.

Per prima cosa, inizializziamo descrivendo la struttura dell'oggetto persona, in Amiga E scriveremo:

OBJECT person
name:PTR TO CHAR
surname:PTR TO CHAR
phone:PTR TO CHAR
ENDOBJECT

come vedete abbiamo definito un OBJECT standard di E, che normalmente si utilizza per la creazione delle strutture. Come abbiamo detto in precedenza, un oggetto non è altro che un insieme di dati organizzati in una struttura e delle funzioni particolari apposite per la loro manipolazione, non c'è da stupirsi, quindi, che un oggetto in Amiga E inizi proprio con la definizione della struttura OBJECT, così come siamo sempre stati abituati a fare.

Tra le funzioni più importanti legate ad un oggetto, troviamo il "costruttore", che serve ad inizializzare un oggetto durante la sua creazione, e il "distruttore", che invece si occupa di liberare tutte le risorse eventualmente allocate dall'oggetto. Solitamente, al "costruttore" si assegna lo stesso nome dato all'oggetto che deve costruire, quindi nel nostro caso si chiamerà "person". Il "distruttore", invece, deve avere il nome fisso "end()" e non dovrebbe MAI essere chiamato, in quanto è AmigaE stesso che si occupa di invocarlo quando decideremo di "uccidere" un oggetto.

Ricordatevi di chiudere tutti gli oggetti creati durante il programma, prima di uscire definitivamente, altrimenti potreste lasciare della memoria allocata.


Creiamo adesso il costruttore e il distruttore dei nostro oggetto person. Il costruttore si occuperà di allocare una stringa di 25 caratteri per ogni campo. Normalmente, non bisognerebbe dare dei limiti "rigidi" ai nostri oggetti, ma per ora ci limiteremo a descrivere il costruttore e lo faremo anche in modo non del tutto "corretto", in seguito miglioreremo anche questo fattore. Per creare una funzione legata ad un oggetto, in E, basta scrivere:

PROC nomeproc() OF nomeoggetto

e così, noi faremo adesso per il costruttore (notate che si chiamerà esattamente come l'oggetto).

PROC person() OF person
self.name: = String(25)
self.surname: = String(25)
self.phone:=String(25)
ENDPROC

come potete notare, le variabili "interne" ad un oggetto vengono referenziate con il suffisso self..
Adesso possiamo creare il distruttore, che si dovrà chiamare per forza end() e che si occuperà di liberare tutte le risorse prima che l'oggetto venga ucciso:

PROC end() OF person
DisposeLink(self.name)
DisposeLink(self.surname)
DisposeLink(self.phone)
ENDPROC

Ora che il nostro oggetto è in grado di inizializzarsi e di distruggersi, possiamo iniziare a scrivere le routines necessarie a gestire i dati interni.

Cominciamo con la creazione di due metodi: set() e get() che, rispettivamente, si occupano di settare e leggere una determinata informazione all'interno.

All'inizio del nostro codice scriviamo la seguente linea per creare delle costanti numeriche:

ENUM MODE_NAME = 1, MODE_SURNAME, MODE_PHONE

e poi scriviamo, il seguente metodo set():

PROC set(s:PTR TO CHAR, mode= MODE_NAME) OF person

SELECT mode
CASE MODE_NAME
StrCopy(self.name, s)

CASE MODE_SURNAME
StrCopy(self.surname, s)

CASE MODE_PHONE
StrCopy(self.phone, s)
ENDSELECT

ENDPROC

e di seguito il metodo get():


DEF s:PTR TO CHAR

SELECT mode
CASE MODE_NAME
s := self.name
CASE MODE_SURNAME
s := self.surname
CASE MODE_PHONE
s := self.phone
ENDSELECT
ENDPROC s


Con quest'ultima procedura, possiamo già testare il nostro oggetto e vederne il funzionamento, per farlo, sarà necessario una procedura main() e una istanza dell'oggetto person. Il codice che segue, crea un oggetto person, tramite set() inserisce dei dati e li rilegge con get().

PROC main()

DEF p:PTR TO person

NEW p.person( -> Notate l'utilizzo del costruttore assieme a NEW
p.set('Fabio') -> Settiamo il nome della persona
p.set('Rotondo', MODE_SURNAME) -> Settiamo il cognome della persona
p.set('(ITA)-(0)321 - 459676', MODE_PHONE) -> Settiamo il numero di telefono.
WriteF('Name:\~\nSurname:\s\nPhone:\s\n', p.get(), p.get(MODE_SURNAME), p.get(MODE_PHONE)
END p -> Uccidendo l'oggetto, automaticamente liberiamo anche la memoria allocata da questo.
CleanUp(0) -> Questo comando e' necessario per ripulire tutto, lo dice Wouter
ENDPROC -> Fine dei programma.

OBJECT INHERITANCE

Una delle caratteristiche più interessanti della programmazione a oggetti è la possibilità di creare degli oggetti che si basano su oggetti creati precedentemente, per formarne altri più complessi. Questa caratteristica è chiamata "inheritance" (eredità) ed è uno strumento molto potente per il programmatore, ma anche, sotto molti aspetti, complesso da utilizzare al meglio.

Disegnando un oggetto che si intende poi rendere ereditabile in modo efficace è necessaria una lunga sessione di pianificazione dell'oggetto stesso: cosa si desidera rendere pubblico e cosa invece mantenere nascosto, tenendo presente che un oggetto "figlio" vedrà solo le parti pubbliche dell'oggetto originario.

Prima di continuare nella nostra discussione, sarà opportuno definire una certa terminologia:

CLASSE: è un oggetto completo di variabili interne e di procedure di manipolazione.
R00TCLASS: è la classe di base sulla quale si appoggiano altri oggetti.
SUPERCLASS: è la classe precedente a quella attuale, diciamo il "padre" dell'oggetto corrente.
METODO: è una funzione di un oggetto.
COSTRUTTORE: è il metodo utilizzato per inizializzare un oggetto.
DISTRUTTORE: è il metodo utilizzato per eliminare un oggetto, liberando tutte le risorse allocate da questo.

In AmigaE, per scrivere un oggetto che ne eredita un altro, bisogna scrivere:

OBJECT nome_oggetto OF oggetto-padre
definizione ...
definizione ...
ENDOBJECT


Per il resto si procede esattamente come nella creazione di un singolo oggetto.


ATTENZIONE: durante il proseguimento di questo articolo, mostreremo delle sezioni di codice incomplete, a semplice scopo dimostrativo. Anche la sintassi di AmigaE nella creazione/descrizione di un oggetto non verrà completamente rispettata.

La scrittura di un oggetto ereditabile porta alla definizione di metodi che rendano accessibili anche risorse più "interne" e questo potrebbe risultare pericoloso se usati da programmatori poco esperti.
Facciamo un esempio pratico:

Supponiamo di avere un oggetto "immagine" cosi definito:

OBJECT immagine
PRIVATE
cols:PTR TO crnap
bmp :PTR TO bitmap
ENDOBEJCT

e che siano definiti questi metodi:

bitmaptoscreen(scr:PTR TO screen) -> Usato per visualizzare la bitmap su di uno schermo.
colorstoscreen(scr: PTR TO screen) -> Usato per settare la palette a quella dell'immagine.

Con questi metodi è possibile, effettivamente mostrare una immagine, e l'utilizzatore dell'oggetto non dovrà mai preoccuparsi di sapere dove effettivamente risiedono i puntatori alla bitmap ed alla colormap: questo è molto sicuro dal momento che le risorse allocate dall'oggetto saranno liberate dall'oggetto stesso; quindi non dovrebbero esserci problemi. Ma supponiamo che si desideri creare un oggetto chiamato "remapper" che si occupi di permettere il remap di un oggetto "immagine" con una palette differente da quella dell'oggetto stesso.

L'oggetto "remapper" deve essere in grado di accedere (e modificare) ai dati dell'oggetto "immagine"; quindi dovremo definire questi due metodi (ancora nell'oggetto "immagine"):

bitmap() -> Restituisce il puntatore alla bitmap
cols() -> Restituisce il puntatore alla colormap

E dovranno essere creati anche metodi che permettano la creazione e la distruzione di "parti" dell'oggetto:

alloc(width, height, depth) -> Alloca una bitmap
free() -> Libera la bitmap

L'oggetto "remapper" dovrà così essere costruito:

OBJECT remapper OF immagine
ENDOBJECT

remap(pal:PTR TO cmap) IS self.makeremap(SUPER self.bitmap(), SUPER self.cols(), pal)


Il metodo remap() che ho appena descritto ha delle peculiarità interessanti delle quali è meglio parlare subito. Innanzitutto, notate la parola chiave SUPER che permette di chiamare un metodo di un oggetto precedente al corrente. Questo è necessario, ad esempio nel nostro caso, poichè l'oggetto "remapper", ereditando completamente "immagine" ne possiede anche i metodi "bitmap()" e "cols()", che però, nell'oggetto in questione, sono due puntatori inutilizzati: con il metodo SUPER verrà invocato il metodo dell'oggetto precedente, in modo da poter accedere effettivamente ai dati desiderati (nel nostro caso, otterremo i puntatori alla bitmap e alla palette di "immagine").

OBJECT REDEFINITION OF METHODS


Un'altra caratteristica degli oggetti che ne ereditano degli altri è la loro possibilità di "ridefinire" dei metodi degli oggetti precedenti. La ridefinizione è uno strumento molto potente che permette a oggetti "simili" di comportarsi in modo completamente differente tra loro. Questa caratteristica, unita con il comando SUPER permette effettivamente di semplificare e migliorare notevolmente il comportamento di oggetti precedenti... o semplicemente di migliorare il nostro codice.

Tornando al nostro esempio precedente, potevamo ridefinire i metodi bitmap() e cols() dell'oggetto "remapper" in questo modo:

bitmap() OF remapper IS SUPER self.bitmap() -> Chiamiamo bitmap() di "immagine"
cols() OF remapper IS SUPER self.cols() -> Chiamiamo cols() di "immagine"

E potevamo definire remap() in questo modo:

remap() IS self.makeremap(self.bitmap(), self.cols(), pal)

Naturalmente, questo è un esempio abbastanza inutile e poco significativo, ma cercheremo di farne di più interessanti più avanti.

RIEPILOGO


Ripassiamo adesso quello che è stato esposto in questo articolo, perchè è molto importante per il corretto (ed efficace) utilizzo degli oggetti e della loro programmazione.

INHERITANCE (Ereditarietà): un oggetto può ereditarne un altro e sfruttare tutte le caratteristiche proprie dell'oggetto originale. In AmigaE la sintassi è la seguente:

OBJECT nuovo_oggetto OF vecchio_oggetto
definizione dei nuovo oggetto
ENDOBEJCT


METHODS REDEFINITIONS (ridefinizione dei metodi):un oggetto creato su di un altro, ne può ridefinire i metodi e i comportamenti in maniera del tutto trasparente. La sintassi in AmigaE è la seguente:

PROC nome metodo() OF nuovo-Oggetto
nuovo codice
nuovo codice

ENDPROC



SUPER:è un comando particolarmente interessante perchè rende possibile invocare un metodo dell'oggetto originario dal nuovo oggetto. La sintassi in AmigaE è questa:

SUPER self.metodo() -> chiama metodo di vecchio_oggetto.


ESEMPIO


MODULE 'tools/exceptions'
- > Queste costanti sono usate dopo per get() e memorize()
ENUM MEMORIZE_NAME=O, MEMORIZE_SURNAME
/*
Ecco la nostra classe "person"
*/
OBJECT person
PRIVATE - > Tutti i dati sono privati
name:PTR TO CHAR
surname:PTR TO CHAR
ENDOBJECT

PROC person() OF person -> Costruttore dell'oggetto "person"
self.name: = String(30)
IF self.name = NIL THEN Raise("MEM")

self.surname: = String(50)
IF self.surname = NIL THEN Raise("MEM")
ENDPROC

PROC end() OF person -> Distruttore dell'oggetto "person"
DisposeLink(self.name)
DisposeLink(self.surname)
ENDPROC

PROC clear() OF person -> Qui puliamo tutte le stringhe
StrCopy(self.name,'')
StrCopy(self.surname, '')
ENDPROC
/*
Questa proc è utilizzata per memorizzare i dati all'interno dell'oggetto.
Per favore, notate il SELECT, che permette una espansione semplice
dell'oggetto.
*/

PROC memorize(v, mode=MEMORIZE_NAME) OF person
SELECT mode
CASE MEMORIZE_NAME
StrCopy(self.name, v)
CASE MEMORIZE_SURNAME
StrCopy(self.surname, v)
ENDSELECT
ENDPROC

-> Questo comando ritorna i valori all'interno dell'oggetto
PROC get(mode=MEMORIZE_NAME) OF person
DEF p=NIL

SELECT mode
CASE MEMORIZE_NAME
p := self.name
CASE MEMORIZE_SURNAME
p := self.surname
ENDSELECT
ENDPROC p
/*
Qui inizia il nostro oggetto ioperson, una classe DERIVATA da
"person" che permette l'I/O.
*/

OBJECT ioperson OF person
-> Non abbiamo nessun altro nuovo campo
ENDPROC

PROC ioperson() OF ioperson -> COSTRUTTORE classe "ioperson"
self.person() -> invochiamo il COSTRUTTORE di "person"
ENDPROC

PROC end() OF ioperson -> il DISTRUTTORE di "ioperson" è lo stesso di "person"
SUPER self.end() -> Hey, un comando SUPER!!
ENDPROC
-> Questa procedura carica i dati da un file a un oggetto "ioperson"
PROC load(fname) OF ioperson HANDLE
DEF fh=NIL, buf[256]:STRING

IF (fh:=Open(fname, MODE_OLDFILE)) = NIL THEN Raise("file")

FgetS(fh, buf, 255)
StrCopy(self.name, buf)
FgetS(fh, buf, 255)
StrCopy(self.surname, buf)

EXCEPT DO
IF fh THEN Close(fh)
ReThrow()
ENDPROC
-> Questa proc salva I dati da un oggetto "ioperson" a un file
PROC save(fname) OF ioperson HANDLE
DEF fh=NIL

IF (fh:=Open(fname, NEWFILE)) = NIL THEN Raise("file")

FputS(fh, self.name)
FputS(fh, self.surname)

EXCEPT DO
IF fh THEN Close(fh)
ReThrow()
ENDPROC
-> Ecco un esempietto veloce veloce
PROC main() HANDLE
DEF iop = NIL:PTR TO ioperson

NEW iop.ioperson()
iop.memorize('Fabio')
iop.memorize('Rotondo', MEMORIZE_SURNAME)
iop.save('Ram:iopTest')
iop.clear()
iop.load('Ram:iopTest')
WriteF('Nome:\s - Cognome:\s\n', iop.get(), iop.get(MEMORIZE_SURNAME))

EXCEPT DO
report_exception()
END iop
CleanUp(0)
ENDPROC


Object Inheritance in AmigaE V3.x.

Uno dei piccoli trucchi che si imparano pian piano programmando in OOP in AmigaE, è quello di non ereditare direttamente un oggetto per crearne un altro, ma di incapsulare il vecchio oggetto all'interno del nuovo, in modo da nasconderlo completamente dalla vista dell'utilizzatore.

Questo metodo di programmazione ha alcuni vantaggi: in primo luogo, il nuovo oggetto non disporrà di tutti i metodi dell'oggetto originario, e questo, anche se a prima vista può apparire più un limite che una caratteristica interessante, assicura che la lista dei metodi non cresca a dismisura man mano che ereditiamo un oggetto sopra ad un altro. Come secondo vantaggio, possiamo ridefinire dei metodi con un numero di parametri differenti dai metodi originari, senza incorrere in alcuna segnalazione da parte del compilatore. Terzo, senza utilizzare il comando SUPER il nostro codice appare più chiaro e pulito.
Questo accorgimento di programmazione, infine, permette di ottenere degli oggetti più piccoli, a livello di occupazione di memoria del modulo, e più funzionali: i metodi presenti sono quelli che, effettivamente, servono all'oggetto e non uno strascico derivato dall'aver ereditato in maniera canonica un oggetto.


Inheritance Si, Inheritance No


A questo punto bisogna effettivamente valutare quanto sia utile ereditare realmente un oggetto, piuttosto che inglobarlo all'interno di uno nuovo. Ecco i vantaggi, ma anche gli svantaggi, derivanti dalla scelta errata del metodo di implementazione. Se, per esempio, intendiamo scrivere un oggetto che sia realmente una estensione del precedente, è meglio considerare veramente la possibilità di utilizzare le capacità di "inheritance". Il nuovo oggetto erediterà tutti i metodi del precedente, partendo già, quindi, con un bagaglio di informazioni considerevoli. E' molto probabile, quindi, che ci troveremo ad utilizzare un nuovo oggetto che ha troppi metodi. Sicuramente molti di più di quanti ne desiderassimo, a meno che il nostro nuovo oggetto non intenda semplicemente ridefinire o ampliare l'oggetto precedente. Una seconda possibilità, viene quindi dallo scrivere un nuovo oggetto che contiene al suo interno l'oggetto precedente. In questo modo, i metodi dell'oggetto originale non entreranno a far parte dei metodi dell'oggetto che stiamo creando, a meno che non vengano definiti nuovamente. Questo approccio, abbastanza singolare, alla programmazione a oggetti ha anche degli svantaggi: se nel nuovo oggetto alcuni metodi eseguono esattamente le stesse cose dei metodi dell'oggetto precedente, dovremo, comunque, scrivere del codice e riscrivere dei metodi, facendo crescere il codice finale del nostro oggetto.

Comunque, credo che ci si possa avvicinare alla OOP anche tramite questo particolare metodo di programmazione, inglobando un oggetto in un altro: a volte i vantaggi sono superiori agli svantaggi.

Fabio Rotondo

Homepage: http://www.intercom.it/~fsoft