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.
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.
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
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_PHONEe 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
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
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.
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
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
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").
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.
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
PROC nome metodo() OF nuovo-Oggetto
nuovo codice
nuovo codice
ENDPROC
SUPER self.metodo() -> chiama metodo di vecchio_oggetto.
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
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.
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.
Homepage: http://www.intercom.it/~fsoft