design-patterns-adapter.html 9.62 KB
Newer Older
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<p>Ein Adapter hilft zwei zunächst inkompatible Schnittstellen miteinander zu verbinden. Die Analogie dieses Entwurfsmusters zu einem Stromnetzadapter für Auslandsreisen ist naheliegend. Der Stromnetzadapter ermöglicht es, eine Steckdose mit einem Stecker für ein elektrisches Gerät zu verbinden, auch wenn der Stecker nicht direkt passt, weil er eine andere Schnittstelle aufweist.</p>

<img src="media/patterns_adapter_plug_socket.png" style="width:540px">
<label>Analogie zum Adapter-Muster</label>

<p>In der Software-Entwicklung wird ein Adapter (engl. auch <i>Wrapper</i>) eingesetzt, wenn eine existierende Klasse verwendet werden soll, deren Interface nicht dem vorgesehenen Interface entspricht. Die existierende Klasse soll oder kann nicht verändert werden, insbesondere wenn sie Teil einer externen Bibliothek ist, die von Dritten entwickelt wird. Die zu adaptierende Klasse aus der Bibliothek ist häufig von Interesse, da sie komplexes Verhalten implementiert, das wiederverwendet werden soll. Das Prinzip der Wiederverwendung ist grundsätzlich effizient, wenn externe Bibliotheken in ein Projekt eingebunden werden, von denen bekannt ist, dass sie umfänglich getestet worden sind und dass sie sich in anderen Projekten bereits praktisch bewährt haben. Im Java-Umfeld werden Bibliotheken i.d.R. aus einem <a href="https://mvnrepository.com/">zentralen Repository</a> über Build-Management-Werkzeuge wie <a href="https://maven.apache.org/">Maven</a> oder <a href="https://gradle.org/">Gradle</a> automatisiert in ein Projekt eingebunden.</p>

<p>Das Adapter-Muster gibt es in zwei Varianten: Objektadapter und Klassenadapter. In beiden Varianten wird die Ziel-Schnittstelle <code>Target</code> von der Adapter-Klasse <code>Adapter</code> implementiert. Weiterhin ruft der Adapter in der von <code>Target</code> vorgeschriebenen Methode <code>operation</code> die Methode <code>service</code> der adaptierten Klasse <code>Adaptee</code> auf. Es können natürlich auch mehrere Methoden adaptiert werden. Beim Objektadapter besteht eine Assoziation zwischen Adapter und adaptierter Klasse, während beim Klassenadapter der Adapter die adaptierte Klasse erweitert. Der Klassenadapter ist in Java nicht zu realisieren, falls <code>Target</code> kein Interface sondern eine zustandsbehaftete Klasse ist, da in diesem Fall Mehrfachvererbung erforderlich wäre. Die folgenden UML-Klassendiagramme visualisieren Objekt- und Klassenadapter im Vergleich zueinander.</p>

<img src="media/patterns_adapter.png" style="width:1000px">
<label>Entwurfsmuster Adapter (in Varianten Objekt- und Klassenadapter)</label>

<p>In dem folgenden Beispiel zur Motivation eines Adapters möchten wir die Fläche und den Umfang für unterschiedliche 2-dimensionale Formen bestimmen. Dazu haben wir das Interface <code>Shape</code> spezifiziert und bereits Implementierungen für einfache Formen wie Kreis, Rechteck und Dreieck entwickelt.</p>

<ul class="nav nav-tabs" id="adapter-tabs" role="tablist">
  <li class="nav-item"><a href="#adapter-tabs-shape" class="nav-link active" data-toggle="tab" role="tab">Shape</a></li>
  <li class="nav-item"><a href="#adapter-tabs-circle" class="nav-link" data-toggle="tab" role="tab">Circle</a></li>
  <li class="nav-item"><a href="#adapter-tabs-rectangle" class="nav-link" data-toggle="tab" role="tab">Rectangle</a></li>
  <li class="nav-item"><a href="#adapter-tabs-triangle" class="nav-link" data-toggle="tab" role="tab">Triangle</a></li>
</ul>
<div class="tab-content" id="adapter-tabs-content">
  <div class="tab-pane show active" id="adapter-tabs-shape" role="tabpanel">
	<pre><code class="language-java line-numbers">interface Shape {
    Double perimeter();
    Double area();
}</code></pre>  
  </div>
  <div class="tab-pane" id="adapter-tabs-circle" role="tabpanel">
	<pre><code class="language-java line-numbers">class Circle implements Shape {
    Point2D center;
    Double radius;

    Circle(Point2D center, Double radius) {
        this.center = center;
        this.radius = radius;
    }

    @Override
    public Double area() { return Math.PI * radius * radius; }

    @Override
    public Double perimeter() { return 2 * Math.PI * radius; }
}</code></pre> 
  </div>
  <div class="tab-pane" id="adapter-tabs-rectangle" role="tabpanel">
	<pre><code class="language-java line-numbers">class Rectangle implements Shape {
    Point2D topLeft;
    Double width, height;

    Rectangle(Point2D topLeft, Double width, Double height) {
        this.topLeft = topLeft;
        this.width = width;
        this.height = height;
    }

    @Override
    public Double perimeter() { return 2 * (width + height); }

    @Override
    public Double area() { return width * height; }
}</code></pre> 
  </div>
  <div class="tab-pane" id="adapter-tabs-triangle" role="tabpanel">
	<pre><code class="language-java line-numbers">class Triangle implements Shape {
    Point2D p1, p2, p3; // points
    Double e1, e2, e3; // edges

    Triangle(Point2D p1, Point2D p2, Point2D p3) {
        this.p1 = p1; this.p2 = p2; this.p3 = p3;
        e1 = p1.distance(p2); e2 = p2.distance(p3); e3 = p3.distance(p1);
    }

    @Override
    public Double perimeter() { return e1 + e2 + e3; }

    @Override
    public Double area() {
        Double s = perimeter() / 2;
        return Math.sqrt(s * (s - e1) * (s - e2) * (s - e3));
    }
}</code></pre> 
  </div>
</div>

<p>Das Interface <code>Shape</code> nutzen wir in unserer Anwendung (<code>Client</code>), um über verschiedene Formen zu iterieren und diese über ihre gemeinsame Schnittstelle zu verarbeiten. Es soll noch eine <code>Shape</code>-Implementierung für beliebige Polygone hinzugefügt werden. Ein Polygon besteht aus einer Reihe von Punkten, die über Kanten miteinander verbunden werden. Der <code>Client</code> soll zur Veranschaulichung die Fläche und den Umfang der folgenden 4 Formen berechnen.</p>

<img src="media/patterns_adapter_shapes.png" style="width:500px">
<label>Polygone zur Motivation eines Adapters</label>

<p>Die Bestimmung der Fläche eines Polygons ist nicht trivial. Aus diesem Grund möchten wir die Bibliothek <a href="http://commons.apache.org/proper/commons-math/">Apache Commons Math</a> einbinden, die eine API für sehr viele mathematische Grundlagen bereitstellt. Wir interessieren uns für die Methode <code>getSize</code> der Klasse <a href="http://commons.apache.org/proper/commons-math/apidocs/org/apache/commons/math4/geometry/euclidean/twod/PolygonsSet.html"><code>PolygonsSet</code></a>, über die Fläche für ein Polygon berechnet werden kann. Das Code-Beispiel zeigt, wie das Polygon über einen Objektadapter (<code>PolygonObjectAdapter</code>) oder alternativ über einen Klassenadapter (<code>PolygonClassAdapter</code>) realisiert werden kann. In der Klasse <code>Client</code> ist zu erkennen, dass auch die neuen Adapter-Klassen das Interface <code>Shape</code> erfüllen.</p>

<ul class="nav nav-tabs" id="adapter-tabs2" role="tablist">
  <li class="nav-item"><a href="#adapter-tabs-polygon1" class="nav-link active" data-toggle="tab" role="tab">PolygonObjectAdapter</a></li>
  <li class="nav-item"><a href="#adapter-tabs-polygon2" class="nav-link" data-toggle="tab" role="tab">PolygonClassAdapter</a></li>
  <li class="nav-item"><a href="#adapter-tabs-client" class="nav-link" data-toggle="tab" role="tab">Client</a></li>
</ul>
<div class="tab-content" id="adapter-tabs2-content">
  <div class="tab-pane show active" id="adapter-tabs-polygon1" role="tabpanel">
	<pre><code class="language-java line-numbers">import org.apache.commons.math3.geometry.euclidean.twod.PolygonsSet;
import org.apache.commons.math3.geometry.euclidean.twod.Vector2D;

class PolygonObjectAdapter implements Shape {
    PolygonsSet adaptee;

    PolygonObjectAdapter(Point2D... points) {
        Vector2D[] vectors = Stream.of(points).map(p -> new Vector2D(p.getX(), p.getY())).toArray(size -> new Vector2D[size]);
        adaptee = new PolygonsSet(.1, vectors);
    }

    @Override
    public Double perimeter() { return adaptee.getBoundarySize(); }

    @Override
    public Double area() { return adaptee.getSize(); }
}</code></pre>  
  </div>
  <div class="tab-pane" id="adapter-tabs-polygon2" role="tabpanel">
	<pre><code class="language-java line-numbers">import org.apache.commons.math3.geometry.euclidean.twod.PolygonsSet;
import org.apache.commons.math3.geometry.euclidean.twod.Vector2D;
	
class PolygonClassAdapter extends PolygonsSet implements Shape {

    PolygonClassAdapter(Point2D... points) {
        super(.1, Stream.of(points).map(p -> new Vector2D(p.getX(), p.getY())).toArray(size -> new Vector2D[size]));
    }

    @Override
    public Double perimeter() { return this.getBoundarySize(); }

    @Override
    public Double area() { return this.getSize(); }
}</code></pre> 
  </div>
  <div class="tab-pane" id="adapter-tabs-client" role="tabpanel">
	<pre><code class="language-java line-numbers">class Client {

    public static void main(String[] args) {
        Shape s1 = new Rectangle(new Point2D(0, 0), 3., 2.);
        Shape s2 = new Triangle(new Point2D(0, 0), new Point2D(0, 2), new Point2D(1, 1));
        Shape s3 = new PolygonObjectAdapter(new Point2D(0, 0), new Point2D(3, 0), new Point2D(3, 2), new Point2D(0, 2), new Point2D(1, 1));
        Shape s4 = new PolygonClassAdapter(new Point2D(0, 0), new Point2D(3, 0), new Point2D(2, 1), new Point2D(3, 2), new Point2D(0, 2), new Point2D(1, 1));

        List.of(s1, s2, s3, s4).forEach(s -> {
            System.out.printf("%s: perimeter = %.2f, area = %.2f\n", s.getClass().getSimpleName(), s.perimeter(), s.area());
        });
    }
}</code></pre> 
  </div>
</div>

<p>Die gezeigten Code-Beispiele zum Entwurfsmuster Adapter finden sich im Verzeichnis <a href="/patterns/adapter" class="repo-link">/patterns/adapter</a> des Modul-Repository.</p>