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.

Lesezeit: 6 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 8 Beiträge

(Bild: Blackboard/Shutterstock.com)

Von
  • 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. Sie sind nach ihrem Zweck geordnet: Erzeugungsmuster, Strukturmuster und Verhaltensmuster. Die Fabrikmethode gehört zu ersterer Kategorie zum Erstellen von Objekten.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Fünf Muster aus dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software" 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.

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:

  1. Welche Muster sind mir als Softwareentwickler in den letzten zwanzig Jahren am häufigsten begegnet?
  2. 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:

Erzeugungsmuster befassen sich mit der Erstellung von Objekten.

Ich werde über zwei der fünf Erzeugungsmuster schreiben: Fabrikmethode und Singleton. 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.

Hier sind die Fakten:

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.

Virtueller Konstruktor

  • 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

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 zeigt
  • end, cend: gibt einen Iterator zurück, der auf das Ende des Containers zeigt
  • rbegin, crbegin: gibt einen Rückwärts-Iterator zurück, der auf den Anfang des Containers zeigt
  • rend, 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.

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.

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:

  1. Wenn meine Anwendung neue Windows unterstützen soll, müsste ich die Aufzählung WindowType und die switch-Anweisung erweitern.
  2. Die switch-Anweisung wird immer schwieriger zu pflegen, wenn ich neue WindowType hinzufüge.
  3. 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 Windows 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 Windows 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.

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)