Softwareentwicklung: Design-Pattern Fabrikmethode ohne Probleme
Die Fortsetzung des Beitrags über die Factory-Methode behebt zwei Probleme: Slicing und Ownership-Semantik.
![](https://heise.cloudimg.io/width/610/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/6/0/4/7/4/5/shutterstock_1460415629-31d02cc3cb16d0e2.jpeg)
(Bild: Kenneth Summers/Shutterstock.com)
- Rainer Grimm
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" (kurz Design Patterns) enthält 23 Muster, darunter die Fabrikmethode, die zu den Erzeugungsmustern gehört.
In der letzten Folge dieses Blogs habe ich die Factory-Methode vorgestellt: "Softwareentwicklung: Das Design-Pattern Fabrikmethode zum Erzeugen von Objekten". Meine Implementierung hatte zwei gravierende Probleme: Slicing und Ownership-Semantik. Heute werde ich diese Probleme beheben.
Zur Erinnerung: Hier ist eine vereinfachte und leicht veränderte Implementierung der Fabrikmethode aus meinem letzten Artikel. Erstens unterstützt diese Implementierung nur die Mitgliedsfunktion clone
; zweitens sollte auch die Basisklasse Window
klonbar sein.
// factoryMethodWindowIssues.cpp
#include <iostream>
// Product
class Window{
public:
virtual Window* clone() {
std::cout << "Clone Window" << '\n';
return new Window(*this);
}
virtual ~Window() {};
};
// Concrete Products
class DefaultWindow: public Window {
DefaultWindow* clone() override {
std::cout << "Clone DefaultWindow" << '\n';
return new DefaultWindow(*this);
}
};
class FancyWindow: public Window {
FancyWindow* clone() override {
std::cout << "Clone FancyWindow" << '\n';
return new FancyWindow(*this);
}
};
// Concrete Creator or Client
Window* cloneWindow(Window& oldWindow) {
return oldWindow.clone();
}
int main() {
std::cout << '\n';
Window window;
DefaultWindow defaultWindow;
FancyWindow fancyWindow;
const Window* window1 = cloneWindow(window);
const Window* defaultWindow1 = cloneWindow(defaultWindow);
const Window* fancyWindow1 = cloneWindow(fancyWindow);
delete window1;
delete defaultWindow1;
delete fancyWindow1;
std::cout << '\n';
}
Das Programm erzeugt das erwartete polymorphe Verhalten.
Slicing
Zuallererst: Was ist Slicing?
- Slicing bedeutet, dass man ein Objekt während der Zuweisung oder Initialisierung kopieren will und nur einen Teil des Objekts erhält.
Slicing ist eine der dunkelsten Ecken von C++, wie folgendes einfache Beispiel illustriert:
// slice.cpp
#include <iostream>
#include <typeinfo>
struct Base { };
struct Derived : Base { };
void displayTypeinfo(const Base& b) {
std::cout << typeid(b).name() << '\n';
}
void needB(Base b) {
displayTypeinfo(b);
};
int main() {
Derived d;
Base b = d;
displayTypeinfo(b); // (1)
Base b2(d);
displayTypeinfo(b2); // (2)
needB(d); // (3)
}
Die Ausdrücke (1), (2) und (3) haben alle den gleichen Effekt: Der abgeleitete Teil von d
wird entfernt. Das ist aber vermutlich nicht beabsichtigt.
Warum ist Slicing ein Problem für das Beispiel factoryMethodWindowIssues.cpp?
Lass mich die C++ Core Guidelines zitieren: C.67: A polymorphic class should suppress public copy/move. Damit steht die nächste Frage im Raum: Was ist eine polymorphe Klasse?
- Eine polymorphe Klasse ist eine Klasse, die mindestens eine virtuelle Funktion definiert oder erbt.
Hier ist das Problem: Window
im Programm factoryMethodWindowIssues.cpp
ist eine polymorphe Klasse, die das öffentliche Kopieren/Verschieben nicht unterdrückt. Ein Window
kann ein Opfer von Slicing werden, wie folgender Codeausschnitt veranschaulicht. Ich habe lediglich die Referenz aus der Funktionssignatur der Funktion cloneWindow
entfernt:
// Concrete Creator or Client
Window* cloneWindow(Window oldWindow) {
return oldWindow.clone();
}
Da die Factory-Funktion cloneWindow
ihr Argument als Kopie und nicht mehr als Referenz nimmt, kommt das Slicing zum Tragen.
Schauen wir uns an, was passiert, wenn ich die Regel: C.67: A polymorphic class should suppress public copy/move. befolge.
Hier ist das korrigierte Programm:
// factoryMethodWindowSlicingFixed.cpp
#include <iostream>
// Product
class Window{
public:
Window() = default; // (3)
Window(const Window&) = delete; // (1)
Window& operator = (const Window&) = delete; // (2)
virtual Window* clone() {
std::cout << "Clone Window" << '\n';
return new Window(*this);
}
virtual ~Window() {};
};
// Concrete Products
class DefaultWindow: public Window {
DefaultWindow* clone() override {
std::cout << "Clone DefaultWindow" << '\n';
return new DefaultWindow(*this);
}
};
class FancyWindow: public Window {
FancyWindow* clone() override {
std::cout << "Clone FancyWindow" << '\n';
return new FancyWindow(*this);
}
};
// Concrete Creator or Client
Window* cloneWindow(Window oldWindow) { // (4)
return oldWindow.clone();
}
int main() {
std::cout << '\n';
Window window;
DefaultWindow defaultWindow;
FancyWindow fancyWindow;
const Window* window1 = cloneWindow(window);
const Window* defaultWindow1 = cloneWindow(defaultWindow);
const Window* fancyWindow1 = cloneWindow(fancyWindow);
delete window1;
delete defaultWindow1;
delete fancyWindow1;
std::cout << '\n';
}
Ich habe den Kopierkonstruktor (1) und den Kopierzuweisungsoperator (2) auf delete
gesetzt. Aufgrund des deklarierten Kopierkonstruktors unterstützt die Klasse Window
keine Move-Semantik. Außerdem erzeugt der Compiler auch nicht den Defaultkonstruktor. Deshalb muss ich ihn anfordern (1).
Die Fehlermeldung des Microsoft Visual C++ Compilers bringt es auf den Punkt:
Der erforderliche Kopierkonstruktor ist nicht verfügbar.
Ownership-Semantik
Im Allgemeinen kennt man die Implementierung der Fabrikfunktion cloneWindow
nicht. cloneWindow
gibt einen Zeiger auf Window
zurück. Zeiger haben einen impliziten Makel. Sie modellieren zwei völlig unterschiedliche Semantiken: Besitz und Ausleihe.
- Besitz: Der Aufrufer ist für das
Window
verantwortlich und muss es zerstören. Das Verhalten modelliert das ProgrammfactoryMethodWindowsIssues.cpp
.
- Ausleihe: Der Aufgerufene ist für das Window verantwortlich und leiht es sich dem Aufrufer aus.
Lass mich das noch einmal betonen:
- Besitzer: Man ist Besitzer des Fensters, muss sich um es kümmern und zerstören. Ansonsten entsteht ein Speicherleck.
- Ausleiher: Man ist nicht der Besitzer des Fensters und darf es nicht zerstören. Ansonsten wird es zweimal gelöscht.
Wie können wir diesen Designfehler überwinden? Die Rettung besteht aus zwei Komponenten. Einer schwachen, die auf Disziplin basiert und einer starken, die auf dem Typensystem beruht.
Die schwache Rettung
Die schwache Rettung basiert auf Disziplin: In modernem C++ übertragen wir den Besitz nicht mit einem rohen Zeiger.
Die starke Rettung
Die starke Rettung basiert auf dem Typsystem. Wer das Eigentum übertragen will, verwende einen Smart Pointer. Dafür gibt es zwei Möglichkeiten:
std::unique_ptr<Widget>
: Die Rückgabe einesstd::unique_ptr<Widget>
bedeutet, dass der Aufrufer der Eigentümer ist. Dasstd::unique_ptr<Widget>
verhält sich wie eine lokale Variable. Wenn sie den Gültigkeitsbereich verlässt, wird sie automatisch zerstört.
std::shared_ptr<Widget>
: Die Rückgabe einerstd::shared_ptr<Widget>
bedeutet, dass sich der Aufrufer und der Aufgerufene den Besitz teilen. Wenn weder der Aufrufer noch der Aufgerufene dasstd::shared_ptr<Widget>
mehr benötigt, wird es automatisch zerstört.
Das folgende Programm factoryMethodUniquePtr.cpp
verwendet eine std::unique_ptr<Window>
, um das Eigentum explizit zu übertragen. Zudem kann eine std::unique_ptr nicht kopiert werden. Folglich erstellt die Factory-Funktion eine neue std::unique_ptr<Widget>
.
// factoryMethodUniquePtr.cpp
#include <iostream>
#include <memory>
// Product
class Window{
public:
virtual std::unique_ptr<Window> create() {
std::cout << "Create Window" << '\n';
return std::make_unique<Window>();
}
};
// Concrete Products
class DefaultWindow: public Window {
std::unique_ptr<Window> create() override {
std::cout << "Create DefaultWindow" << '\n';
return std::make_unique<DefaultWindow>();
}
};
class FancyWindow: public Window {
std::unique_ptr<Window> create() override {
std::cout << "Create FancyWindow" << '\n';
return std::make_unique<FancyWindow>();
}
};
// Concrete Creator or Client
auto createWindow(std::unique_ptr<Window>& oldWindow) {
return oldWindow->create();
}
int main() {
std::cout << '\n';
std::unique_ptr<Window> window = std::make_unique<Window>();
std::unique_ptr<Window> defaultWindow = std::make_unique<DefaultWindow>();
std::unique_ptr<Window> fancyWindow = std::make_unique<FancyWindow>();
const auto window1 = createWindow(window);
const auto defaultWindow1 = createWindow(defaultWindow);
const auto fancyWindow1 = createWindow(fancyWindow);
std::cout << '\n';
}
Abschließend ist hier die Ausgabe des Programms.
Jede create
-Mitgliedsfunktion gibt ein std::unique_ptr<Widget>
zurück, erzeugt aber unter der Haube ein std::unique_ptr<Widget>
, ein std::unique_ptr<DefaultWindow>
oder ein std::unique_ptr<FancyWindow>.
Daher bleibt das polymorphe Verhalten der createWindow
-Funktion erhalten.
Außerdem löst diese auf std::unique_ptr
basierende Implementierung das Slicing-Problem automatisch, da ein std::unique_ptr<Widget>
nicht kopiert werden kann.
Wie geht's weiter?
Das endgültige Programm factoryMethodUniquePtr.cp
p ist korrekt. Es überwindet die Probleme des Slicing und der Ownerhip-Semantik. In meinem nächsten Artikel werde ich näher auf das umstrittenste Muster des Design Patterns Buch eingehen: Singleton.
()