Adapter Design Pattern

L’Adapter, spesso denominato anche Wrapper, è un Design Patten di tipo strutturale che è utilizzato quando si ha la necessità di rendere  compatibili  due interfacce che di fatto non lo sono, senza modificarne il codice.

Una tipica situazione è quella in cui un client utilizza oggetti che implementano una determinata interfaccia ma, per un qualsiasi motivo, ad esempio perchè open source o perchè sviluppate da un gruppo differente, vengono poi rese disponibili librerie simili, ma che implementano un’interfaccia differente.

Esempio Pratico

Consideriamo il semplice esempio di una classe ShapeCalculator che riceve in input oggetti che implementano l’interfaccia Shape e ne calcola area e perimetro:

Le classi Triangle e Square implementano l’interfaccia Shape, la quale espone due soli metodi: perimeter() ed area(). La situazione è quella mostrata nel class diagram rappresentato nella figura di seguito.

 Per completezza riportiamo anche le implementazioni delle classi e delle interfacce menzionate.

Supponiamo ora che ci venga fornita una API con altre figure geometriche già implementate e che vogliamo utilizzare nel nostro programma. Chiaramente l’esempio è molto semplice, ma in contesti reali, con librerie molto più complesse, disporre di codice già implementato e testato è una situazione abbastanza comune ed in generale  reinventare la ruota  non è una strada praticabile, per motivi di tempo e costi.

Sfortunatamente la nuova API utilizza l’interfaccia GeometricShape che definisce gli stessi metodi di Shape, perimeter() e area(), ma poiché l’ereditarietà in java è di tipo  strutturale  (o sub-typing), le due interfacce sono comunque differenti. Quindi se volessimo utilizzare nel nostro client la classe Rectangle che implementa GeometricShape, si presenterebbe una incompatibilità evidenziata dal compilatore nel momento in cui tentiamo di eseguire il codice seguente:

Implementazione del Pattern

Scartate tutte le opzioni che prevedono di rinunciare all’utilizzo della libreria incompatibile o la riscrittura del nostro codice, la sola opzione praticabile è quella di implementare il design pattern Adapter. Prima di vedere come si realizza operativamente tale pattern, facciamo un po’ di ordine introduciamo formalmente i concetti che lo caratterizzano:

Target Indica la specifica interfaccia che è utilizzata dal client. Nel nostro esempio è l’interfaccia Shape.
Adaptee Identifica l’interfaccia esistente che necessita di essere adattata e resa compatibile con l’interfaccia Target. Nel nostro caso si tratta dell’interfaccia GeometricShape.
Adapter E’ la classe che realizza il pattern e rende le due interfacce compatibili. La sua implementazione dipende dall’approccio che si intende seguire, come mostrato di seguito.
Client Rappresenta il client che utilizza esclusivamente oggetti che implementano l’interfaccia Target. Ovvero la classe ShapeCalculator del nostro esempio.

La situazione è quella mostrata nella figura seguente con la nomenclatura appena introdotta:

Object Adapter Pattern

Un primo approccio per implementare il pattern è quello di utilizzare la composition, ovvero di includere l’oggetto sorgente nell’implementazione dell’adapter. In altri termini la classe Adapter implementa l’interfaccia Target (ovvero Shape del nostro esempio) e fa riferimento ad un oggetto di tipo Adaptee (ovvero GeometricShape del nostro esempio). Quindi implementa tutti i metodi richiesti da Target eseguendo la conversione necessaria per soddisfare i requisiti di tale interfaccia, utilizzando i metodi esposti daAdaptee.

Di seguito la classe GeometricShapeObjectAdapter che realizza tale approccio per il nostro semplice caso di esempio:

Il Main del client ShapeCalculator è quindi modificato nel modo seguente:
La figura seguente mostra il Class Diagram per la soluzione proposta.

Class Adapter Pattern

Un secondo approccio consiste nell’utilizzare l’ereditarietà, estendendo la classe dell’oggetto che si intende adattare, in una nuova classe che implementa l’interfaccia Target. Come è evidente tale approccio, diversamente dal precedente, richiede lo sviluppo di una classe distinta per ogni oggetto che implementa l’interfaccia Adaptee.

Di seguito la classe RhombusAdapter che realizza tale approccio per la classe Rhombus che implementa l’interfaccia GeometricShape:

Si noti che nel nostro caso poichè abbiamo mantenuto uniformità nei nomi dei metodi delle interfacce Shape e GeometricShape la classe RhombusAdapter è vuota.

Il Main del client ShapeCalculator è quindi modificato nel modo seguente:

Il diagramma UML complessivo rappresentativo del nostro piccolo progetto diviene quindi:

Codie Sorgente

Il codice sorgente del progetto è disponibile qui adapter-pattern.