di Riccardo Golia - Microsoft MVP
Questo articolo prosegue la carrellata di esempi riguardanti i design pattern della famiglia dei GoF (Gang of Four). Nella prima parte sono stati affrontati i pattern GoF creazionali, ovvero quelli relativi alla creazione di istanze. In questa seconda parte vengono presi in considerazione i pattern GoF strutturali che si riferiscono principalmente alla composizione di classi e oggetti.
Come per i GoF creazionali, anche in questo articolo per ogni pattern viene specificato quanto segue:
-
un’introduzione con la spiegazione delle motivazioni che possono comportare l’uso del design pattern in questione;
-
l’elenco dei partecipanti del pattern con l’indicazione delle rispettive responsabilità e lo schema UML di base (diagramma delle classi);
-
la spiegazione dell’esempio e il relativo schema UML (diagramma delle classi);
-
l’implementazione dell’esempio in C#.
.gif)
In questa pagina
I design pattern strutturali
Adapter
Composite
Facade
Proxy
Conclusioni
Riferimenti
I design pattern strutturali
I design pattern di tipo strutturale riguardano le modalità con cui classi e oggetti vengono aggregati allo scopo di formare entità più complesse. In generale possiamo distinguere due tipologie di pattern strutturali:
-
pattern strutturali basati sulle classi, che sfruttano l’ereditarietà multipla (se prevista) per combinare tra loro le interfacce e le implementazioni;
-
pattern strutturali basati sugli oggetti, che descrivono le modalità di composizione di oggetti al fine di estendere in fase di esecuzione le funzionalità di una classe (cosa non possibile nel caso della composizione statica e tramite ereditarietà).
In generale la maggior parte dei pattern strutturali sono basati sugli oggetti. Il pattern Adapter è un esempio di pattern basato sia sulle classi che sugli oggetti. Dal momento che in .NET Framework i tipi possono derivare da un unico tipo base, lo schema presentato nell’articolo per il pattern in questione e il relativo esempio si riferiscono al caso di composizione basata sugli oggetti, tralasciando la versione basata sulle classi che sfrutta l’ereditarietà multipla.
Adapter
L’Adapter (detto anche Wrapper) è un pattern strutturale basato sulle classi e sugli oggetti che ha lo scopo di convertire l’interfaccia di una classe in un’altra interfaccia. Questo pattern permette a classi diverse di operare insieme anche nel caso in cui questo non sarebbe possibile per via delle interfacce tra loro non compatibili.
Questo pattern trova una forte applicabilità soprattutto in quegli scenari dove oggetti simili appartenenti ad ambienti o sistemi diversi hanno la necessità di interoperare tra loro. In questi casi l’utilizzo di un oggetto wrapper consente di adattare tra loro oggetti strutturalmente organizzati in modo diverso, di farli comunicare tra loro e di metterli in correlazione.
Come detto in precedenza, nell’articolo viene preso in considerazione unicamente il caso di composizione basata sugli oggetti.
I partecipanti di questo pattern sono (tra parentesi sono indicati gli oggetti equivalenti nell’esempio proposto successivamente):
-
Target (Contact)
Definisce l’interfaccia di riferimento alla quale l’oggetto Adaptee si deve adattare.
-
Adaptee (Employee)
Rappresenta l’interfaccia che deve essere adattata.
-
Adapter (EmployeeAdapter)
Adatta l’interfaccia di Adaptee all’interfaccia di Target.
-
Client (Program)
Utilizza unicamente oggetti compatibili con l’interfaccia di Target.
L’esempio proposto per questo pattern include due classi Employee e Customer con interfacce diverse. La classe Employee include due proprietà FirstName e LastName che rappresentano il nome e il cognome dell’impiegato. La classe Customer (cliente) deriva direttamente dalla classe base astratta Contact che include un’unica proprietà FullName che rappresenta il nome completo del contatto, composto dalla concatenazione di nome e cognome. In questo scenario non è possibile assegnare direttamente un’istanza di Employee a un riferimento di tipo Contact. Per poterlo fare, occorre adattare l’interfaccia di Employee a quella di Contact. La classe EmployeeAdapter deriva da Contact e presenta un campo privato di tipo Employee. In fase di creazione l’istanza della classe Employee da adattare viene passata all’adapter come parametro sul costruttore. La proprietà FullName di EmployeeAdapter esegue la concatenazione del nome e del cognome dell’impiegato utilizzando le proprietà dell’oggetto interno.
using System;
using System.Collections.Generic;
using System.Text;
namespace DesignPatterns.Adapter
{
public class Employee
{
private string _firstName;
private string _lastName;
public Employee(string firstName, string lastName)
{
_firstName = firstName; _lastName = lastName;
}
public string FirstName
{
get { return _firstName; }
}
public string LastName
{
get { return _lastName; }
}
}
public abstract class Contact
{
public abstract string FullName { get; }
}
public class Customer : Contact
{
private string _fullName;
public Customer(string fullName)
{
_fullName = fullName;
}
public override string FullName
{
get { return _fullName; }
}
}
public class EmployeeAdapter : Contact
{
private Employee _employee;
public EmployeeAdapter(Employee employee)
{
_employee = employee;
}
public override string FullName
{
get { return _employee.FirstName + " " + _employee.LastName; }
}
}
public class Program
{
public static void Main(string[] args)
{
Contact c = new Customer("Riccardo Golia");
Console.WriteLine(c.FullName);
c = new EmployeeAdapter(new Employee("Riccardo", "Golia"));
Console.WriteLine(c.FullName);
Console.ReadLine();
}
}
}
Composite
Il pattern Composite, di tipo strutturale basato sugli oggetti, consente di creare gerarchie di oggetti aggregando insieme elementi primitivi e compositi direttamente a runtime. Questo pattern può essere applicato per rappresentare strutture ad albero in una logica parte-tutto (part-whole), dove ciascun elemento può a sua volta aggregare insieme altri elementi della stessa specie e dove oggetti singoli e composizioni possono essere trattati in modo uniforme.
Uno dei pregi principali nell’applicazione di questo pattern risiede nel fatto di rendere molto agevole l’aggiunta di nuove tipologie di oggetti componenti. In più, dal momento che i vari elementi vengono trattati in modo uniforme, l’utilizzo della struttura composita nell’ambito del client risulta essere molto semplificata. Ciascun elemento, seppur presentando caratteristiche simili, può essere caratterizzato da comportamenti assai diversi, a seconda dei casi. Il client non conosce la differenza che esiste tra i diversi elementi e, in particolare, tra oggetti primitivi e oggetti compositi, pertanto tratta entrambe le tipologie allo stesso modo, con un significativo miglioramento della leggibilità del codice.
I partecipanti di questo pattern sono:
-
Component (DocumentElement)
Fornisce l’interfaccia di riferimento valida per tutti gli elementi della struttura ad albero, ovvero sia per gli elementi terminali che per gli elementi intermedi. Rappresenta la classe base per Composite (oggetto composito) e per Leaf (oggetto primitivo).
-
Composite (DocumentChapter)
Definisce il comportamento degli elementi intermedi che hanno figli e che aggregano insieme altri Component.
-
Leaf (DocumentParagraph)
Definisce il comportamento degli elementi terminali che non hanno figli e che rappresentano gli oggetti primitivi.
-
Client (Program)
Utilizza la struttura composita, accedendo ai vari elementi tramite l’interfaccia Component.
Consideriamo l’organizzazione di un documento suddiviso in capitoli e paragrafi rappresentati da altrettanti oggetti il cui tipo base è la classe astratta DocumentElement. Sebbene la struttura del documento considerata è volutamente molto semplificata per non introdurre una complessità tale da limitare l’efficacia e la chiarezza dell’esempio, il documento risulta essere un’aggregazione più o meno complessa dei suoi elementi costituenti. Nell’esempio ciascun capitolo (classe DocumentChapter) è un elemento composito che contiene uno o più paragrafi. L’elemento terminale della struttura del documento è rappresentato dal paragrafo (classe DocumentParagraph).
Occorre fare alcune considerazioni. Si noti innanzitutto che, a parità di interfaccia, i diversi oggetti di tipo DocumentElement mostrano comportamenti peculiari a seconda dei casi: l’implementazione interna dell’elemento terminale (il paragrafo) è differente da quella dell’elemento composito (il capitolo). Questo fa sì che, nella rappresentazione del documento, il metodo Write(string)possa essere eseguito in modo iterativo mantenendo il codice molto essenziale e senza la necessità di introdurre strutture particolari di controllo del flusso, dal momento che ogni singola istanza presenta un comportamento particolare in funzione del tipo di appartenenza. Si noti inoltre che nulla vieta che un capitolo possa a sua volta contenere uno o più sottocapitoli sempre di tipo DocumentChapter, dato che gli elementi della lista dei suoi figli sono in ogni caso di tipo DocumentElement (il tipo base). Si noti infine come l’aggiunta di una nuova tipologia di elemento (per esempio, una ipotetica classe DocumentSection) non rappresenti un grosso problema, dal momento che la struttura è pensata per evolvere in modo flessibile nel tempo.
using System;
using System.Collections.Generic;
namespace DesignPatterns.Composite
{
public abstract class DocumentElement
{
public abstract void Add(DocumentElement child);
public abstract void Remove(DocumentElement child);
public abstract void Write();
}
public class DocumentChapter : DocumentElement
{
private int _chapterNumber;
private List<DocumentElement> _children =
new List<DocumentElement>();
public DocumentChapter(int number)
{
_chapterNumber = number;
}
public override void Add(DocumentElement child)
{
_children.Add(child);
}
public override void Remove(DocumentElement child)
{
_children.Remove(child);
}
public override void Write()
{
Console.WriteLine("Chapter " + _chapterNumber.ToString());
foreach (DocumentElement child in _children)
child.Write();
}
}
public class DocumentParagraph : DocumentElement
{
private string _text = string.Empty;
public DocumentParagraph(string text) { _text = text; }
public override void Add(DocumentElement child)
{
throw new NotSupportedException();
}
public override void Remove(DocumentElement child)
{
throw new NotSupportedException();
}
public override void Write()
{
Console.WriteLine(_text);
}
}
public class Program
{
public static void Main(string[] args)
{
DocumentParagraph pg1 = new DocumentParagraph("1.1");
DocumentParagraph pg2 = new DocumentParagraph("1.2");
DocumentParagraph pg3 = new DocumentParagraph("2.1");
DocumentParagraph pg4 = new DocumentParagraph("2.2");
DocumentChapter chp1 = new DocumentChapter(1);
chp1.Add(pg1);
chp1.Add(pg2);
chp1.Write();
DocumentChapter chp2 = new DocumentChapter(2);
chp2.Add(pg3);
chp2.Add(pg4);
chp2.Write();
Console.ReadLine();
}
}
}
Facade
Il pattern Facade, di tipo strutturale basato sugli oggetti, permette di individuare un’interfaccia unificata per un insieme di interfacce nell’ambito di un sottosistema. Questo pattern in pratica consente di definire un’interfaccia a un livello più alto che semplifica l’accesso alle funzionalità erogate dal sottosistema e che fornisce un’entry-point unico al sottosistema stesso.
La presenza di un oggetto di facciata in un sottosistema permette di mascherare all’esterno la sua complessità interna, limitando le dipendenze dirette e l’accoppiamento. Il client comunica con il sottosistema inviando le sue richieste all’oggetto di facciata, il quale a sua volta funge da tramite verso le parti interne più idonee a fornire la risposta attesa. Il client non conosce come il sottosistema è strutturato e non ha accesso diretto ai suoi oggetti interni con i quali comunica unicamente tramite l’oggetto di facciata.
I partecipanti di questo pattern sono:
-
Facade (SystemManager)
Conosce la struttura del sottosistema e delega agli oggetti interni più appropriati la richieste provenienti dall’esterno.
-
Classi di Subsystem (SystemOne, SystemTwo e SystemThree)
Forniscono le funzionalità interne adatte a rispondere alle richieste provenienti da Facade. Esse non hanno conoscenza dell’esistenza di Facade e non dipendono da esso.
Nell’esempio proposto la classe statica SystemManager presenta un metodo DoSomething() che interagisce con le parti interne del sottosistema. Internamente al metodo vengono infatti richiamati in sequenza altri tre metodi di altrettante classi appartenenti al sottosistema. Il client non conosce l’esatto ordine con cui le chiamate dei metodi vengono effettuate, né quali classi del sottosistema sono effettivamente coinvolte. Di fatto la classe SystemManager disaccoppia il client dalle classi interne, eliminando completamente eventuali dipendenze.
using System;
namespace DesignPatterns.Facade
{
public static class SystemManager
{
public static void DoSomething()
{
new SystemOne().DoSomething();
new SystemTwo().DoSomething();
new SystemThree().DoSomething();
}
}
internal class SystemOne
{
public void DoSomething()
{
Console.WriteLine("One");
}
}
internal class SystemTwo
{
public void DoSomething()
{
Console.WriteLine("Two");
}
}
internal class SystemThree
{
public void DoSomething()
{
Console.WriteLine("Three");
}
}
public class Program
{
public static void Main(string[] args)
{
SystemManager.DoSomething();
Console.ReadLine();
}
}
}
Proxy
Lo scopo del pattern Proxy (detto anche Surrogate) è quello di fornire un surrogato o un segnaposto di un altro oggetto per controllarne l’accesso. Questo pattern, di tipo strutturale basato sugli oggetti, è applicabile ogni volta che si voglia disporre di un riferimento a un oggetto più versatile di un semplice puntatore, tale da permettere, per esempio, di controllare l’accesso all’oggetto vero e proprio piuttosto che di fornire una rappresentazione locale di un oggetto remoto.
Il pattern in questione introduce un livello di indirezione nell’accesso a un oggetto. Questa indirezione ricopre significati diversi a seconda dei casi:
-
si parla di proxy remoto quando si vuole nascondere al client che un oggetto risiede in uno spazio di indirizzamento diverso (esempio classico: Web Service);
-
si parla di proxy virtuale quando si vuole eseguire un’ottimizzazione nella creazione di un oggetto particolarmente “costoso” e pesante piuttosto che memorizzare informazioni aggiuntive relative all’oggetto rappresentato per posticipare l’accesso all’oggetto stesso;
-
si parla di proxy di protezione quando si vuole gestire l’accesso a un oggetto tramite l’esecuzione di azioni preliminari di controllo.
I partecipanti di questo pattern sono:
-
Proxy (ServiceProxy)
Fornisce un’interfaccia identica a quella di Subject e agisce da sostituto di RealSubject.
-
Subject (IService)
Definisce l’interfaccia comune per Proxy e RealSubject, rendendo possibile l’uso di Proxy in tutte le situazioni in cui è possibile utilizzare RealSubject.
-
RealSubject (MyService)
Rappresenta l’oggetto vero e proprio di cui Proxy è il surrogato.
Nell’esempio proposto l’interfaccia IService fornisce il contratto che deve essere rispettato sia dall’oggetto vero e proprio di tipo MyService, sia dal suo surrogato ServiceProxy. Tramite la classe factory ServiceFactory, il client attiva un’istanza della classe proxy che assegna a un riferimento di tipo IService. Il client peraltro non è consapevole di stare usando un surrogato, semplicemente richiama i membri definiti dal contratto, indipendentemente dal tipo concreto istanziato. La classe proxy permette di eseguire più codice rispetto all’oggetto originario: internamente al metodo HandleRequest() vengono infatti eseguite istruzioni prima e dopo la chiamata del metodo di destinazione. Nel caso dell’esempio il tipo di istruzioni aggiuntive incluse nella funzione sono davvero semplici, ma si può arrivare ad avere situazioni in cui il codice presente all’interno della classe proxy è molto più corposo e sostanzioso.
using System;
namespace DesignPatterns.Proxy
{
public interface IService
{
void HandleRequest();
}
public class MyService : IService
{
public void HandleRequest()
{
Console.WriteLine("Handling the request...");
}
}
public class ServiceProxy : IService
{
private MyService _service;
public ServiceProxy(MyService svc)
{
_service = svc;
}
public void HandleRequest()
{
Console.WriteLine("Preprocessing by proxy...");
_service.HandleRequest();
Console.WriteLine("Postprocessing by proxy...");
}
}
public static class ServiceFactory
{
public static IService CreateService()
{
return new ServiceProxy(new MyService());
}
}
public class Program
{
static void Main(string[] args)
{
IService svc = ServiceFactory.CreateService();
svc.HandleRequest();
Console.ReadLine();
}
}
}
Conclusioni
Come si è avuto modo di ricordare anche nel corso degli articoli precedenti, l’implementazione di un pattern non deve necessariamente seguire lo schema di base. La versione, per così dire, ufficiale di un pattern deve rappresentare semplicemente un punto da cui partire per operare le scelte implementative utili a risolvere un determinato problema di disegno e di sviluppo. Sta a chi progetta e scrive il codice trovare di volta in volta le soluzioni giuste, spesso dettate non solo dalla conoscenza della teoria, ma anche e soprattutto dal buon senso e dall’esperienza personale.
In questo articolo abbiamo visto quattro esempi di implementazione relativi ai design pattern GoF di tipo strutturale. Nel prossimo articolo tratteremo i pattern comportamentali.
Riferimenti
-
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides – Design Patterns: Elements of Reusable Object-Oriented Software – Addison Wesley, 1995.
-
Riccardo Golia – Introduzione ai design pattern – MSDN Italia, Novembre 2006.
-
Riccardo Golia – Design pattern per esempi: i GoF creazionali – MSDN Italia, Dicembre 2006.