design-patterns-factory_method.html 15 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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
<p>Die Fabrikmethode (engl. <i>Factory Method</i>) ist ein Erzeugungsmuster, das beschreibt, wie ein Objekt durch den Aufruf einer Methode anstatt eines Konstruktors erzeugt wird. Diese Methode ist Teil einer sogenannten Fabrik-Klasse, die für die Erzeugung von Objekten zuständig ist. Es ist irreführend, dass die Fabrikmethode im allgemeinen Sprachgebrauch von Software-Entwicklern sowohl eine beliebige statische Methode zur Objekterzeugung beschreibt als auch eines der ursprünglichen GoF-Entwurfsmuster.</p>

<h4>Statische Fabrikmethode</h4>

<p>Wir stellen hier zunächst die statische Fabrikmethode vor, die von Joshua Bloch in seinem bekannten Java-Lehrbuch "Effective Java" <a href="#cite-Blo17">[Blo17]</a> wie folgt definiert:</p>

<div class="cite">"A class can provide a public static factory method, which is simply a static method that returns an instance of the class." (Joshua Bloch)</div>

<p>Das folgende Code-Beispiel illustriert die statische Fabrikmethode anhand der Klasse <code>LoggerFactory</code>, die hier zwei unterschiedliche Logger (nämlich einen <code>ConsoleLogger</code> und einen <code>FileLogger</code>) erzeugen kann (Zeilen 5-12). Über ein Argument wird beispielhaft entschieden, welcher konkrete Logger erzeugt wird. Der <code>ConsoleLogger</code> ist hier der Default, falls die Methode <code>createLogger</code> ohne Argument aufgerufen wird (Zeilen 14-16). Dieser Default könnte auch aus einer Konfigurationsdatei eingelesen oder per <i>Dependency Injection</i> verknüpft werden.</p>

<ul class="nav nav-tabs" id="factory1-tabs" role="tablist">
  <li class="nav-item"><a href="#factory1-tabs-loggerfactory" class="nav-link active" data-toggle="tab" role="tab">LoggerFactory</a></li>
  <li class="nav-item"><a href="#factory1-tabs-logger" class="nav-link" data-toggle="tab" role="tab">MyLogger</a></li>
  <li class="nav-item"><a href="#factory1-tabs-consolelogger" class="nav-link" data-toggle="tab" role="tab">ConsoleLogger</a></li>
  <li class="nav-item"><a href="#factory1-tabs-filelogger" class="nav-link" data-toggle="tab" role="tab">FileLogger</a></li>
  <li class="nav-item"><a href="#factory1-tabs-client" class="nav-link" data-toggle="tab" role="tab">Client</a></li>
</ul>
<div class="tab-content" id="factory1-tabs-content">
  <div class="tab-pane show active" id="factory1-tabs-loggerfactory" role="tabpanel">
	<pre><code class="language-java line-numbers">public class LoggerFactory {

    public enum LogType {CONSOLE, FILE};

    public static MyLogger createLogger(LogType type) {
        if (type.equals(LogType.CONSOLE)) {
            return new ConsoleLogger();
        } else if (type.equals(LogType.FILE)) {
            return new FileLogger();
        }
        return null;
    }

    public static MyLogger createLogger() {
		return createLogger(LogType.CONSOLE); // default logger
    }
}</code></pre> 
  </div>
  <div class="tab-pane" id="factory1-tabs-logger" role="tabpanel">
	<pre><code class="language-java line-numbers">interface MyLogger {
    void log(String message);
    default void close() {}
}</code></pre> 
  </div>
  <div class="tab-pane" id="factory1-tabs-consolelogger" role="tabpanel">
	<pre><code class="language-java line-numbers">public class ConsoleLogger implements MyLogger {
	
    @Override
    public void log(String message) {
        System.out.println(message);
    }
}</code></pre> 
  </div>
  <div class="tab-pane" id="factory1-tabs-filelogger" role="tabpanel">
	<pre><code class="language-java line-numbers">public class FileLogger implements MyLogger {

    BufferedWriter writer;

    @Override
    public void log(String message) {
        try {
            if (writer == null) {
                writer = new BufferedWriter(new FileWriter("log.txt", true));
            }
            writer.append(message + "\n");
        } catch (IOException e) {}
    }

    @Override
    public void close() {
        try {
            writer.close();
        } catch (IOException e) {}
    }
}</code></pre> 
  </div> 
  <div class="tab-pane" id="factory1-tabs-client" role="tabpanel">
	<pre><code class="language-java line-numbers">class Client {

    public static void main(String[] args) {
        MyLogger a = new ConsoleLogger(); // constructor instantiation >> causes dependency to ConsoleLogger
        a.log("Hello World!");

        MyLogger b = LoggerFactory.createLogger(); // static factory method instantiation
        b.log("Hello World!");
    }
}</code></pre> 
  </div> 
</div>

<p>Das Interface <code>MyLogger</code> schreibt die gemeinsame Schnittstelle für alle konkreten Logger vor, die diese individuell implementieren. Der Client ist – im Gegensatz zur einfachen Instantiierung eines Loggers per Konstruktor – bei Verwendung der Fabrikmethode nicht mehr von einer konkreten Logger-Klasse abhängig, sondern kann ausschließlich auf dem Interface <code>MyLogger</code> arbeiten.</p>

<p>Die Stärke dieses Musters liegt in seiner Einfachheit. Im Vergleich zu dem GoF-Entwurfsmuster Fabrikmethode (s. unten) muss die Fabrik-Klasse nicht ihrerseits instanziiert werden, um Objekte zu erzeugen, und für die erzeugende Methode muss kein Interface definiert werden. Eine statische Fabrikmethode setzt natürlich voraus, dass die Programmiersprache Klassenmethoden (≙ Schlüsselwort <code>static</code> in Java) kennt.</p>

<p>Die statische Fabrikmethode kann auch Gebrauch von der Java Reflection API machen, was in folgendem Code-Beispiel dargestellt ist (Zeile 4).</p>

<pre><code class="language-java line-numbers">public class ReflectionLoggerFactory {

    public static MyLogger createLogger(Class&lt;? extends MyLogger> loggerClass) {
        return loggerClass.getConstructor().newInstance(); // create instance by default constructor reflection
    }

    public static MyLogger createLogger() {
        return createLogger(ConsoleLogger.class); // default logger
    }	
}</code></pre> 

<p>Auch die wirkliche Java Logging API setzt übrigens auf eine statische Fabrikmethode <code>getLogger</code> in der Klasse <code>java.util.logging.Logger</code>, um ein Logger-Objekt zu erzeugen:</p>
<pre><code class="language-java line-numbers">import java.util.logging.Logger;
// ...
Logger logger = Logger.getLogger(getClass().getName());
logger.log(Level.INFO, "Hello World!");
</code></pre> 

<h4>Entwurfsmuster Fabrikmethode</h4>

<p>Nun möchten wir die bisherige statische Fabrikmethode zu dem GoF-Entwurfsmuster Fabrikmethode erweitern. Das Ziel des Musters ist es, dass neue Klassen zu einer bestehenden Schnittstelle oder Vererbungshierarchie durch Dritte hinzugefügt werden können, und Objekte dieser neuen Klassen über eine zugehörige Fabrikmethode erzeugt werden können. Wir können uns z.B. vorstellen, dass ein Dritter einen weiteren Logger für das obige Interface <code>MyLogger</code> implementieren möchte, z.B. einen <code>JDBCLogger</code> oder einen <code>RedisLogger</code>, um das Log in einer Datenbank abzulegen. Der Dritte hat aber keinen Zugriff auf unseren Code, insbesondere nicht auf die Klasse <code>LoggerFactory</code>. Demzufolge muss die <code>LoggerFactory</code> ihrerseits von außen erweiterbar sein. Eine Lösung könnte wie folgt aussehen, wobei die bisherige Klasse <code>LoggerFactory</code> in <code>LoggerCreator</code> umbenannt wird:</p>

<img src="media/patterns_factorymethod_example.png" style="width:900px">
<label>Anwendungsbeispiel für das Fabrikmethode-Muster</label>

<ul class="nav nav-tabs" id="factory2-tabs" role="tablist">
  <li class="nav-item"><a href="#factory2-tabs-creator" class="nav-link active" data-toggle="tab" role="tab">LoggerCreator</a></li>
  <li class="nav-item"><a href="#factory2-tabs-concretecreator" class="nav-link" data-toggle="tab" role="tab">ConsoleLoggerCreator</a></li>
  <li class="nav-item"><a href="#factory2-tabs-client" class="nav-link" data-toggle="tab" role="tab">Client</a></li>
</ul>
<div class="tab-content" id="factory2-tabs-content">
  <div class="tab-pane show active" id="factory2-tabs-creator" role="tabpanel">
	<pre><code class="language-java line-numbers">abstract class LoggerCreator {

    Set&lt;MyLogger> loggers;

    abstract MyLogger createLogger();

    MyLogger register() {
        MyLogger logger = createLogger();
        loggers.add(logger);
        return logger;
    }
}</code></pre> 
  </div>
  <div class="tab-pane" id="factory2-tabs-concretecreator" role="tabpanel">
	<pre><code class="language-java line-numbers">class ConsoleLoggerCreator extends LoggerCreator {

    @Override
    MyLogger createLogger() { return new ConsoleLogger(); }
}</code></pre> 
  </div>
  <div class="tab-pane" id="factory2-tabs-client" role="tabpanel">
	<pre><code class="language-java line-numbers">class Client {

    public static void main(String[] args) {
        LoggerCreator creator = new ConsoleLoggerCreator();

        MyLogger a = creator.register();
        a.log("Hello World!");

        MyLogger b = creator.register();
        b.log("Hello World!");

        System.out.println("Created "+ creator.loggers.size() + " loggers until now.");
    }
}</code></pre> 
  </div> 
</div>

<p>Die abstrakte Klasse <code>LoggerCreator</code> definiert eine abstrakte Methode <code>createLogger</code>. Eine statische Methode zur Erzeugung der bekannten konkreten Logger gibt es hingegen nicht mehr. Es wird angenommen, dass zukünftig verschiedene konkrete Logger (wie z.B. der <code>JDBCLogger</code>) entstehen, die durch Dritte realisiert werden. Der Fokus liegt auf der Erweiterbarkeit. Die Oberklasse zur Objekterzeugung <code>LoggerCreator</code> kann auch Methoden beinhalten, die Verhalten implementieren, das für alle Logger gleich ist – hier beispielhaft die Methode <code>register</code>. Nur die konkreten Erzeuger (z.B. <code>JDBCLoggerCreator</code>) sind von einer zugehörigen Logger-Implementierung (z.B. <code>JDBCLogger</code>) abhängig.</p>

168
<p>Das folgende UML-Klassendiagramm verallgemeinert das Logger-Beispiel, sodass das allgemeingültige Muster der GoF-Fabrikmethode entsteht, das durch Erich Gamma et al. <a href="#cite-GHJ+10">[GHJ+10]</a> wie folgt definiert ist:<p>
169
170
171
172
173
174
175
176
177
178
179
180
181
182

<div class="cite">"Define an interface for creating an object, but let subclasses decide which class to instantiate. The factory method lets a class defer instantiation it uses to subclasses." (Erich Gamma et al.)</div>

<img src="media/patterns_factorymethod.png" style="width:700px">
<label>Entwurfsmuster Fabrikmethode</label>

<p>In diesem Muster gibt es eine allgemeine Oberklasse (oder Schnittstelle) zur Erstellung von ähnlichen Objekten (<code>Creator</code>). Den konkreten Erzeugern wird eine Fabrik-Methode (<code>factoryMethod</code>) zur Objekterzeugung vorgeschrieben. Die Oberklasse hat keine Abhängigkeit zu einem konkreten Erzeuger oder konkreten Produkt. Dieses Muster ist insbesondere dann sinnvoll, wenn die Oberklasse eine weitere Methode enthält, in der sie Verhalten implementiert, das unabhängig vom konkreten Typ eines Produkts für jedes Produkt gleichermaßen gilt. Im obigen UML-Klassendiagramm ist dies die Methode <code>anyOperation</code>. Aus objektorientierter Sicht gehört dieses gemeinsame Verhalten eigentlich eher zur Schnittstelle <code>Product</code> als zur Fabrik-Klasse. Es kann aber in der Praxis sein, dass die Fabrik-Klasse zusätzlich fachliches Verhalten für die Produkte implementiert, da letztere auf einfache Modellklassen reduziert sind, die über ihre Attribute nur Zustand erfassen, aber keine fachlichen Methoden enthalten sollen.</p>

<p>Der Nachteil des Entwurfsmusters Fabrikmethode ist, dass zwei parallele Spezialisierungshierarchien gepflegt werden müssen. Das ist der Preis für die Erweiterbarkeit durch Dritte. Im Gegensatz dazu ist eine statische Fabrikmethode immer dann als einfacherer Ansatz zu bevorzugen, wenn alle Produktklassen vorab bekannt sind oder die Erweiterbarkeit durch Dritte nicht wichtig ist.</p>

<p>Wenn die zu erzeugenden Produkte sich in den notwendigen Argumenten beim Konstruktoraufruf unterscheiden und sich daher zur Instantiierung nicht auf eine gemeinsame Schnittstelle einigen können, kann das Entwurfsmuster <a href="https://en.wikipedia.org/wiki/Builder_pattern">Erbauer (engl. <i>Builder</i>)</a> herangezogen werden, ggf. in Kombination mit der Fabrikmethode.</p>

<h4>Abstrakte Fabrik</h4>

183
<p>Wenn die Fabrik-Klasse nicht nur ein Produkt, sondern gleich eine ganze Familie von zusammengehörigen Produkten erzeugen soll, wird die Fabrikmethode schnell zum Entwurfsmuster <a href="https://en.wikipedia.org/wiki/Abstract_factory_pattern">Abstrakte Fabrik (engl. <i>Abstract Factory</i>)</a> ausgebaut. Ein beliebtes Beispiel ist hier im Kontext von UI-Frameworks die Erzeugung von UI-Elementen (wie Buttons, Textfelder, usw.), die jeweils einem bestimmten Design-Stil folgen sollen (wie Cupertino oder Material Design). Hierbei entsteht für jedes Produkt einer Produktfamilie eine separate Spezialisierungshierarchie. Die konkreten Produkte einer Familie werden alle aus einer gemeinsamen Fabrik-Klasse erzeugt, die dadurch einen konsistenten Design-Stil gewährleistet. Der Client arbeitet ausschließlich auf abstrakten Produkten, die er über eine konkrete Fabrik-Klasse erzeugt. Die Idee der Abstrakten Fabrik wird durch das folgende UML-Klassendiagramm und das zugehörige Code-Beispiel verdeutlicht.</p>
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223

<img src="media/patterns_abstractfactory_example.png" style="width:1000px">
<label>Anwendungsbeispiel für das Entwurfsmuster Abstrakte Fabrik</label>

<ul class="nav nav-tabs" id="factory3-tabs" role="tablist">
  <li class="nav-item"><a href="#factory3-tabs-abstractfactory" class="nav-link active" data-toggle="tab" role="tab">Abstrakte Fabrik/Produkte</a></li>
  <li class="nav-item"><a href="#factory3-tabs-concretefactory" class="nav-link" data-toggle="tab" role="tab">Konkrete Fabrik/Produkte</a></li>
  <li class="nav-item"><a href="#factory3-tabs-client" class="nav-link" data-toggle="tab" role="tab">Client</a></li>
</ul>
<div class="tab-content" id="factory3-tabs-content">
  <div class="tab-pane show active" id="factory3-tabs-abstractfactory" role="tabpanel">
	<pre><code class="language-java line-numbers">interface AbstractFactory {
    Button createButton();
    TextField createTextField();
}

interface Button {} // abstract product
interface TextField {} // another abstract product</code></pre> 
  </div>
  <div class="tab-pane" id="factory3-tabs-concretefactory" role="tabpanel">
	<pre><code class="language-java line-numbers">class MaterialFactory implements AbstractFactory {

    @Override
    public Button createButton() { return new MaterialButton(); }

    @Override
    public TextField createTextField() { return new MaterialTextField(); }
}

class MaterialButton implements Button {} // concrete product
class MaterialTextField implements TextField {} // another concrete product</code></pre> 
  </div>
  <div class="tab-pane" id="factory3-tabs-client" role="tabpanel">
	<pre><code class="language-java line-numbers">AbstractFactory factory = new MaterialFactory();
Button button = factory.createButton();
TextField textField = factory.createTextField();</code></pre> 
  </div> 
</div>

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