1.
CLASSI
A differenza di altri linguaggi di programmazione, in WinDev le classi non
sono composte solamente da metodi e proprietà, ma possono avere anche dei
membri. Pertanto, i componenti di una classe saranno:
• i membri della classe, cioè i dati della classe
• i metodi, cioè le procedure e funzioni definite nella classe
• le proprietà, cioè i metodi di esposizione dei dati della classe
Le classi, di per sé. non sono altro che raggruppamenti di dati (i membri
della classe, e in parte le proprietà) e di procedure (i metodi e, in parte, le
proprietà della classe). I membri, tecnicamente, si potrebbero considerare
delle proprietà 'senza corpo'. In pratica se si ha la necessità di eseguire una
pur minima elaborazione dei dati, allora si usa una proprietà, altrimenti si usa
un membro ('assegnazione diretta')
Ad esempio se facciamo una classe 'Clienti' il valore 'DataDiNascita' sarà
contenuto in un membro (in questo modo possiamo sempre leggere/scrivere la
sua data di nascita) mentre se dobbiamo calcolarne l'età possiamo creare una
proprietà 'QuantiAnni' in sola lettura che restituisce l'età del cliente
calcolandolo dalla data di nascita.
In questo senso una proprietà in sola lettura è praticamente identica ad una
funzione (procedure), ma è sicuramente più elegante.
I membri della classe (non le proprietà) costituiscono a tutti gli effetti i dati
statici della classe, e come tali possono essere serializzati e deserializzati (altro
argomento da approfondire) ma, soprattutto, possono fungere da contenitori
automatici di dati 'simili'. Ad esempio se noi abbiamo una classe 'Clienti'
(magari creata, guarda guarda, tramite UML) possiamo valorizzare tutti i suoi
membri tramite il comando FileToMemory. Alla gestione delle classi e della loro
interazione con i database è opportuno dedicare un intero capitolo.
Per default, tutti i membri, le proprietà ed i metodi di una classe sono
pubblici, vengono cioè 'esposti' nell'istanza di quella classe. Se ho una classe
Anagrafica con dentro il membro Nome, posso scrivere
Ana is Anagrafica
Ana.Nome="Mario"
Però, spesso, non si vuole che tutti gli elementi della classe vengano
esposti. Ad esempio la classe Anagrafica potrebbe avere bisogno di alcuni
membri (proprietà, metodi...) che servono solo al suo funzionamento interno
ma che non si vuole rendere pubblici. Ad esempio potrei avere un metodo
pubblico “Salva” e due membri privati “AggiungiNuovo” e “ModificaEsistente”
che vengono richiamati dal metodo Salva.
Per dichiarare dei membri privati, basta dichiarare all'inizio della classe
(dove sono collocati i membri) una sezione PRIVATE...END all'interno della
quale elencare i membri che dovranno essere privati.
Per i metodi, basta inserire la clausola PRIVATE dopo PROCEDURE, come ad
esempio in PROCEDURE PRIVATE AggiungiNuovo
Classi derivate (ereditarietà)
Le classi possono essere organizzate in classi e sub-classi, possiamo cioè
dare una struttura gerarchica alle nostre classi. In pratica possiamo avere una
classe (o più classi) le cui proprietà e metodi vengono ereditate da una
seconda classe che ne estende il funzionamento. Dalla 'classe base' (o
Ancestor) possiamo derivare una nuova classe (detta appunto classe derivata)
che ha tutti i metodi, le proprietà ed i membri della classe base ed a questi
aggiunge i propri metodi, proprietà e membri. Una classe può ereditare anche
da più classi base.
Ad esempio possiamo avere una classe 'Anagrafica' che contiene i dati di
base per realizzare un'anagrafica (nome, cognome, indirizzo, telefono etc.) e
poi realizzare una classe 'Cliente' che eredita i dati della classe 'Anagrafica' ed
aggiunge i membri e i metodi specifici per la gestione dei clienti
Clienti is Class
Inherits from Anagrafica
idCliente is int....
END
SCOPE (visibilità) degli elementi di una classe
Ogni classe ha una prima parte 'dichiarativa' (che contiene, come minimo, i
membri della classe) seguita dalle sue proprietà e dai suoi metodi. I membri
della classe possono essere dichiarati come Pubblici (Public), Privati
(Private) o Protetti (Protected)
• PUBLIC: i membri pubblici (tutti i membri sono pubblici se non
altrimenti dichiarato, quindi 'Public' si può anche evitare di scrivere) sono
accessibili in tutte le istanze della classe. In pratica, se 'Clienti' è la
classe e 'Nome' è uno dei suoi membri (pubblici), possiamo istanziare
“c is Clienti” e poi usare c.Nome per accedere al membro 'Nome'.
• PRIVATE: questo membro non è visibile nell'istanza nella classe ma è
visibile nel codice della classe. In pratica è una variabile che viene vista
nella classe ma che non viene 'esposta' nell'istanza della classe.
• PROTECTED: Una classe derivata eredita tutti gli elementi pubblici della
classe base, che diventeranno in pratica elementi pubblici della classe
derivata. Ma se ci serve accedere anche agli elementi privati della classe
base? in questo caso la classe base deve dichiarare tali elementi come
PROTECTED. Gli elementi 'protetti' sono elementi che sono privati ma sono
comunque visibili anche nella classe derivata senza per questo doverli
rendere pubblici (e quindi visibili anche esternamente alla classe).
In pratica private costituisce il massimo livello di segregazione (visibile
solo nella classe dove il membro è stato dichiarato), protected è il livello
intermedio (visibile all'interno della classe dove è stato dichiarato, e visibile in
tutte le classi da essa derivate), public è ovviamente il livello a maggiore
visibilità (si 'vede' anche nelle istanze della classe)
I membri pubblici ed i membri protetti possono anche essere resi a sola
lettura nell'istanza tramite costant:
• PUBLIC CONSTANT: all'interno del codice della classe questo membro
si comporta come una normale variabile in lettura e scrittura. Ma
nell'istanza della classe il membro si comporta come una costante a sola
lettura. Ad esempio se il mio cliente è identificato da un membro 'Codice'
e voglio evitare che questo campo venga inavvertitamente modificato al
di fuori del codice di gestione incapsulato nella classe, basta dichiararlo
come public constant e non potrò mai scrivere c.Codice =... in quanto
il campo Codice (nell'istanza) è in sola lettura
• PROTECTED CONSTANT: non è visibile nell'istanza della classe (non ne
costituisce membro esposto) ed è visibile in sola lettura(!) nella classe
derivata.
Ovviamente all'interno della classe è possibile usare anche delle costanti. Le
costanti sono per definizione pubbliche (ovvero visibili anche nell'istanza). Si
possono dichiarare singolarmente oppure a gruppo:
CONSTANT PiGreco = 3.14
CONSTANT
PiGreco = 3.14
AlphaTurk = 7.31
END
GLOBAL. Per finire, i membri di una classe possono essere innalzati fino ad
avere uno scope globale tramite la keyword GLOBAL. Un membro globalizzato
ha due particolarità:
1. è membro comune di tutte le istanze di quella classe. Cioè se nella mia
classe ho un GLOBAL PUBLIC NomeFinestra (public potevo anche non
scriverlo) tutte le istanze di quella classe condivideranno la stessa
identica variabile 'NomeFinestra'. Modificandola in una istanza, viene
modificata in tutte le istanze (è sempre lei...)
2. I membri (ma anche i metodi) globali possono essere indirizzati senza
dover instanziare la classe. A cosa serve? Beh, a fare una bella
GlobalClass con dentro tutte le parti globali del software (ad es. le
connessioni ai DB) e quella classe si utilizza senza doverla istanziare. E
anche se la si istanzia non succede niente in quanto sono tutte variabili
globali (le singole istanze occupano lo stesso spazio di indirizzamento).
Metodi della classe
I metodi costituiscono le PROCEDURE utilizzate dalla classe. Queste
possono avere vari tipi di scope, possono cioè essere visibili anche nell'istanza
oppure possono essere delle procedure scritte per il solo uso interno alla codice
della classe.
Come abbiamo già visto per i membri, anche le procedure possono essere
pubbliche (di default), private o protette.
• PROCEDURE PUBLIC: sono pubbliche e quindi, come i membri pubblici,
possono essere usate nell'istanza della classe (ad es. una classe cliente
potrebbe avere una procedura 'Maggiorenne' che restituisce un booleano
se, in base al membro DataDiNascita, il cliente è maggiorenne). Public si
può omettere di scriverlo in quanto è il default.
• PROCEDURE PRIVATE sono procedure che servono alla gestione
interna della classe, ma che non vengono 'esposte' nell'istanza della
classe.
• PROCEDURE PROTECTED: come per i membri protetti, si tratta di
procedure che possono essere richiamate solo all'interno della classe
(come per le private) oppure all'interno di una classe derivata
GLOBAL. Public, Private e Protected definisco pertanto lo scope di
accesso al metodo (chi può usarlo e chi no). E come i membri, anche i metodi
possono essere globalizzati tramite global (PROCEDURE PUBLIC GLOBAL...). I
metodi globalizzati diventano di fatto delle procedure globali a tutto il progetto
(come i membri globali) ma hanno il pregio di essere incapsulate dentro un
contenitore (classe) che non serve istanziare. Ad esempio se noi creiamo la
classe ProcDate e dentro ci mettiamo tutte le nostre funzioni globalizzate
relative alla gestione delle date (tipo 'Maggiorenne') possiamo utilizzare quelle
funzioni ovunque (che all'interno di una classe – e questo mi piace meno)
senza dover istanziare la classe ProcDate ma usando direttamente ProcDate
come fosse un container di funzioni (che ovviamente espone solo le GLOBAL e
non le sue funzioni interne).
VIRTUAL. La keyword VIRTUAL serve per poter definire l'override di
metodi e proprietà. Poniamo ad esempio che la classe 'Anagrafe' contenga un
metodo 'Save' per salvare i dati in essa contenuti. Dalla classe anagrafe
deriviamo una classe Cliente che ne eredità metodi, proprietà e membri. Però
anche nella classe Cliente ci servirebbe un metodo Save. Per averlo, basta
dichiararlo come VIRTUAL (PROCEDURE VIRTUAL Save) e, se vogliamo,
all'interno di quel metodo possiamo richiamare il Save della classe base con
Anagrafica.Save. In verità la keyword VIRTUAL si può anche omettere in
quanto WinDev virtualizza automaticamente proprietà e metodi della classe
derivata il cui nome combacia col nome dei una proprietà o metodo della classe
base. Però io credo che sia meglio dichiararlo esplicitamente (anche per avere
un codice più leggibile)
Overload di procedure in classi derivate
In WinDev tutte le procedure (non solo quelle classi per intenderci) possono
avere degli overload. Possiamo cioè avere delle procedure che hanno lo stesso
nome ma si differenziano per il tipo e/o numero dei parametri.
Poniamo ad esempio di dover scrivere una procedura che legge un record
“Clienti” da un database. Per identificare il record, questa procedura può
ricevere come parametro o un ID numerico oppure un codice di tipo string. In
pratica ci basta scrivere due procedure con lo stesso nome, ma con parametri
diversi:
PROCEDURE GetCliente (id is int)
//cerchiamo il cliente tramite il suo ID
END
PROCEDURE GetCliente (codice is string)
//cerchiamo il cliente tramite il codice
END
Fino a qui tutto bene: niente di nuovo sotto il sole.
Ma come si comporta l'overload quando abbiamo delle procedure virtuali in
classi derivate?
Il caso più semplice è il classico override: la classe derivata ridefinisce uno
o più metodi della classe base. Poniamo che le due GetCliente usate sopra
siano nella classe base: se nella classe derivata creo una GetCliente (p is
int), questa procedura è una virtual che esegue l'override della medesima
procedura presente nella classe base. Quindi se nell'istanza della mia classe
Clienti chiamo la GetCliente tramite un parametro int, verrà eseguita la
GetCliente della classe Clienti (classe derivata) mentre se chiamo la GetCliente
con un parametro di tipo string, sarà eseguita la GetCliente della classe base
(in quanto non ha alcun override nella classe derivata)
La classe derivata può anche estendere l'overload della classe base.
Vediamolo con un esempio un po' complesso:
nella classe Base:
PROCEDURE GetCliente(p)
PROCEDURE GetCliente(codice is string)
nella classe Derivata:
PROCEDURE GetCliente(id is string)
In questo esempio abbiamo due GetCliente nella classe Base, una con un
parametro di tipo string ed una con un parametro non tipizzato. Nella classe
derivata abbiamo aggiunto un terzo overload per il parametro di tipo intero.
Pertanto se dall'istanza della classe derivata usiamo un parametro di tipo
string, viene eseguita la GetCliente della classe base, se il parametro è un int
viene eseguita la procedura della classe derivata. Negli altri casi (ad es. se il
parametro è un array, un file o altro) viene eseguita la procedura della classe
base.
In caso di ambiguità tra override e overload (ma se avete ambiguità, meglio
rivedere il codice) è possibile aiutare il compilatore aggiungendo l'hint
<override> o <overload> dopo la dichiarazione della procedura.
CLASSI ASTRATTE
Normalmente le classi vengono create per essere istanziate. Ma vi sono
delle classi che non possono venire istanziate ma solamente ereditate: le classi
astratte.
Questo tipo di classe risulta estremamente comodo per realizzare dei pool
di funzionalità comuni ad un gruppo di classi. In un certo modo possiamo
considerarle come degli 'ini' (o include), ovvero dei files contenenti una serie di
procedure comuni.
Ad esempio poniamo di avere un database composto da vari files (clienti,
fornitori, articoli...). Ognuno di questi files corrisponderà ad almeno una classe
(che ne rappresenta il record) ma sicuramente avranno dei metodi in comune
(ad es. i metodi Salva, Cancella, etc). Questi metodi si possono mettere in una
classe astratta e farla ereditare da tutte le classi che ne necessitano le
funzionalità.
Possiamo pensare alle classi astratte come ad un insieme di procedure
(proprietà, metodi, membri) che sono 'globali' ad un determinato gruppo di
classi senza dover essere necessariamente globali a tutto il progetto.