zurück zum Artikel

Softwareentwicklung: Das Design-Pattern Fabrikmethode zum Erzeugen von Objekten

Rainer Grimm

(Bild: Blackboard/Shutterstock.com)

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.

Modernes C++ – Rainer Grimm
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 [2]" sind erzeugend, sieben strukturell und die restlichen verhaltensorientiert. Was bedeutet das zunĂ€chst einmal?

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 [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.

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

Jeder Container der Standard Template Library hat acht Fabrikfunktionen, um verschiedene Iteratoren zu erzeugen.

Die Fabrikfunktionen, die mit c beginnen, geben konstante Iteratoren zurĂŒck.

Product

Concret Product

Creator

Concrete Creator

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 [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