|
Scopo delle Interfacce
Le interfacce sono un'entità davvero singolare all'interno del .NET Framework. La loro funzione è assimilabile a quella delle
classi astratte, ma il modo con cui esse la svolgono è molto diverso da ciò che abbiamo visto nel capitolo precedente.
Il principale scopo di un'interfaccia è definire lo scheletro di una classe; potrebbe essere scherzosamente assimilata alla ricetta
con cui si prepara un dolce. Quello che l'interfaccia X fa, ad esempio, consiste nel dire che per costruire una classe Y che rispetti
"la ricetta" descritta in X servono una proprietà Id di tipo Integer, una funzione GetSomething senza parametri che restituisce
una stringa e una procedura DoSomething con un singolo parametro Double. Tutte le classi che
avranno intenzione di seguire i precetti di X (in gergo implementare X) dovranno definire, allo stesso modo, quella proprietà
di quel tipo e quei metodi con quelle specifiche signature (il nome ha importanza relativa).
Faccio subito un esempio. Fino ad ora, abbiamo visto essenzialmente due tipi di collezione: gli Array e gli ArrayList. Sia per l'uno che
per l'altro, ho detto che è possibile eseguire un'iterazione con il costrutto For Each:
Dim Ar() As Int32 = {1, 2, 3, 4, 5, 6}
Dim Al As New ArrayList
For I As Int32 = 1 To 40
Al.Add(I)
Next
'Stampa i valori di Ar:
For Each K As Int32 In Ar
Console.WriteLine(K)
Next
'Stampa i valori di Al
For Each K As Int32 In Al
Console.WriteLine(K)
Next
Ma il sistema come fa a sapere che Ar e Al sono degli insiemi di valori? Dopotutto, il loro nome è significativo solo per noi programmatori,
mentre per il calcolatore non è altro che una sequenza di caratteri. Allo stesso modo, il codice di Array e ArrayList, definito dai
programmatori che hanno scritto il Framework, è intelligibile solo agli uomini, perchè al computer non comunica nulla sullo
scopo per il quale è stato scritto. Allora, siamo al punto di partenza: nelle classi Array e ArrayList non c'è nulla che possa
far "capire" al programma che quelli sono a tutti gli effetti delle collezioni e che, quindi, sono iterabili; e, anche se in qualche strano
modo l'elaboratore lo potesse capire, non "saprebbe" (in quanto entità non senziente) come far per estrarre singoli dati e darceli
uno in fila all'altro. Ecco che entrano in scena le interfacce: tutte le classi che rappresentano un insieme o una collezione di elementi
implementano l'interfaccia IEnumerable, la quale, se potesse parlare, direbbe "Guarda che questa classe è una collezione,
trattala di conseguenza!". Questa interfaccia obbliga le classi dalle quali è implementata a definire alcuni metodi che servono
per l'enumerazione (Current, MoveNext e Reset) e che vedremo nei prossimi capitoli.
In conclusione, quindi, il For Each prima di tutto controlla che l'oggetto posto
dopo la clausola "In" implementi l'interfaccia IEnumerable. Quindi richiama il metodo Reset per porsi sul primo elemento, poi deposita
in K il valore esposto dalla proprietà Current, esegue il codice contenuto nel proprio corpo e, una volta arrivato a Next, esegue
il metodo MoveNext per avanzare al prossimo elemento. Il For Each "è sicuro" dell'esistenza di questi membri perchè l'interfaccia
IEnumerable ne impone la definizione.
Riassumendo, le interfacce hanno il compito di informare il sistema su quali siano le caratteristiche e i compiti di una classe. Per questo
motivo, il loro nomi terminano spesso in "-able", come ad esempio IEnumerable, IEquatable, IComprable, che ci dicono "- è enumerabile",
"- è eguagliabile", "- è comparabile", "è ... qualcosa".
Dichiarazione e implementazione
La sintassi usata per dichiarare un'interfaccia è la seguente:
Interface [Nome]
'Membri
End Interface
I membri delle interfacce, tuttavia, sono un po' diversi dai membri di una classe, e nello scriverli bisogna rispettare queste regole:
- Nel caso di metodi, proprietà od eventi, il corpo non va specificato;
- Non si possono mai usare gli specificatori di accesso;
- Si possono comunque usare dei modificatori come Shared, ReadOnly e WriteOnly.
Il primo ed il secondo punto saranno ben compresi se ci si sofferma a pensare che l'interfaccia ha il solo scopo di definire quali membri
una classe debba implementare: per questo motivo, non se ne può scrivere il corpo, dato che spetta espressamente alle
classi implementanti, e non ci si preoccupa dello specificatore di accesso, dato che si sta specificando solo il "cosa" e non il "come".
Ecco alcuni semplici esempi di dichiarazioni:
'Questa interfaccia dal nome improbabile indica che
'la classe che la implementa rappresenta qualcosa di
'"identificabile" e per questo espone una proprietà Integer Id
'e una funzione ToString. Id e ToString, infatti, sono gli
'elementi più utili per identificare qualcosa, prima in
'base a un codice univoco e poi grazie ad una rappresentazione
'comprensibile dall'uomo
Interface IIdentifiable
ReadOnly Property Id() As Int32
Function ToString() As String
End Interface
'La prossima interfaccia, invece, indica qualcosa di resettabile
'e obbliga le classi implementanti a esporre il metodo Reset
'e la proprietà DefaultValue, che dovrebbe rappresentare
'il valore di default dell'oggetto. Dato che non sappiamo ora
'quali classi implementeranno questa interfaccia, dobbiamo
'per forza usare un tipo generico come Object per rappresentare
'un valore reference. Vedremo come aggirare questo ostacolo
'fra un po', con i Generics
Interface IResettable
Property DefaultValue() As Object
Sub Reset()
End Interface
'Come avete visto, i nomi di interfaccia iniziano per convenzione
'con la lettera I maiuscola
Ora che sappiamo come dichiarare un'interfaccia, dobbiamo scoprire come usarla. Per implementare un'interfaccia in una classe, si usa
questa sintassi:
Class Example
Implements [Nome Interfaccia]
[Membro] Implements [Nome Interfaccia].[Membro]
End Class
Si capisce meglio con un esempio:
Module Module1
Interface IIdentifiable
ReadOnly Property Id() As Int32
Function ToString() As String
End Interface
'Rappresenta un pacco da spedire
Class Pack
'Implementa l'interfaccia IIdentifiable, in quanto un pacco
'dovrebbe poter essere ben identificato
Implements IIdentifiable
'Notate bene che l'interfaccia ci obbliga a definire una
'proprietà, ma non ci obbliga a definire un campo
'ad essa associato
Private _Id As Int32
Private _Destination As String
Private _Dimensions(2) As Single
'La classe definisce una proprietà id di tipo Integer
'e la associa all'omonima presente nell'interfaccia in
'questione. Il legame tra questa proprietà Id e quella
'presenta nell'interfaccia è dato solamente dalla
'clausola (si chiama così in gergo) "Implements",
'la quale avvisa il sistema che il vincolo imposto
'è stato soddisfatto.
'N.B.: il fatto che il nome di questa proprietà sia uguale
'a quella definita in IIdentifiable non significa nulla.
'Avremmo potuto benissimo chiamarla "Pippo" e associarla
'a Id tramite il codice "Implements IIdentifiable.Id", ma
'ovviamente sarebbe stata una palese idiozia XD
Public ReadOnly Property Id() As Integer Implements IIdentifiable.Id
Get
Return _Id
End Get
End Property
'Destinazione del pacco.
'Il fatto che l'interfaccia ci obblighi a definire quei due
'membri non significa che non possiamo definirne altri
Public Property Destination() As String
Get
Return _Destination
End Get
Set(ByVal value As String)
_Destination = value
End Set
End Property
'Piccolo ripasso delle proprietà indicizzate e
'della gestione degli errori
Public Property Dimensions(ByVal Index As Int32) As Single
Get
If (Index >= 0) And (Index < 3) Then
Return _Dimensions(Index)
Else
Throw New IndexOutOfRangeException()
End If
End Get
Set(ByVal value As Single)
If (Index >= 0) And (Index < 3) Then
_Dimensions(Index) = value
Else
Throw New IndexOutOfRangeException()
End If
End Set
End Property
Public Overrides Function ToString() As String Implements IIdentifiable.ToString
Return String.Format("{0}: Pacco {1}x{2}x{3}, Destinazione: {4}", _
Me.Id, Me.Dimensions(0), Me.Dimensions(1), _
Me.Dimensions(2), Me.Destination)
End Function
End Class
Sub Main()
'...
End Sub
End Module
Ora che abbiamo implementato l'interfaccia nella classe Pack, tuttavia, non sappiamo che farcene. Siamo a conoscenza del fatto che gli oggetti
Pack saranno sicuramente identificabili, ma nulla di più. Ritorniamo, allora, all'esempio del primo paragrafo: cos'è che
rende veramente utile IEnumerable, al di là del fatto di rendere funzionante il For Each? Si applica a qualsiasi collezione o insieme,
non importa di quale natura o per quali scopi, non importa nemmeno il codice che sottende all'enumerazione: l'importante è che
una vastissima gamma di oggetti possano essere ricondotti ad un solo archetipo (io ne ho nominati solo due, ma ce ne sono a iosa). Allo
stesso modo, potremo usare IIdentifiable per manipolare una gran quantità di dati di natura differente. Ad esempio, il codice di sopra
potrebbe essere sviluppato per creare un sistema di gestione di un ufficio postale. Eccone un esempio:
Module Module1
Interface IIdentifiable
ReadOnly Property Id() As Int32
Function ToString() As String
End Interface
Class Pack
Implements IIdentifiable
Private _Id As Int32
Private _Destination As String
Private _Dimensions(2) As Single
Public ReadOnly Property Id() As Integer Implements IIdentifiable.Id
Get
Return _Id
End Get
End Property
Public Property Destination() As String
Get
Return _Destination
End Get
Set(ByVal value As String)
_Destination = value
End Set
End Property
Public Property Dimensions(ByVal Index As Int32) As Single
Get
If (Index >= 0) And (Index < 3) Then
Return _Dimensions(Index)
Else
Throw New IndexOutOfRangeException()
End If
End Get
Set(ByVal value As Single)
If (Index >= 0) And (Index < 3) Then
_Dimensions(Index) = value
Else
Throw New IndexOutOfRangeException()
End If
End Set
End Property
Sub New(ByVal Id As Int32)
_Id = Id
End Sub
Public Overrides Function ToString() As String Implements IIdentifiable.ToString
Return String.Format("{0:0000}: Pacco {1}x{2}x{3}, Destinazione: {4}", _
Me.Id, Me.Dimensions(0), Me.Dimensions(1), _
Me.Dimensions(2), Me.Destination)
End Function
End Class
Class Telegram
Implements IIdentifiable
Private _Id As Int32
Private _Recipient As String
Private _Message As String
Public ReadOnly Property Id() As Integer Implements IIdentifiable.Id
Get
Return _Id
End Get
End Property
Public Property Recipient() As String
Get
Return _Recipient
End Get
Set(ByVal value As String)
_Recipient = value
End Set
End Property
Public Property Message() As String
Get
Return _Message
End Get
Set(ByVal value As String)
_Message = value
End Set
End Property
Sub New(ByVal Id As Int32)
_Id = Id
End Sub
Public Overrides Function ToString() As String Implements IIdentifiable.ToString
Return String.Format("{0:0000}: Telegramma per {1} ; Messaggio = {2}", _
Me.Id, Me.Recipient, Me.Message)
End Function
End Class
Class MoneyOrder
Implements IIdentifiable
Private _Id As Int32
Private _Recipient As String
Private _Money As Single
Public ReadOnly Property Id() As Integer Implements IIdentifiable.Id
Get
Return _Id
End Get
End Property
Public Property Recipient() As String
Get
Return _Recipient
End Get
Set(ByVal value As String)
_Recipient = value
End Set
End Property
Public Property Money() As Single
Get
Return _Money
End Get
Set(ByVal value As Single)
_Money = value
End Set
End Property
Sub New(ByVal Id As Int32)
_Id = Id
End Sub
Public Overrides Function ToString() As String Implements IIdentifiable.ToString
Return String.Format("{0:0000}: Vaglia postale per {1} ; Ammontare = {2}?", _
Me.Id, Me.Recipient, Me.Money)
End Function
End Class
'Classe che elabora dati di tipo IIdentifiable, ossia qualsiasi
'oggetto che implementi tale interfaccia
Class PostalProcessor
'Tanto per tenersi allenati coi delegate, ecco una
'funzione delegate che funge da filtro per i vari id
Public Delegate Function IdSelector(ByVal Id As Int32) As Boolean
Private _StorageCapacity As Int32
Private _NextId As Int32 = 0
'Un array di interfacce. Quando una variabile viene
'dichiarata come di tipo interfaccia, ciò
'che può contenere è qualsiasi oggetto
'che implementi quell'interfaccia. Per lo stesso
'discorso fatto nel capitolo precedente, noi
'possiamo vedere attraverso l'interfaccia
'solo quei membri che essa espone direttamente, anche
'se il contenuto vero e proprio è qualcosa
'di più
Private Storage() As IIdentifiable
'Capacità del magazzino. Assumeremo che tutti
'gli oggetti rappresentati dalle classi Pack, Telegram
'e MoneyOrder vadano in un magazzino immaginario che,
'improbabilmente, riserva un solo posto per ogni
'singolo elemento
Public Property StorageCapacity() As Int32
Get
Return _StorageCapacity
End Get
Set(ByVal value As Int32)
_StorageCapacity = value
ReDim Preserve Storage(value)
End Set
End Property
'Modifica od ottiene un riferimento all'Index-esimo
'oggetto nell'array Storage
Public Property Item(ByVal Index As Int32) As IIdentifiable
Get
If (Index >= 0) And (Index < Storage.Length) Then
Return Me.Storage(Index)
Else
Throw New IndexOutOfRangeException()
End If
End Get
Set(ByVal value As IIdentifiable)
If (Index >= 0) And (Index < Storage.Length) Then
Me.Storage(Index) = value
Else
Throw New IndexOutOfRangeException()
End If
End Set
End Property
'Restituisce la prima posizione libera nell'array
'Storage. Anche se in questo esempio non l'abbiamo
'contemplato, gli elementi possono anche essere rimossi
'e quindi lasciare un posto libero nell'array
Public ReadOnly Property FirstPlaceAvailable() As Int32
Get
For I As Int32 = 0 To Me.Storage.Length - 1
If Me.Storage(I) Is Nothing Then
Return I
End If
Next
Return (-1)
End Get
End Property
'Tutti gli oggetti che inizializzeremo avranno bisogno
'di un id: ce lo fornisce la stessa classe Processor
'tramite questa proprietà che si autoincrementa
Public ReadOnly Property NextId() As Int32
Get
_NextId += 1
Return _NextId
End Get
End Property
'Due possibili costruttori: uno che accetta un insieme
'già formato di elementi...
Public Sub New(ByVal Items() As IIdentifiable)
Me.Storage = Items
_SorageCapacity = Items.Length
End Sub
'... e uno che accetta solo la capacità del magazzino
Public Sub New(ByVal Capacity As Int32)
Me.StorageCapacity = Capacity
End Sub
'Stampa a schermo tutti gli elementi che la funzione
'contenuta nel parametro Selector di tipo delegate
'considera validi (ossia tutti quelli per cui
'Selector.Invoke restituisce True)
Public Sub PrintByFilter(ByVal Selector As IdSelector)
For Each K As IIdentifiable In Storage
If K Is Nothing Then
Continue For
End If
If Selector.Invoke(K.Id) Then
Console.WriteLine(K.ToString())
End If
Next
End Sub
'Stampa l'oggetto con Id specificato
Public Sub PrintById(ByVal Id As Int32)
For Each K As IIdentifiable In Storage
If K Is Nothing Then
Continue For
End If
If K.Id = Id Then
Console.WriteLine(K.ToString())
Exit For
End If
Next
End Sub
'Cerca tutti gli elementi che contemplano all'interno
'della propria descrizione la stringa Str e li
'restituisce come array di Id
Public Function SearchItems(ByVal Str As String) As Int32()
Dim Temp As New ArrayList
For Each K As IIdentifiable In Storage
If K Is Nothing Then
Continue For
End If
If K.ToString().Contains(Str) Then
Temp.Add(K.Id)
End If
Next
Dim Result(Temp.Count - 1) As Int32
For I As Int32 = 0 To Temp.Count - 1
Result(I) = Temp(I)
Next
Temp.Clear()
Temp = Nothing
Return Result
End Function
End Class
Private Processor As New PostalProcessor(10)
Private Cmd As Char
Private IdFrom, IdTo As Int32
Function SelectId(ByVal Id As Int32) As Boolean
Return (Id >= IdFrom) And (Id <= IdTo)
End Function
Sub InsertItems(ByVal Place As Int32)
Console.WriteLine("Scegliere la tipologia di oggetto:")
Console.WriteLine(" p - pacco;")
Console.WriteLine(" t - telegramma;")
Console.WriteLine(" v - vaglia postale;")
Cmd = Console.ReadKey().KeyChar
Console.Clear()
Select Case Cmd
Case "p"
Dim P As New Pack(Processor.NextId)
Console.WriteLine("Pacco - Id:{0:0000}", P.Id)
Console.Write("Destinazione: ")
P.Destination = Console.ReadLine
Console.Write("Larghezza: ")
P.Dimensions(0) = Console.ReadLine
Console.Write("Lunghezza: ")
P.Dimensions(1) = Console.ReadLine
Console.Write("Altezza: ")
P.Dimensions(2) = Console.ReadLine
Processor.Item(Place) = P
Case "t"
Dim T As New Telegram(Processor.NextId)
Console.WriteLine("Telegramma - Id:{0:0000}", T.Id)
Console.Write("Destinatario: ")
T.Recipient = Console.ReadLine
Console.Write("Messaggio: ")
T.Message = Console.ReadLine
Processor.Item(Place) = T
Case "v"
Dim M As New MoneyOrder(Processor.NextId)
Console.WriteLine("Vaglia - Id:{0:0000}", M.Id)
Console.Write("Beneficiario: ")
M.Recipient = Console.ReadLine
Console.Write("Somma: ")
M.Money = Console.ReadLine
Processor.Item(Place) = M
Case Else
Console.WriteLine("Comando non riconosciuto.")
Console.ReadKey()
Exit Sub
End Select
Console.WriteLine("Inserimento eseguito!")
Console.ReadKey()
End Sub
Sub ProcessData()
Console.WriteLine("Selezionare l'operazione:")
Console.WriteLine(" c - cerca;")
Console.WriteLine(" v - visualizza;")
Cmd = Console.ReadKey().KeyChar
Console.Clear()
Select Case Cmd
Case "c"
Dim Str As String
Console.WriteLine("Inserire la parola da cercare:")
Str = Console.ReadLine
Dim Ids() As Int32 = Processor.SearchItems(Str)
Console.WriteLine("Trovati {0} elementi. Visualizzare? (y/n)", Ids.Length)
Cmd = Console.ReadKey().KeyChar
Console.WriteLine()
If Cmd = "y" Then
For Each Id As Int32 In Ids
Processor.PrintById(Id)
Next
End If
Case "v"
Console.WriteLine("Visualizzare gli elementi")
Console.Write("Da Id: ")
IdFrom = Console.ReadLine
Console.Write("A Id: ")
IdTo = Console.ReadLine
Processor.PrintByFilter(AddressOf SelectId)
Case Else
Console.WriteLine("Comando sconosciuto.")
End Select
Console.ReadKey()
End Sub
Sub Main()
Do
Console.WriteLine("Gestione ufficio")
Console.WriteLine()
Console.WriteLine("Selezionare l'operazione da effettuare:")
Console.WriteLine(" i - inserimento oggetti;")
Console.WriteLine(" m - modifica capacità magazzino;")
Console.WriteLine(" p - processa i dati;")
Console.WriteLine(" e - esci.")
Cmd = Console.ReadKey().KeyChar
Console.Clear()
Select Case Cmd
Case "i"
Dim Index As Int32 = Processor.FirstPlaceAvailable
Console.WriteLine("Inserimento oggetti in magazzino")
Console.WriteLine()
If Index > -1 Then
InsertItems(Index)
Else
Console.WriteLine("Non c'è più spazio in magazzino!")
Console.ReadKey()
End If
Case "m"
Console.WriteLine("Attuale capacità: " & Processor.StorageCapacity)
Console.WriteLine("Inserire una nuova dimensione: ")
Processor.StorageCapacity = Console.ReadLine
Console.WriteLine("Operazione effettuata.")
Console.ReadKey()
Case "p"
ProcessData()
End Select
Console.Clear()
Loop Until Cmd = "e"
End Sub
End Module
Avevo in mente di definire anche un'altra interfaccia, IPayable, per calcolare anche il costo di spedizione di ogni pezzo: volevo far notare
come, sebbene il costo vada calcolato in maniera diversa per i tre tipi di oggetto (in base alle dimensioni per il pacco, in base al numero
di parole per il telegramma e in base all'ammontare inviato per il vaglia), bastasse richiamare una funzione attraverso l'interfaccia
per ottenere il risultato. Poi ho considerato che un esempio di 400 righe era già abbastanza. Ad ogni modo, userò adesso
quel'idea in uno spezzone tratto dal programma appena scritto per mostrare l'uso di interfacce multiple:
Module Module1
'...
Interface IPayable
Function CalculateSendCost() As Single
End Interface
Class Telegram
'Nel caso di più interfacce, le si separa con la virgola
Implements IIdentifiable, IPayable
'...
Public Function CalculateSendCost() As Single Implements IPayable.CalculateSendCost
'Come vedremo nel capitolo dedicato alle stringhe,
'la funzione Split(c) spezza la stringa in tante
'parti, divise dal carattere c, e le restituisce
'sottoforma di array. In questo caso, tutte le sottostringhe
'separate da uno spazio sono all'incirca tante
'quanto il numero di parole nella frase
Select Case Me.Message.Split(" ").Length
Case Is <= 20
Return 4.39
Case Is <= 50
Return 6.7
Case Is <= 100
Return 10.3
Case Is <= 200
Return 19.6
Case Is <= 500
Return 39.75
End Select
End Function
End Class
'...
End Class
Definizione di tipi in un'interfaccia
Così come è possibile dichiarare una nuova classe all'interno di un'altra, o una struttura in una classe, o un'interfaccia
in una classe, o una struttura in una struttura, o tutte le altre possibili combinazioni, è anche possibile dichiarare un nuovo
tipo in un'interfaccia. In questo caso, solo le classi che implementeranno quell'interfaccia saranno in grado di usare quel tipo.
Ad esempio:
Interface ISaveable
Structure FileInfo
'Assumiamo per brevità che queste variabili Public
'siano in realtà proprietà
Public Path As String
'FileAttribues è un enumeratore su bit che contiene
'informazioni sugli attributi di un file (nascosto, a sola
'lettura, archivio, compresso, eccetera...)
Public Attributes As FileAttributes
End Structure
Property SaveInfo() As FileInfo
Sub Save()
End Interface
Class A
Private _SaveInfo As ISaveable.FileInfo 'SBAGLIATO!
'...
End Class
Class B
Implements ISaveable
Private _SaveInfo As ISaveable.FileInfo 'GIUSTO
'...
End Class
Ereditarietà, polimorfismo e overloading per le interfacce
Anche le interfacce possono ereditare da un'altra interfaccia base. In questo caso, dato che in un'interfaccia non si possono usare specificatori
di accesso, la classe derivata acquisisce tutti i membri di quella base:
Interface A
Property PropA() As Int32
End Interface
Interface B
Inherits A
Sub SubB()
End Interface
Non si può usare il polimorfismo perchè non c'è nulla da ridefinire, in quanto i metodi non hanno un corpo.
Si può, invece, usare l'overloading come si fa di consueto: non ci sono differenze significative in questo ambito.
Perchè preferire un'interfaccia a una classe astratta
Ci sono casi e casi. In genere, un'interfaccia va preferita quando si dovranno abbracciare grandi quantità di classi diverse: con
grandi mi riferisco a numeri abbastanza grandi da rendere difficile l'uso di una sola classe astratta. È bene usare le interfacce
anche quando si progetta di usarne più di una: una classe, infatti, può implementare quante interfacce vuole, ma può
ereditare da una sola classe base.
Proprio per questi motivi, è ideale usare un'interfaccia quando si vuole delineare una caratteristica o un particolare comportamento
che può essere comune a più tipi, mentre è meglio puntare a una classe astratta quando si vuole sottolineare
l'appartenenza di molti tipi ad un unico archetipo base.
|
|