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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class ShapeCalculator { List<Shape> shapes = new ArrayList<Shape>(); public void addShape(Shape shape) { shapes.add(shape); } public void areas() { shapes.stream().forEach(shape -> System.out.println( shape.getClass().getSimpleName() + " area: " + shape.area() ) ); } public void perimeters() { shapes.stream().forEach(shape -> System.out.println( shape.getClass().getSimpleName() + " perimeter: " + shape.perimeter() ) ); } public static void main(String[] args) { ShapeCalculator calculator = new ShapeCalculator(); calculator.addShape( new Triangle() ); calculator.addShape( new Square() ); calculator.perimeters(); calculator.areas(); } } |
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.
1 2 3 4 |
public interface Shape { public double perimeter(); public double area(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class Triangle implements Shape { private double side1 = 1.0; private double side2 = 1.0; private double side3 = 1.0; public Triangle() {} public Triangle(double side1, double side2, double side3) { this.side1 = side1; this.side2 = side2; this.side3 = side3; } public double perimeter() { return side1 + side2 + side3; } public double area() { // Heron's formula double p = perimeter() / 2; return Math.sqrt(p * (p - side1) * (p - side2) * (p - side3) ); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Square implements Shape { private double side = 1.0; public Square() {} public Square(double side) { this.side = side; } public double perimeter() { return 4 * side; } public double area() { return side * side; } } |
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:
1 2 3 4 5 |
public static void main(String[] args) { .... calculator.addShape( new Rectangle() ); .... } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class GeometricShapeObjectAdapter implements Shape { private GeometricShape adaptee; public GeometricShapeObjectAdapter(GeometricShape adaptee) { this.adaptee = adaptee; } public double perimeter() { return adaptee.perimeter(); } public double area() { return adaptee.area(); } } |
Main
del client ShapeCalculator
è quindi modificato nel modo seguente:
1 2 3 4 5 6 7 8 9 10 11 |
public static void main(String[] args) { ShapeCalculator calculator = new ShapeCalculator(); calculator.addShape( new Triangle() ); calculator.addShape( new Square() ); calculator.addShape( new GeometricShapeObjectAdapter( new Rectangle() ) ); calculator.perimeters(); calculator.areas(); } |
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
:
1 2 3 |
public class RhombusAdapter extends Rhombus implements Shape { } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static void main(String[] args) { ShapeCalculator calculator = new ShapeCalculator(); calculator.addShape( new Triangle() ); calculator.addShape( new Square() ); calculator.addShape( new GeometricShapeObjectAdapter( new Rectangle() ) ); calculator.addShape( new RhombusAdapter() ); calculator.perimeters(); calculator.areas(); } |
Il diagramma UML complessivo rappresentativo del nostro piccolo progetto diviene quindi:
Codie Sorgente
Il codice sorgente del progetto è disponibile qui adapter-pattern.