Softwareentwicklung: Das Design-Pattern Fabrikmethode zum Erzeugen von Objekten
Die Fabrikmethode aus dem Buch "Design Patterns" ist auch als virtueller Konstruktor bekannt. Sie definiert eine Schnittstelle, um ein Objekt zu erstellen.
Patterns sind eine wichtige Abstraktion in der modernen Softwareentwicklung. Sie bieten eine klar definierte Terminologie, eine saubere Dokumentation und das Lernen von den Besten. Das klassische Buch "Design Patterns: Elements of Reusable Object-Oriented Software [1]" (kurz Design Patterns) enthält 23 Muster. Sie sind nach ihrem Zweck geordnet: Erzeugungsmuster, Strukturmuster und Verhaltensmuster. Die Fabrikmethode gehört zu ersterer Kategorie zum Erstellen von Objekten.
Fünf Muster aus dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software [2]" sind erzeugend, sieben strukturell und die restlichen verhaltensorientiert. Was bedeutet das zunächst einmal?
- Erzeugungsmuster befassen sich mit dem Erstellen von Objekten auf eine genau definierte Weise.
- Strukturelle Muster bieten Mechanismen zur Organisation von Klassen und Objekten für größere Strukturen.
- Verhaltensmuster beschäftigen sich mit Kommunikationsmustern zwischen Objekten.
Bevor ich mit den Erzeugungsmustern beginne, möchte ich einen kurzen Disclaimer machen.
Kurzer Disclaimer
Ich stelle etwa die Hälfte der 23 Muster vor. Für die übrigen biete ich nur einen Steckbrief an. Die Auswahl der vorgestellten Muster basiert auf zwei Punkten:
- Welche Muster sind mir als Softwareentwickler in den letzten zwanzig Jahren am häufigsten begegnet?
- Welche Muster sind immer noch in Gebrauch?
Meine Erklärung der vorgestellten Entwurfsmuster ist absichtlich kurz gehalten. Meine Idee ist es, das Schlüsselprinzip eines Musters vorzustellen und es aus der Sicht von C++ zu präsentieren. Wer mehr Details wissen will, findet hervorragende Dokumentationen. Hier sind ein paar Beispiele:
- Der Klassiker: "Design Patterns: Elements of Reusable Object-Oriented Software [3]"
- Eine gute Einführung: "Head First Design Patterns [4]"
- Wikipedia-Artikel: Design Patterns [5]
Erzeugungsmuster befassen sich mit der Erstellung von Objekten.
Erzeugungsmustermuster
Ich werde über zwei der fünf Erzeugungsmuster schreiben: Fabrikmethode [6]und Singleton [7]. Ich weiß, ich weiß, das Singleton könnte auch als Anti-Pattern betrachtet werden. In einem späteren Beitrag werde ich Singleton ausführlich behandeln. Beginnen möchte ich mit der Fabrikmethode.
Fabrikmethode
Hier sind die Fakten:
Zweck
Die Fabrikmethode definiert eine Schnittstelle, um ein einzelnes Objekt zu erstellen, überlässt aber den Unterklassen die Entscheidung, welche Objekte sie erstellen wollen. Die Schnittstelle kann eine Standardimplementierung für die Erstellung von Objekten bereitstellen.
Auch bekannt als
Virtueller Konstruktor
Anwendungsfall
- Eine Klasse weiß nicht, welche Art von Objekten sie erstellen soll
- Unterklassen entscheiden, welches Objekt erstellt werden soll
- Klassen delegieren die Erstellung von Objekten an Unterklassen
Beispiel
Jeder Container der Standard Template Library hat acht Fabrikfunktionen, um verschiedene Iteratoren zu erzeugen.
begin, cbegin
: gibt einen Iterator zurück, der auf den Anfang des Containers zeigtend, cend
: gibt einen Iterator zurück, der auf das Ende des Containers zeigtrbegin, crbegin
: gibt einen Rückwärts-Iterator zurück, der auf den Anfang des Containers zeigtrend, crend
: gibt einen Rückwärts-Iterator zurück, der auf das Ende des Containers zeigt
Die Fabrikfunktionen, die mit c beginnen, geben konstante Iteratoren zurück.
Struktur
Product
- Objekte, die von
factoryMethod
erstellt werden.
Concret Product
- Implementiert die Schnittstelle
Creator
- Deklariert die Fabrikmethode
- Ruft die Fabrikmethode auf
Concrete Creator
- Überschreibt die Fabrikmethode
Der Creator instanziiert das konkrete Produkt nicht. Er ruft seine virtuelle Mitgliedsfunktion factoryMethod
auf. Folglich wird das konkrete Produkt vom konkreten Erzeuger erstellt, und die Objekterstellung ist unabhängig vom Erzeuger.
Dieses Muster wird auch als virtueller Konstruktor bezeichnet.
Virtueller Konstruktor
Ehrlich gesagt ist der Name virtueller Konstruktor irreführend. In C++ gibt es keinen virtuellen Konstruktor, aber wir können virtuelle Konstruktion verwenden, um ihn zu simulieren.
Als Beispiel dient eine Klassenhierarchie mit einer Schnittstellenklasse Window
und zwei Implementierungsklassen DefaultWindow
und FancyWindow
.
// Product
class Window {
public:
virtual ~Window() {};
};
// Concrete Products
class DefaultWindow: public Window {};
class FancyWindow: public Window {};
Nun will man ein neues Window
erstellen, das auf einem bereits vorhandenen Window
basiert. Das heißt, wenn man eine Instanz von DefaultWindow
oder FancyWindow
in der Fabrikfunktion getNewWindow
verwendet, sollte sie eine Instanz der gleichen Klasse zurückgeben.
Klassischerweise wird die Fabrikmethode mit einer Aufzählung und einer Fabrikfunktion implementiert. Hier ist mein erster Versuch:
// factoryMethodClassic.cpp
#include <iostream>
enum class WindowType { // (5)
DefaultWindow,
FancyWindow
};
// Product
class Window {
public:
virtual ~Window() {};
virtual WindowType getType() const = 0;
virtual std::string getName() const = 0;
};
// Concrete Products
class DefaultWindow: public Window {
public:
WindowType getType() const override {
return WindowType::DefaultWindow;
}
std::string getName() const override {
return "DefaultWindow";
}
};
class FancyWindow: public Window {
public:
WindowType getType() const override {
return WindowType::FancyWindow;
}
std::string getName() const override {
return "FancyWindow";
}
};
// Concrete Creator or Client
Window* getNewWindow(Window* window) { // (1)
switch(window->getType()){ // (4)
case WindowType::DefaultWindow:
return new DefaultWindow();
break;
case WindowType::FancyWindow:
return new FancyWindow();
break;
}
return nullptr;
}
int main() {
std::cout << '\n';
DefaultWindow defaultWindow;
FancyWindow fancyWindow;
const Window* defaultWindow1 = getNewWindow(&defaultWindow); // (2)
const Window* fancyWindow1 = getNewWindow(&fancyWindow); // (3)
std::cout << defaultWindow1->getName() << '\n';
std::cout << fancyWindow1->getName() << '\n';
delete defaultWindow1;
delete fancyWindow1;
std::cout << '\n';
}
Die Fabrikfunktion in (1) entscheidet auf der Grundlage des eingehenden Window
welches Window
(2 und 3) erstellt werden soll. Sie verwendet window->getType()
(4), um den richtigen WindowType
zu ermitteln. Der WindowType
ist eine Aufzählung.
Hier ist die Ausgabe des Programms:
Ehrlich gesagt, gefällt mir diese Lösung aus den folgenden Gründen nicht:
- Wenn meine Anwendung neue
Window
s unterstützen soll, müsste ich die AufzählungWindowType
und dieswitch
-Anweisung erweitern. - Die
switch
-Anweisung wird immer schwieriger zu pflegen, wenn ich neueWindowType
hinzufüge. - Der Code ist zu kompliziert. Das liegt vor allem an der
switch
-Anweisung.
Deshalb werde ich die switch
-Anweisung durch einen virtuellen Dispatch ersetzen. Außerdem möchte ich auch die bestehenden Window
s klonen.
// factoryMethod.cpp
#include <iostream>
// Product
class Window{
public:
virtual Window* create() = 0; // (1)
virtual Window* clone() = 0; // (2)
virtual ~Window() {};
};
// Concrete Products
class DefaultWindow: public Window {
DefaultWindow* create() override {
std::cout << "Create DefaultWindow" << '\n';
return new DefaultWindow();
}
DefaultWindow* clone() override {
std::cout << "Clone DefaultWindow" << '\n';
return new DefaultWindow(*this);
}
};
class FancyWindow: public Window {
FancyWindow* create() override {
std::cout << "Create FancyWindow" << '\n';
return new FancyWindow();
}
FancyWindow* clone() override {
std::cout << "Clone FancyWindow" << '\n';
return new FancyWindow(*this); // (5)
}
};
// Concrete Creator or Client
Window* createWindow(Window& oldWindow) { // (3)
return oldWindow.create();
}
Window* cloneWindow(Window& oldWindow) { // (4)
return oldWindow.clone();
}
int main() {
std::cout << '\n';
DefaultWindow defaultWindow;
FancyWindow fancyWindow;
const Window* defaultWindow1 = createWindow(defaultWindow);
const Window* fancyWindow1 = createWindow(fancyWindow);
const Window* defaultWindow2 = cloneWindow(defaultWindow);
const Window* fancyWindow2 = cloneWindow(fancyWindow);
delete defaultWindow1;
delete fancyWindow1;
delete defaultWindow2;
delete fancyWindow2;
std::cout << '\n';
}
Die Klasse Window
unterstützt nun zwei Möglichkeiten, neue Window
s zu erstellen: ein default-konstruiertes Window
mit der Memberfunktion create (1) und ein kopiertes Window
mit der Memberfunktion clone
(2). Der feine Unterschied besteht darin, dass der Konstruktor den this
-Zeiger in die Mitgliedsfunktion clone
(5) übernimmt. Die Fabrikfunktionen createWindow
(3) und cloneWindow
(4) arbeiten mit dem dynamischen Typ.
Die Ausgabe des Programms ist vielversprechend. Beide Mitgliedsfunktionen create
und clone
zeigen den Namen des Objekts an, das sie erzeugen.
Im Übrigen: Es ist in Ordnung, dass die virtuellen Memberfunktionen create
und clone
des DefaultWindow
und des FancyWindow
privat sind, da sie über die Window
-Schnittstelle verwendet werden. In der Schnittstelle sind beide Mitgliedsfunktionen öffentlich.
Wie geht's weiter?
Ist meine Fabrikmethode fertig implementiert? NEIN! Das Programm factoryMethod.cpp
besitzt zwei ernsthafte Probleme: die explizite Klärung der Besitzverhältnisse und das Slicing. In meinem nächsten Artikel werde ich genauer auf beide Punkte eingehen.
(rme [8])
URL dieses Artikels:
https://www.heise.de/-7252845
Links in diesem Artikel:
[1] https://en.wikipedia.org/wiki/Design_Patterns
[2] https://en.wikipedia.org/wiki/Design_Patterns
[3] https://en.wikipedia.org/wiki/Design_Patterns
[4] https://www.oreilly.com/library/view/head-first-design/9781492077992/
[5] https://en.wikipedia.org/wiki/Design_Patterns
[6] https://en.wikipedia.org/wiki/Factory_method_pattern
[7] https://en.wikipedia.org/wiki/Singleton_pattern
[8] mailto:rme@ix.de
Copyright © 2022 Heise Medien