L’Adapter Pattern appartiene alla categoria “Structural Pattern”, secondo l’identificazione proposta dalla GoF. In questa categoria ricadono i patterns che usano i meccanismi di un linguaggio per facilitare la progettazione, individuando un modo semplice per stabilire la relazione tra gli oggetti.
Per descrivere il contesto in cui può essere applicato il pattern in oggetto, supponiamo di avere il seguente scenario: dobbiamo sviluppare un software utilizzando librerie di terze parti di cui non abbiamo il codice sorgente. Vogliamo far “collaborare” due classi A e B appartenenti alle due librerie, ma aventi interfacce incompatibili. Per far funzionare il tutto abbiamo bisogno di costruire qualcosa che serva da “adattatore”. Nel mondo reale, se abbiamo un elettrodomestico che usa una spina schuko, ma a parete abbiamo una presa da 10A, per poter utilizzare l’apparecchio dobbiamo comprare un adattore ( se non ne abbiamo uno).
Un altro contesto in cui può essere utilizzato questo pattern è il seguente: supponiamo di voler creare un oggetto che contenta un metodo che possa essere invocato da un client in maniera non diretta: il metodo può essere invocato solo da uno o più metodi pubblici esposti verso l’esterno che possono avere firme differenti dal metodo “core” nascosto al resto del mondo. In questo modo la classe può subire modifiche mantenendo le retrocompatibilità, oppure implementare un unico punto funzionalità a cui il client accede. Non bisogna confondere l’Adapter Pattern con il Bridge Pattern.
Gli attori che entrano in gioco nell’Adapter Pattern sono:
- Target, l’interfaccia che gli oggetti (client) devono utilizzare
- Client, oggetti che utilizzano altri oggetti che implementano l’interfaccia Target
- Adaptee, interfaccia che vogliamo adattare
- Adattatore (Adapter), adatta l’interfaccia Adaptee (vecchia) a quella Target (Nuova)
Secondo che l’adapter abbia al suo interno un riferimento ad un Adaptee o che utilizzi l’ereditarietà multiplia, abbiamo due modi diversi di risolvere il problema:
Object Adapters: usa una tecnica composizionale per adattare un’interfaccia ad un’altra. L’Adapter eredita l’interfaccia target che il client si aspetta di utilizzare, gestendo un riferimento all’istanza dell’Adaptee. Quando il client invoca un metodo dell’Adapter, la richiesta è tradotta in una specifica richiesta per l’Adaptee.
Class Adapter: utilizza l’eredità multipla per raggiungere lo scopo. Come per Object Adapters, la classe Adapter eredita l’interfaccia Target (quella che si aspetta il client), ma anche quella dell’Adaptee. In questo caso, una richiesta ad un metodo Target viene semplicemente “girata” allo specifico metodo descritto dall’interfaccia dell’Adaptee.
Per aggiungere del colore al post, di seguito il diagramma delle classi (creato con VS2010) secondo la soluzione Object Adapters :
Nel diagramma sono presenti due Package (namespace C#) che cercano di evidenziare lo scenario descritto precedentemente dove all’interno di QueueObjects, ICustomQueue e CustomAdapter possono essere visibili da tutti (Public) mentre ListAdapter è internal, quindi visibile solo da i file presenti nello stesso assembly. Se immaginiamo quindi i due namespace come assembly, il gioco è fatto.
In questo esempio, il client pensa di lavorare con una coda (Queue), ma in realtà lavora con un oggetto (tramite il CustomAdapter) che richiama i metodi di una lista presente nell’Adaptee (listAdaptee) e che si comporta da coda FIFO. Il codice dell’esempio è il seguente:
ICustomQueue
1: public interface ICustomQueue
2: {
3: Object Dequeue();
4:
5: void Enqueue(Object item);
6:
7: void Purge();
8:
9: }
CustomAdapter
1: public class CustomAdapter : ICustomQueue
2: {
3: private ListAdaptee _listAdaptee = null;
4:
5:
6: public virtual Object Dequeue()
7: {
8: return _listAdaptee.Get();
9: }
10:
11: public virtual void Enqueue(Object item)
12: {
13: _listAdaptee.Add(item);
14: }
15:
16: public virtual void Purge()
17: {
18: _listAdaptee.Clear();
19: }
20:
21: public CustomAdapter()
22: {
23: _listAdaptee = new ListAdaptee();
24: }
25: }
ListAdaptee. Piccola osservazione: in un caso reale si potrebbe usare direttamente un oggetto di tipo List, ma nell’esempio ho cercato di enfatizzare il concetto di “Adattamento” che non può sempre essere risolto in modo banale
1: internal class ListAdaptee
2: {
3: private List<Object> _objectsList = null;
4:
5: public virtual void Add(Object item)
6: {
7: _objectsList.Add(item);
8: }
9:
10: public virtual Object Get()
11: {
12: Object item = null;
13:
14: if (_objectsList.Count > 0)
15: {
16: item = _objectsList.Take(1).FirstOrDefault();
17: _objectsList.Remove(item);
18: }
19:
20: return item;
21: }
22:
23: public virtual void Clear()
24: {
25: _objectsList.Clear();
26: }
27:
28: public ListAdaptee()
29: {
30: _objectsList = new List<object>(3);
31: }
32: }
Client
1: public class Client
2: {
3: ICustomQueue _customQueue = null;
4:
5: public virtual ICustomQueue ICustomQueue
6: {
7: get
8: {
9: return _customQueue;
10: }
11: }
12:
13: public Client()
14: {
15: _customQueue = new CustomAdapter();
16: }
17:
18: public virtual void Test()
19: {
20: _customQueue.Enqueue("1");
21: _customQueue.Enqueue("2");
22: _customQueue.Enqueue("3");
23: _customQueue.Enqueue("4");
24: _customQueue.Enqueue("5");
25:
26: Console.WriteLine(_customQueue.Dequeue().ToString());
27: Console.WriteLine(_customQueue.Dequeue().ToString());
28: Console.WriteLine(_customQueue.Dequeue().ToString());
29: Console.WriteLine(_customQueue.Dequeue().ToString());
30: Console.WriteLine(_customQueue.Dequeue().ToString());
31: }
32: }
Possiamo testare il codice con una semplice applicazione console
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: ClientObjects.Client client = new ClientObjects.Client();
6: client.Test();
7:
8: Console.Read();
9: }
10: }
Ottenendo: