(Credits: Blog von Vishal Chovatiya)
„Das Simple Factory Pattern kapselt die Erzeugung von Objekten, sodass der aufrufende Code nicht selbst entscheiden muss, welche konkrete Klasse instanziiert wird.”
Das Simple Factory Pattern gehört zu den grundlegendsten Erzeugungsmustern und wird oft als Einstieg in die „richtigen” Factory-Patterns (Factory Method, Abstract Factory) behandelt – streng genommen zählt es im klassischen GoF-Katalog gar nicht als eigenständiges Pattern, sondern eher als idiomatische Programmierpraxis.
Die Grundidee ist einfach: Statt dass Client-Code direkt mit new konkrete Klassen instanziiert, delegiert er diese Aufgabe an eine zentrale Factory-Klasse, die anhand eines Parameters (z. B. eines Enums oder Strings) entscheidet, welches konkrete Objekt erzeugt und zurückgegeben wird.
Dadurch wird der Client von der konkreten Implementierung entkoppelt und arbeitet stattdessen nur mit einer gemeinsamen Basisklasse oder einem Interface.
Das bringt vor allem Vorteile bei der Wartbarkeit: Wenn neue Produktvarianten hinzukommen oder sich die Erzeugungslogik ändert, muss nur die Factory angepasst werden, nicht der gesamte Client-Code.
In C++ wird dies typischerweise über eine statische Methode realisiert,
die einen Zeiger oder std::unique_ptr auf die Basisklasse zurückgibt.
Ein Nachteil ist, dass die Factory bei jeder neuen Produktklasse selbst erweitert werden muss – sie verletzt also tendenziell das Open-Closed-Prinzip, was einer der Gründe ist, warum in komplexeren Szenarien oft zu Factory Method oder Abstract Factory übergegangen wird.
Dennoch ist das Simple Factory Pattern in der Praxis sehr verbreitet, weil es unkompliziert zu implementieren ist und die Objekterzeugung an einer einzigen, klar erkennbaren Stelle bündelt.
Bemerkung:
Nach dem Separation of Concerns-Prinzip sollte die Objekterstellung bzw. -beschaffung von den domänenspezifischen Aufgaben, die ein Objekt hat, getrennt werden.
Eigentlich haben wir hierzu schon eine Vorgehensweise kennen gelernt:
Das Prinzip der Dependency Injection.
Dependency Injection folgt diesem Prinzip in der Gestalt, dass der gesamte Objekterstellungs- und Abhängigkeitsauflösungsprozess in einem Infrastrukturelement zentralisiert ist und sich die Objekte selbst nicht darum kümmern müssen.
Vorsicht:
Was tun wir, wenn ein Objekt irgendwann zur Laufzeit dynamisch erstellt werden muss?
An dieser Stelle kommen Objektfabriken ins Spiel!
Das folgende UML-Diagramm beschreibt eine Implementierung des Simple Factory Patterns. Es besteht im Wesentlichen aus drei Teilen:
- ProductBase: Basisklasse (oder Schnittstelle) für alle Produkte, die von der Factory-Klasse hergestellt werden sollen. Die Schnittstelle beschreibt eine oder mehrere Methoden, die von den konkreten Ableitungen der Klasse implementiert werden.
- ConcreteProduct: Konkrete Implementierung der Klasse
ProductBase. Objekte des TypsConcreteProductwerden von der Klasse Factory erzeugt. - Factory: Diese Klasse besitzt eine Methode
getProduct, die Objekte zurückliefert, die dieProductBase-Schnittstelle implementieren. Über einen Parameter dergetProduct-Methode wird typischerweise gesteuert, welchesConcreteProductObjekt zu erzeugen ist.
Abbildung 1: Schematische Darstellung des Factory Patterns.
Die Architektur des Simple Factory Patterns lässt sich an einer einzigen Frage festmachen:
Wer entscheidet, welches konkrete Produkt erzeugt wird?
- Bei der Simple Factory entscheidet eine zentrale Factory-Funktion/-Klasse anhand eines Parameters über die Erzeugung eines Zielobjekts.
- Bei der Factory Method wird die Entscheidung durch Polymorphie in eine konkrete Factory-Klasse verlagert.
Quellcode – Einfaches Beispiel
01: // =======================================================================
02: // Product
03: // =======================================================================
04:
05: class ProductBase
06: {
07: public:
08: virtual ~ProductBase() = default;
09:
10: [[nodiscard]]
11: virtual std::string_view getName() const = 0;
12:
13: virtual void anyOperation() const = 0;
14: };
15:
16: class ConcreteProductA final : public ProductBase
17: {
18: public:
19: [[nodiscard]]
20: std::string_view getName() const override
21: {
22: return "ConcreteProductA";
23: }
24:
25: void anyOperation() const override
26: {
27: std::println("Working with ConcreteProduct A");
28: }
29: };
30:
31: class ConcreteProductB final : public ProductBase
32: {
33: public:
34: [[nodiscard]]
35: std::string_view getName() const override
36: {
37: return "ConcreteProductB";
38: }
39:
40: void anyOperation() const override
41: {
42: std::println("Working with ConcreteProduct B");
43: }
44: };
45:
46: // =======================================================================
47: // Simple Factory
48: // =======================================================================
49:
50: enum class ProductType
51: {
52: Variant_A,
53: Variant_B
54: };
55:
56: class ProductFactory final
57: {
58: public:
59: [[nodiscard]]
60: static std::unique_ptr<ProductBase>
61: createProduct(ProductType type)
62: {
63: switch (type)
64: {
65: case ProductType::Variant_A:
66: return std::make_unique<ConcreteProductA>();
67:
68: case ProductType::Variant_B:
69: return std::make_unique<ConcreteProductB>();
70: }
71:
72: // Should be unreachable for a valid ProductType.
73: std::unreachable();
74: }
75: };
76:
77: // =======================================================================
78: // Client
79: // =======================================================================
80:
81: static void clientCode(ProductType type)
82: {
83: auto product = ProductFactory::createProduct(type);
84:
85: product->anyOperation();
86:
87: std::println("Created {}", product->getName());
88: }Im Simple Factory Pattern entscheidet die Fabrik selbst:
ProductFactory
│
│ createProduct(type)
▼
┌────┴────┐
│ switch │
└────┬────┘
│
┌─────────┴─────────┐
▼ ▼
ConcreteProductA ConcreteProductB
Der Client sagt:
ProductFactory::createProduct(ProductType::A);Im Factory Method Pattern entscheidet eine abgeleitete, konkrete Fabrik:
FactoryBase
│
requestProduct()
│
▼
createProduct()
virtual method
│
┌──────────┴──────────┐
▼ ▼
ConcreteFactoryA ConcreteFactoryB
│ │
▼ ▼
ConcreteProductA ConcreteProductB
Der Client sagt:
ConcreteFactoryA factory;
clientCode(factory);oder
ConcreteFactoryB factory;
clientCode(factory);und damit nicht
factory.createProduct(ProductType::A);| Simple Factory | Factory Method | |
|---|---|---|
| Wer erzeugt das Produkt? | Factory | konkrete Factory |
| Wer entscheidet über den Typ? | switch / zentrale Factory |
virtuelle Methode |
| Polymorphie bei Factory? | normalerweise nein | ja |
| Neue Produktvariante | Factory ändern | neue Factory-Klasse |
| Neue Concrete Factory | nicht erforderlich | erforderlich |
| Factory Method? | nein | ja |
| GoF Pattern? | Nein, kein GoF-Pattern | Ja |
Hinweis:
Das UML-Diagramm aus Abbildung 1 kann in Sprachen wie C++ nicht immer auf genau diese Weise umgesetzt werden. C++ ist – im Gegensatz zu vielen anderen Sprachen – konzeptionell
- Wert- und
- Referenz-basiert.
Objekte in C++ können sowohl am Stack (direkt erreichbar / als Wert) als auch auf der Halde (indirekt erreichbar über einen Zeiger) liegen. Was bedeutet das: Möchte eine Fabrik-Methode ein Objekt per Value zurückgeben, ist dies nicht mit einer Umsetzung des Schnittstellenkonzepts möglich. Siehe hierzu den folgenden Anwendungsfall.
Das Simple Factory Pattern kommt beispielsweise zum Zuge, wenn es
- viele unterschiedliche Möglichkeiten gibt, ein Objekt zu konstruieren und
- dies aber die Ursache von Fehlerquellen sein kann.
Beispiel:
01: struct Point {
02: Point(double x, double y) { /*...*/ } // Cartesian coordinates
03: // ... Implementation
04:
05: // Not OK: Cannot overload with same type of arguments
06: //
07: // Point(double a, double b){ /*...*/ } // Polar coordinates
08: // ... Implementation
09: };Zwei Konstruktoren in einer Klasse Point mit identischer Signatur,
aber unterschiedlicher Bedeutung:
Dies ist nicht möglich, eine Abhilfe könnte so aussehen:
01: enum class PointType { cartesian, polar };
02:
03: class Point
04: {
05: public:
06: Point(double a, double b, PointType type = PointType::cartesian)
07: {
08: if (type == PointType::cartesian) {
09: m_x = a;
10: m_y = b;
11: }
12: else {
13: m_x = a * cos(b);
14: m_y = a * sin(b);
15: }
16: }
17:
18: private:
19: double m_x;
20: double m_y;
21: };Dies ist jedoch keine sehr einfallsreiche Vorgehensweise, das Problem auf diese Weise zu lösen. Wir sollten vielmehr die jeweilige Instanziierung an separate Methoden delegieren:
01: class Point
02: {
03: private:
04: double m_x;
05: double m_y;
06: PointType m_type;
07:
08: // private constructor, so that object can't be created directly
09: Point(const double x, const double y, PointType t)
10: : m_x{ x }, m_y{ y }, m_type{ t } {}
11:
12: public:
13: friend std::ostream& operator<<(std::ostream& os, const Point& obj) {
14: return os << "x: " << obj.m_x << " y: " << obj.m_y;
15: }
16:
17: static Point NewCartesian(double x, double y) {
18: return { x, y, PointType::cartesian };
19: }
20:
21: static Point NewPolar(double a, double b) {
22: return { a * cos(b), a * sin(b), PointType::polar };
23: }
24: };Wie man an der Implementierung beobachten kann, wird der explizite Gebrauch des Konstruktors untersagt. Der Benutzer wird stattdessen gezwungen, statische Methoden (Klassenmethoden) zu verwenden:
Point p{ Point::NewPolar(5.0, M_PI / 4) };Jetzt haben wir die Funktionalitäten zweier Concerns in eine Klasse gepackt:
Die von der Klasse Point als auch die ihrer Fabrik. Wir sollten den Codeanteil der Fabrik
in eine dedizierte Klasse verlagern. So fühlen wir uns auch besser,
was unsere Bedenken bzgl. des Single Responsibility Principles der SOLID-Designprinzipien anbelangt:
01: class Point
02: {
03: friend class PointFactory;
04:
05: private:
06: double m_x;
07: double m_y;
08:
09: ...
10: };
11:
12: class PointFactory
13: {
14: public:
15: static Point NewCartesian(double x, double y) {
16: return { x, y, PointType::cartesian };
17: }
18:
19: static Point NewPolar(double a, double b) {
20: return { a * cos(b), a * sin(b), PointType::polar };
21: }
22: };Auch hier gibt es noch die Möglichkeit einer Verfeinerung bzw. einer Stolperfalle:
Der Gebrauch des friend-Schlüsselworts ist häufig ein Indikator,
dass gegen das Open-Closed-Prinzip verstoßen wird.
Wir machen eine kritische Beobachtung, die wir in unserer Factory-Klasse übersehen haben:
Es gibt keine wirkliche Verbindung zwischen den beiden Klassen PointFactory und Point!
Warum müssen wir eine Fabrik überhaupt außerhalb der betroffenen Klasse entwerfen?
Wir könnten diese in die Point-Klasse integrieren (in einer so genannten nested class)
und den Benutzer auf diese Weise ermutigen, die Fabrik (sog. Inner Factory) zu verwenden.
01: class Point
02: {
03: private:
04: double m_x;
05: double m_y;
06:
07: Point(double x, double y) : m_x{ x }, m_y{ y } {}
08:
09: public:
10: struct Factory
11: {
12: static Point NewCartesian(double x, double y) {
13: return { x,y };
14: }
15:
16: static Point NewPolar(double r, double theta) {
17: return{ r * cos(theta), r * sin(theta) };
18: }
19: };
20: };Anwendung:
Point p{ Point::Factory::NewCartesian(2, 3) };01: enum class PointType
02: {
03: Cartesian,
04: Polar
05: };
06:
07: class PointFactory
08: {
09: public:
10: [[nodiscard]]
11: static Point create(
12: double a,
13: double b,
14: PointType type)
15: {
16: switch (type)
17: {
18: case PointType::Cartesian:
19: return Point{ a, b };
20:
21: case PointType::Polar:
22: return Point{
23: a * std::cos(b),
24: a * std::sin(b)
25: };
26: }
27:
28: std::unreachable();
29: }
30: };Anwendung:
Point p{ PointFactory::create(2, 3, PointType::Cartesian) };Umstellung auf private Konstruktoren und Bereitstellung von Klassenmethoden.
Das Factory Pattern erstellt seine Objekte im Ganzen im Gegensatz zur Builder-Vorgehensweise. Hier werden die Objekte stückweise erstellt.
Im Quellcode finden Sie ein selbsterklärendes Beispiel: Mobil Phones
Wir betrachten ein zweites, konkretes Beispiel, in dem es um Dokumente unterschiedlichen Formats geht:
// non recommendable implementation
std::unique_ptr<IDocument> open(const std::string& path)
{
if (path.ends_with(".pdf"))
return std::make_unique<PdfDocument>(path);
if (path.ends_with(".html"))
return std::make_unique<HtmlDocument>(path);
return nullptr;
}Wie gehen wir vor, wenn wir die open-Funktion um weitere Dokumentarten wie zum Beispiel
OdtDocument erweitern wollen?
Dabei sollten wir das Open-Closed-Prinzip nicht außer Acht lassen!
Eine Simple Factory-Klasse ist hier angesagt. Eine weitere Option vor dem Hintergrund sich ständig variierender Dokumenttypen ist eine Registrierungsfunktionen, die es gestattet, eigene Typen zu registrieren:
01: struct IDocument
02: {
03: virtual ~IDocument() = default;
04: virtual std::vector<std::string> getText() = 0;
05: };
06:
07: class PdfDocument : public IDocument
08: {
09: private:
10: std::string m_path;
11:
12: public:
13: PdfDocument(const std::string& path) : m_path{ path } {}
14:
15: std::vector<std::string> getText() override {
16: return { "Text from PDF" };
17: }
18: };
19:
20: class HtmlDocument : public IDocument
21: ...
22:
23: class OdtDocument : public IDocument
24: ...
25:
26: template <typename T>
27: using DocumentType = std::unique_ptr<T>;
28:
29: using Document = DocumentType<IDocument>;
30:
31: using DocumentReader = std::function<Document(std::string)>;
32:
33: class DocumentFactory
34: {
35: private:
36: std::unordered_map<std::string, DocumentReader> m_readers;
37:
38: public:
39: void add(const std::string& extension, const DocumentReader& reader) {
40: m_readers.emplace(extension, reader);
41: }
42:
43: Document open(const std::string& path) {
44: auto lastDot = path.find_last_of('.');
45: if (lastDot != std::string::npos) {
46: std::string extension = path.substr(lastDot + 1);
47: DocumentReader& reader = m_readers.at(extension);
48: Document document = reader(path);
49: return document;
50: }
51: else {
52: throw std::invalid_argument{ "Trying to open a file with no extension" };
53: }
54: }
55: };Bemerkung: In dem Beispiel sind einige Modern C++–Sprachkonstrukte enthalten:
std::map-Objekt mit Callables (hier:std::function-Objekte)- Lambdas mit Trailing Return Types
Die Anregungen zum konzeptionellen Beispiel finden Sie unter
Factory Method vs. Simple Factory
und
Factory vs Factory Method vs Abstract Factory
vor.
Das zweite „Real-World”-Beispiel stammt aus dem Buch Software Architecture with C++ von Adrian Ostrowski und Piotr Gaczkowski.
