| Introduzione alle classi |
| |
E' ormai da tempo che l'avvento della programmazione orientata agli
oggetti ha fatto irruzione nel mondo dell'industria informatica e
molti programmatori, abituati al vecchio stile, si sono dovuti arrangiare
per sfruttare questo importante paradigma. I giovani sviluppatori,
se non opportunamente preparati, fanno fatica ad apprendere il pensiero
che sta alla base ed i programmatori abituati al vecchio stile hanno
sudato assai per essere al passo con i tempi.
In questo appuntamento affronteremo insieme alcuni degli arcani segreti
che la programmazione orientata agli oggetti, detta più brevemente
OO (Object Oriented), vela ai nostri occhi e cercheremo di comprendere
quali sono i suoi vantaggi. Conclusa la lettura, troverete anche il
progetto VB documentato utilizzato per gli esempi di questo articolo.
Cominciamo!
Molti libri danno una propria definizione per che cosa sia una classe,
ma mi astengo per ora da tale pratica. Sarebbe utile capire da dove
si è partiti e qual è l'attuale traguardo. Agli albori
dello sviluppo, quando lo spaghetti code spopolava ovunque, per fare
i calcoli si utilizzavano i registri del processore, i programmatori
assembler sanno bene quanto sia noioso fare il debug con così
poca astrazione. In seguito, nei linguaggi più evoluti, sono
stati introdotti i tipi base : l'intero, il numerico in virgola mobile
a singola e doppia precisione e così via. Era anche necessario
accorpare i tipi base in modo da esprimere entità omogenee
di elementi e questa astrazione fu raggiunta con i vettori (array).
In questo scenario evolutivo fecero anche la loro comparsa le entità
eterogenee come le strutture, che tutti noi conosciamo come tipi definiti
dall'utente. Fino a questo punto nulla di strano direte voi, ma c'è
da notare che negli anni si è sempre ben distinto il ruolo
dei dati da quello delle funzioni che operavano su di essi.
La programmazione tradizionale utilizza una
tecnica di sviluppo funzionale o procedurale,
che fa una netta distinzione dei dati rispetto alle
funzioni. Un intero programma è strutturato in moduli
che accedono ad uno o più gruppi di dati. Lo sviluppo funzionale
ha, però, diversi svantaggi:
- Lo stretto legame che sussiste
tra le funzioni ed i dati porta a situazioni in cui singole
modifiche si ripercuotono su diverse parti del programma, aumentando
la difficoltà di debug ed evidenziando i limiti di tale approccio
per uno sviluppo incrementale del software. Per sviluppo incrementale
si intende l'evoluzione di un sistema (programma) grazie a modifiche
successive che correggono i difetti delle prime versioni ed aumentano
le funzionalità delle versioni aggiornate.
- Difficoltà di riutilizzo del software.
Ogni volta che si vuole riciclare una funzione è necessario
apportare delle modifiche strutturali per adattarla alla nuova applicazione.
Lo sviluppo procedurale è prevalso per molti anni, finché
alcune brillanti menti non fecero l'azzardo di accorpare i dati e
le funzioni in un'unica entità. Questa entità prende
il nome di Classe ed apre nuovi orizzonti per lo sviluppo e la progettazione
del software.
Ogni giorno utilizziamo oggetti più o meno complessi senza
renderci conto della loro tecnologia interna, quello che ci interessa
è la parte esterna che percepiamo. Ad esempio una sveglia ha
una propria tecnologia interna, che in genere
non ci interessa sapere perché utilizziamo l'interfaccia
esterna, insieme di pulsanti con varie funzionalità.
Una classe definisce la tecnologia interna degli oggetti e l'interfaccia
che questi avranno verso l'esterno. Quando alla mattina, sotto le
calde coperte, ci sentiamo chiamare dalla sveglia, l'istinto di sopravvivenza
porta alla ricerca del punto magico in cui la sveglia ha l'interruttore
di spegnimento, cioè quella parte della sua interfaccia che
la zittisce. Così, come accade per gli oggetti reali anche
per gli oggetti nella programmazione si ha una diretta interazione
solo con le interfacce pubbliche, che fin
ora sono state nominate come esterne.
Le classi permettono di formalizzare la struttura degli oggetti che
si desidera rappresentare all'interno di un programma/sistema, da
qui il nome di programmazione orientata agli oggetti.
Una classe si compone essenzialmente di due parti: -
dati - funzioni
I dati di una classe sono chiamati attributi
o membri, mentre le funzioni prendono il nome di metodi.
Ogni metodo agisce esclusivamente sugli attributi della classe di
appartenenza. Questo vincolo rappresenta la grande forza della programmazione
ad oggetti, la quale costringe il programmatore ad organizzare il
software per componenti riciclabili ben distinti (classi). Una possibile
dichiarazione degli attributi di una classe è la seguente: |
| |
Dim prAttributo1 As Integer Dim prAttributo2 AS String Dim prAttributo3 As Single |
|
| |
La dichiarazione degli attributi interna alla classe prende il nome
di incapsulamento dei dati. Esso consiste
nella protezione degli attributi dagli accessi non desiderati.
Un modo più efficace di esprimere gli attributi si raggiunge
introducendo le proprietà che accedono
alle variabili poch'anzi dichiarate. Ad esempio:
|
| |
Property Let Attributo1(vNewValue As Integer)
prAttributo1 = vNewValue
End Property
Property Get Attributo1()
Attributo1 = prAttributo1
End Property |
|
| |
Come potete notare le proprietà ci permettono di qualificare
un attributo come sola lettura Get, sola
scrittura Let (Set se si tratta di variabili oggetto) oppure
entrambe, lettura e scrittura. E' buona pratica utilizzare le proprietà
per esprimere gli attributi, se non si segue tale regola si rischia
di realizzare codice che non sfrutta in modo proficuo la programmazione
orientata agli oggetti.
I metodi si dichiarano esattamente come le funzioni e le procedure.
|
| |
Sub Metodo1()
'qui va il codice
End Sub
Function Metodo2() AS Integer
'qui va il codice
End Function |
|
| |
| Sia gli attributi sia i metodi hanno un ambito
di validità (scope). Per gli attributi così come
per i metodi gli ambiti sono tre: pubblico,
privato e protetto.
Ecco alcuni esempi che riguardano le proprietà. |
| |
Private prAttributo1 As Integer
Private prAttributo2 As Integer
Private prAttributo3 As Integer
Public Property Let Attributo1(vNewValue As Integer)
prAttributo1 = vNewValue
End Property
Public Property Get Attributo1()
Attributo1 = prAttributo1
End Property
Private Property Let Attributo2(vNewValue As Integer)
prAttributo2 = vNewValue
End Property
Private Property Get Attributo2()
Attributo2 = prAttributo2
End Property
Friend Property Let Attributo3(vNewValue As Integer)
prAttributo3 = vNewValue
End Property
Friend Property Get Attributo3()
Attributo3 = prAttributo3
End Property |
|
| |
Notate che le variabili sono state dichiarate Private,
è tramite le proprietà che controlliamo l'accesso ai
nostri attributi.
Per la dichiarazione dei metodi il codice è il seguente: |
Public Sub Metodo1()
End Sub
Private Sub Metodo2()
End Sub
Friend Sub Metodo3()
End Sub
Public Function Metodo4() As Integer
End Function
Private Function Metodo5() As Integer
End Function
Friend Function Metodo6() As Integer
End Function |
|
| |
| Spendiamo due parole sull'ambito di validità. |
| |
Ambito privato
Le dichiarazioni private, come lascia intuire il nome, hanno visibilità
esclusivamente all'interno del modulo di classe. Se dichiariamo un
membro privato, questo non potrà essere visto all'esterno della
classe e di conseguenza nessuno vi potrà accedere. Con un semplice
esempio possiamo dire che il contenuto della sveglia, cioè
tutti i circuiti elettronici, per noi risulta essere privato, visto
che non possiamo interagire direttamente con i vari dispositivi interni.
Ambito pubblico
Le dichiarazioni pubbliche rappresentano di fatto l'interfaccia che
i nostri oggetti avranno verso l'esterno, una volta che avremo completato
la costruzione della classe. Per fare un confronto con la sveglia,
possiamo dire che i suoi pulsanti rappresentano l'interfaccia pubblica.
Ambito protetto
E' molto simile alla visibilità pubblica, ma con una sostanziale
differenza: i membri protetti (friend) risultano visibili come se
fossero pubblici solo all'interno dello stesso progetto. Questo tipo
di scope è indispensabile per realizzare un colloquio protetto
tra le classi di uno stesso componente, ma in questo articolo non
ne faremo ulteriore menzione.
Dopo questo chiarimento potete intuire che dichiarare una proprietà
Private, come è il caso di Attributo2, non ha molto senso visto
che all'interno della classe possiamo accedere direttamente alla relativa
variabile prAttributo2. Sebbene sia possibile all'interno di una classe
dichiarare le variabili con lo scope Public è fortemente sconsigliata
questa pratica. La ragione sta nel fatto che la tecnologia interna
non deve essere mai direttamente esposta all'esterno.
Per i metodi la situazione è leggermente differente: possiamo
realizzare dei metodi di comodo, che non ci interessa esporre all'esterno
e di conseguenza attribuire a questi lo scope Private. Possiamo ad
esempio avere diversi metodi che producono risultati parziali di una
complessa elaborazione, ma all'esterno tali parziali non sono percepiti
come risultati di valore.
Un caso banale è rappresentato da un metodo che esegue la lettura
di un file un byte alla volta e l'oggetto nel suo complesso deve fornire
il contenuto del file per intero.
Visto che abbiamo citato lo scope a questo punto è possibile
definire le interfacce di una classe. Un'interfaccia di una classe
è l'insieme dei suoi metodi e dei suoi attributi. Più
precisamente un'interfaccia pubblica è l'insieme
dei metodi e degli attributi pubblici, un
interfaccia privata è l'insieme dei metodi e degli attributi
privati ed infine, l'interfaccia protetta
è l'insieme dei metodi e degli attributi protetti. Da
qui possiamo citare uno dei principi fondamentali della programmazione
OO: mascheramento dell'informazione. Questo
principio fa sì che un oggetto sia diviso in due parti ben
distinte: una interfaccia pubblica e una rappresentazione
privata o protetta.
Tornando agli attributi, questi si utilizzano per memorizzare lo stato
di un oggetto, mentre i metodi servono per i compiti di elaborazione.
Dal punto di vista della programmazione, gli attributi, come si è
già visto, sono variabili dichiarate all'interno della classe
alle quali accediamo tramite le proprietà, mentre i metodi
sono rappresentati dalle procedure e dalle funzioni.
Le classi, oltre a quanto descritto, contengono anche un paio di utili
eventi: Inizialize e Terminate.
I due eventi vengono generati rispettivamente all'atto
della creazione dell'oggetto e prima che l'oggetto venga distrutto.
Se non avete confidenza con il concetto di evento (sicuramente sì,
ma a scanso di equivoci ^_^) dovete immaginare una procedura che viene
eseguita in "automatico" al verificarsi di una certa condizione.
Ad esempio l'evento click di un pulsante viene generato quando l'utente
esegue un click sul pulsante che è appunto la condizione grazie
alla quale l'evento viene attivato. Per ora accontentatevi di queste
poche righe, più avanti faremo una panoramica più approfondita
sull'argomanto.
I concetti teorici si chiariscono spesso vedendo un esempio pratico.
Per rappresentare la figura geometrica rettangolo realizziamo l'omonima
classe.
Un rettangolo è caratterizzato da due attributi principali:
altezza e base.
Per realizzare una classe in VB è sufficiente creare un modulo
di classe all'interno del nostro progetto. Un modulo di classe
non è molto diverso da un modulo standard e di conseguenza
per i novizi non ci dovrebbero essere grosse difficoltà durante
l'inserimento del codice. Gli attributi della classe sono le caratteristiche
che abbiamo poch'anzi elencato e li rappresentiamo sotto forma di
variabili. |
| |
Private prAltezza As Single Private prBase As Single |
|
| |
Come avrete notato ho utilizzato la visibilità privata per
entrambi gli attributi. Questa scelta, come già sottolineato,
ci permette di separare nettamente la tecnologia interna degli oggetti
dall'interfaccia che questi avranno verso l'esterno. E' buona pratica
dichiarare tutti gli attributi con la visibilità privata. Per
le variabili si è utilizzato il prefisso pr per sottolineare
che sono private. Non seguire questa regola equivale, nel mondo reale,
alla realizzazione di una sveglia con dei fili che penzolano all'esterno.
Non è un bel vedere ^_^
Per leggere e scrivere i valori degli attributi privati, realizziamo
le apposite proprietà pubbliche Let e Get. Il caso in cui un
attributo si riferisce ad una variabile oggetto, si deve fare uso
di una proprietà Set (Public Property Set MioOggetto(ByVal
vNewValue As Oggetto)) al posto della proprietà Let. |
| |
Public Property Get Altezza() As Single
Altezza = prAltezza
End Property
Public Property Let Altezza(ByVal vNewValue As Single)
prAltezza = vNewValue
End Property
Public Property Get Base() As Single
Base = prBase
End Property
Public Property Let Base(ByVal vNewValue As Single)
prBase = vNewValue
End Property |
|
| |
Quello che abbiamo creato è la classe Rettangolo che permette
di impostare e di leggere i propri attributi Altezza e Base grazie
all'interfaccia pubblica costituita dalle proprietà pubbliche.
Notate che ogni proprietà è in lettura e scrittura (Get
e Let). Se volessimo realizzare una proprietà in sola lettura
ci basterebbe rimuovere la relativa proprietà Let/Set.
A partire da questa classe possiamo generare una miriade di oggetti
che hanno le caratteristiche di un rettangolo.
Per creare gli oggetti di classe Rettangolo la procedura è
semplice. Dichiariamo una variabile riferimento, inizializziamo
la variabile con un oggetto creato al volo e quando l'oggetto non
ci serve più lo buttiamo via. Possiamo scrivere il seguente
codice di prova all'interno dell'evento click di un pulsante. |
| |
Dim RettangoloDiProva As Rettangolo
Set RettangoloDiProva = New Rettangolo
RettangoloDiProva.Altezza = 2
RettangoloDiProva.Base = 3
MsgBox "Area rettangolo: " & RettangoloDiProva.Base * RettangoloDiProva.Altezza
Set RettangoloDiProva = Nothing |
|
| |
Prima particolarità è la presenza dell'istruzione
New, che fa un compito semplicissimo: crea
in memoria un oggetto di classe specificata (istanza della
classe). L'oggetto creato viene assegnato ad un riferimento tramite
il quale ci possiamo riferire all'interfaccia pubblica dell'oggetto.
La seconda sta nell'uso del valore particolare Nothing.
Quando si assegna il valore Nothing ad un riferimento, questo viene
annullato e l'oggetto al quale si riferiva viene cestinato da un particolare
meccanismo detto garbage collector. In Visual Basic le risorse occupate
da un oggetto vengono completamente rilasciate quando non c'è
nessun riferimento che punta ad esso. In parole povere: se nessuno
si ricorda dov'è l'oggetto, questo viene buttato via in automatico.
I lettori più attenti si saranno accorti che la classe Rettangolo
non definisce nessun metodo, in altre parole non esegue elaborazioni
sugli attributi. Per supplire a tale mancanza introduciamo il metodo
Area che calcola l'area del rettangolo. |
| |
Public Function Area() as Single
Area = prBase * prAltezza
End Function |
|
| |
| Ecco fatto, ora possiamo ottenere l'area di tutti i rettangoli senza
dover fare nessun calcolo diretto con gli attributi. Il codice di
prova diventa: |
| |
Dim RettangoloDiProva As Rettangolo
Set RettangoloDiProva = New Rettangolo
RettangoloDiProva.Altezza = 2
RettangoloDiProva.Base = 3
MsgBox "Area rettangolo: " & RettangoloDiProva.Area
Set RettangoloDiProva = Nothing |
|
| |
| Visto com'è facile? Volendo possiamo calcolare la lunghezza
delle diagonali interne al rettangolo con quest'altro metodo: |
| |
Public Function Diagonale() As Single
Diagonale = Sqr(prAltezza^2 + prBase^2)
End Function |
|
| |
| Aggiungiamo anche una funzione di test che ci ritorna True se il
rettangolo è un quadrato, False altrimenti. Ed un'altra che
scali l'oggetto in funzione di un fattore di scala specificato. |
| |
Public Function Quadrato() As Boolean
Quadrato = (prAltezza = prBase)
End Function
Public Sub CambiaScala(FattoreScala As Single)
prAltezza = prAltezza * FattoreScala
prBase = prBase * FattoreScala
End Sub |
|
| |
Come avrete capito non c'è limite alle funzionalità
che possiamo aggiungere. Tenete a mente che è buona abitudine
utilizzare le procedure e le funzioni per eseguire le elaborazioni
sugli attributi di un oggetto, mentre le proprietà dovrebbero
servire esclusivamente per validare le assegnazioni e per leggere/scrivere
i valori degli attributi.
Riprendiamo con gli eventi standard della classe che abbiamo lasciato
da parte. La causa scatenante dell'evento Initialize
è la creazione dell'oggetto. |
| |
Dim RettangoloDiProva As Rettangolo
Set RettangoloDiProva = New Rettangolo |
|
| |
| Dualmente, l'evento Terminate si verifica quando l'oggetto viene
distrutto. |
| |
Set RettangoloDiProva = Nothing
|
|
| |
| In relatà in questo caso la richiesta di rilascio delle risorse
è esplicita. Un oggetto viene distrutto anche quando il riferimento
ad esso esce dall'ambito di validità (scope) e non vi sono altri riferimenti
che puntano ad esso. |
| |
Public Sub MySub()
Dim RettangoloDiProva as Rettangolo
Set RettangoloDiProva = New Rettangolo
End Sub
|
|
| |
Lo scopo di questi eventi è relativamente semplice. Initialize
in genere si utilizza per inizializzare le variabili
interne all'oggetto, in modo che questo sia "pronto all'uso".
Terminate è comoda per rilasciare
esplicitamente eventuali risorse allocate. Ad esempio se usiamo
una matrice dinamica di stringhe all'interno della classe è opportuno
che prima di terminare la classe lo spazio allocato venga liberato
(Erase MyStrArray() ).
Ovviamente nulla vieta di crearsi due appositi metodi InitVars
e TearDown e gestire manualmente l'inizializzazione
ed il rilascio delle risorse. In alcuni casi gli automatismi VB non
bastano e quindi ci si deve arrangiare. Ad esempio, possiamo chiamare
i nostri metodi dopo aver creato l'oggetto (InitVars)
e prima di inizializzare il riferimento a Nothing (TearDown).
|
| |
Dim RettangoloDiProva as Rettangolo
Set RettangoloDiProva = New Rettangolo
RettangoloDiProva.InitVars
RettangoloDiProva.TearDown
Set RettangoloDiProva = Nothing
|
|
| |
Anche se implementiamo i nostri metodi ad hoc, l'esecuzione degli
eventi Initialize e Terminate rimane.
Visto che la nostra classe Rettangolo non ha particolari esigenze,
possiamo utilizzare l'evento Initialize per impostare a zero sia l'altezza
sia la base. Tralasciamo l'evento Terminate perché nel caso in analisi
non ha particolare utilità. |
| |
Private Sub Class_Initialize()
'
prAltezza = 1
prBase = 1
End Sub |
|
| |
Visto che siamo alla fine facciamo un breve riassunto di quanto
detto. Le classi racchiudono la definizione degli attributi e dei
metodi, i quali hanno un ambito di validità (scope). Le variabili
relative agli attributi si dichiarano sempre private e si utilizzano
le proprietà per accedervi. I metodi si utilizzano solo per
eseguire elaborazioni e possono essere sia funzioni che procedure.
Le classi definiscono le caratteristiche degli oggetti e di conseguenza
gli oggetti rispecchiano l'implementazione delle classi.
Per la creazione degli oggetti di una determinata classe si utilizza
l'istruzione New. Per utilizzare gli oggetti è necessario un
riferimento al quale assegnare l'oggetto creato. Per rilasciare le
risorse occupate da un oggetto si utilizza il valore Nothing, stando
ben attenti a tutti i riferimenti che puntano allo stesso oggetto.
Per inizializzare le variabili interne possiamo utilizzare l'evento
Initialize e per rilasciare le eventuali risorse allocate l'evento
Terminate.
Qui troverete
il progetto di esempio con la classe che abbiamo realizzato insieme.
Direi che per essere il primo approccio vi potete accontentare, sono
sicuro che non avrete le idee del tutto chiare su come sfruttare questo
tipo di programmazione, ma vi assicuro che con un po' di impegno vi
sarà facile creare programmi fichissimi scritti in logica object
oriented e non vi servirà solo in Visual Basic. Per esercitarvi
potete provare a realizzare delle diverse classi a partire dai componenti
di una bicicletta. Nel prossimo appuntamento continueremo la nostra
esplorazione, nel frattempo esercitatevi finché non sarete
in grado di pensare agli oggetti reali in forma di classi con gli
attributi ed i metodi ^_^. |