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) NextMa 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 InterfaceI 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.
'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 maiuscolaOra 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 ClassSi 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 ModuleOra 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 ModuleAvevo 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 InterfaceNon 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.
A cura di: Il Totem