Softwareentwicklung: Design-Pattern Fabrikmethode ohne Probleme

Die Fortsetzung des Beitrags über die Factory-Methode behebt zwei Probleme: Slicing und Ownership-Semantik.

In Pocket speichern vorlesen Druckansicht 10 Kommentare lesen

(Bild: Kenneth Summers/Shutterstock.com)

Lesezeit: 5 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

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.

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.

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 Programm factoryMethodWindowsIssues.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 basiert auf Disziplin: In modernem C++ übertragen wir den Besitz nicht mit einem rohen Zeiger.

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 eines std::unique_ptr<Widget> bedeutet, dass der Aufrufer der Eigentümer ist. Das std::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 einer std::shared_ptr<Widget> bedeutet, dass sich der Aufrufer und der Aufgerufene den Besitz teilen. Wenn weder der Aufrufer noch der Aufgerufene das std::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.

Das endgültige Programm factoryMethodUniquePtr.cpp 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. ()