Patterns in der Softwareentwicklung: Die Alternativen zum Singleton-Muster

Eine Frage zum umstrittenen Singleton-Muster ist noch offen: Welche Alternativen zu diesem Design Pattern gibt es?

In Pocket speichern vorlesen Druckansicht 8 Kommentare lesen

(Bild: Shutterstock.com/Kenishirotie)

Lesezeit: 5 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Nach einer Einführung zum Singleton-Muster, das als das umstrittenste Entwurfsmuster aus dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software" gilt, und einer Diskussion seiner Vor- und Nachteile ist nur noch eine Frage offen: Welche Alternativen gibt es zum Singleton-Muster? Zwei alternative Pattern möchte ich heute vorstellen: das Monostate-Muster und Dependency Injection.

Das Monostate-Muster ähnelt dem Singleton-Muster und ist in Python sehr beliebt. Während das Singleton-Muster garantiert, dass nur eine Instanz einer Klasse existiert, stellt das Monostate-Muster sicher, dass alle Instanzen einer Klasse den gleichen Zustand besitzen. Das Monostate-Muster ist auch als Borg Idiom bekannt – in Anlehnung an die Borg aus der Science-Fiction-Serie Star Trek, die ein gemeinsames Gehirn beziehungsweise Gedächtnis teilen.

Im Monostate-Muster sind alle Datenelemente statisch. Das bedeutet, dass die Instanzen einer Klasse dieselben Daten verwenden. Die Mitgliedsfunktionen für den Zugriff auf die Daten hingegen sind nicht statisch. Die Benutzer der Instanzen wissen nichts von dem singletonartigen Verhalten der Klasse.

// monostate.cpp

#include <iostream>
#include <string>
#include <unordered_map>

class Monostate {
  
 public:
    
    void addNumber(const std::string& na, int numb) {
        teleBook[na] = numb;
    }
 
    void getEntries () const {
        for (auto ent: teleBook){ 
            std::cout << ent.first << ": " << ent.second << '\n';
        }
    }
    
private:
    static std::unordered_map<std::string, int> teleBook;
 
};

std::unordered_map<std::string, int> Monostate::teleBook{};

int main() {
    
    std::cout << '\n';
    
    Monostate tele1;
    Monostate tele2;
    tele1.addNumber("grimm", 123);
    tele2.addNumber("huber", 456);
    tele1.addNumber("smith", 789);
    
    tele1.getEntries();
    
    std::cout << '\n';
    
    tele2.getEntries();
    
    std::cout << '\n';
    
}

Jede Instanz der Klasse Monostate teilt denselben Zustand:

Die vermutlich naheliegendere Alternative zum Singleton-Muster dürfte die Dependency Injection sein.

Das Singleton-Muster hat eine Reihe schwerwiegender Nachteile, die ich in meinem Artikel "Pattern in der Softwareentwicklung: Vor- und Nachteile des Singleton-Musters" beschrieben habe. Vielen gilt das Singleton Pattern daher eher als Anti-Pattern, und es dürfte in einer Neuauflage des Buches "Design Patterns: Elements of Reusable Object-Oriented Software" wahrscheinlich nicht mehr zu finden sein. Dependency Injection ist stattdessen ein wahrscheinlicher Ersatzkandidat für die Zukunft.

Der Kerngedanke von Dependency Injection ist, dass einem Objekt oder einer Funktion (Client) bei seiner Initialisierung aus einer zentralen Instanz derjenige Dienst zugeteilt wird, von dem es abhängt. In diesem Fall weiß der Client nichts von der Konstruktion des Dienstes. Der Client ist vollständig von dem zu nutzenden Dienst getrennt, der von einem Injektor injiziert wird. Dies steht im Gegensatz zum Singleton-Muster, bei dem der Client den Dienst bei Bedarf selbst erstellt.

int client(){
  
  ...

  auto singleton = Singleton::getInstance();
  singleton.doSomething();

  ...

}

Dependency Injection ist eine Form der Umkehrung der Kontrolle (Inversion of Control). Nicht der Client erstellt und ruft den Dienst auf, sondern der Injektor injiziert den Dienst in den Client.

In C++ werden drei Arten von Dependency Injection verwendet:

  • Konstruktor-Injektion
  • Setter-Injektion
  • Template-Parameter-Injektion

Im folgenden Programm dependencyInjection.cpp verwende ich Konstructor- und Setter-Injektion

// dependencyInjection.cpp

#include <chrono>
#include <iostream>
#include <memory>

class Logger {
public:
    virtual void write(const std::string&) const = 0;
    virtual ~Logger() = default;
};

class SimpleLogger: public Logger {
    void write(const std::string& mess) const override {
        std::cout << mess << '\n';
    }
};

class TimeLogger: public Logger {
    typedef std::chrono::duration<long double> MySecondTick;
    long double timeSinceEpoch() const {
        auto timeNow = std::chrono::system_clock::now();
        auto duration = timeNow.time_since_epoch();
        MySecondTick sec(duration);
        return sec.count();
    }
    void write(const std::string& mess) const override {
        std::cout << std::fixed;
        std::cout << "Time since epoch: " << timeSinceEpoch() << ": " << mess << '\n';
    }

};

class Client {
public:
    Client(std::shared_ptr<Logger> log): logger(log) {}   // (1)
    void doSomething() {
        logger->write("Message");
    }
    void setLogger(std::shared_ptr<Logger> log) {         // (2)
        logger = log;
    }
private:
    std::shared_ptr<Logger> logger;
};
        

int main() {
    
    std::cout << '\n';
    
    Client cl(std::make_shared<SimpleLogger>());
    cl.doSomething();
    cl.setLogger(std::make_shared<TimeLogger>());
    cl.doSomething();
    cl.doSomething();
    
    std::cout << '\n';
 
}

Der Client cl benötigt eine Logger-Funktion. Zunächst wird der Logger SimpleLogger über den Konstruktor injiziert (Zeile 1), danach wird der Logger durch den leistungsfähigeren Logger TimeLogger ersetzt. Die setter-Mitgliedsfunktion ermöglicht es, den neuen Logger zu injizieren. Der Client ist vollständig vom Logger entkoppelt. Er unterstützt lediglich die Schnittstellen, um Logger zu injizieren.

Hier folgt die Ausgabe des Programms:

In der Standard Template Library gibt es viele Beispiele für Dependency Injection basierend auf Template-Parametern. Ich nenne hier exemplarisch die Container.

  • Die Container der STL verwenden einen Standard-Allokator. Dieser kann durch einen eigenen Allokator ersetzt werden.
  • Der geordnete assoziative Container verwendet std::less als Sortierkriterium. Alternativ lässt sich auch ein anderes Sortierkriterium nutzen.
  • Die ungeordneten assoziativen Container benötigen eine Hash-Funktion und eine Gleichheitsfunktion. Beides sind Template-Parameter und können daher ersetzt werden.

Zum Schluss möchte ich noch ein paar kurze Einschätzungen zu den drei verbleibenden Erzeugungsmuster abgeben.

Abstrakte Fabrik

Mit der Abstrakten Fabrik kannst du Familien von verwandten Objekten erstellen, ohne ihre konkreten Klassen angeben zu müssen. Ein typisches Beispiel ist ein IDE-Theme, das aus vielen verwandten Objekten besteht. Jedes IDE-Thema hat zum Beispiel verschiedene Widgets wie Kontrollkästchen, Schieberegler, Drucktasten, Optionsfelder etc. Ein konkretes IDE-Theme hat typischerweise verschiedene Fabrikmethoden für die verschiedenen Widgets. Ein Kunde kann das IDE-Theme und damit auch die Widgets während der Nutzung der IDE ändern.

Erbauer

Das Erbauer-Muster bauen komplexe Objekte Schritt für Schritt auf. Dieses Muster erlaubt es, verschiedene Arten und Darstellungen eines Objekts mit demselben schrittweisen Konstruktionsprozess zu erstellen. Das Erbauer-Muster extrahiert den Objektkonstruktionscode aus seiner Klasse und verschiebt ihn in separate Objekte, die Erbauer genannt werden. Es müssen nicht alle Schritte des Bauprozesses aufgerufen werden, und ein Schritt kann mehr als einen Erbauer besitzen.

Prototyp

Das Prototyp-Muster erstellt Objekte durch Klonen eines bestehenden Objekts. Der Artikel "Softwareentwicklung: Design-Pattern Fabrikmethode ohne Probleme" setzt das Prototyp-Muster bereits in dem Programm factoryMethodWindowSlicingFixed.cpp um. Das Prototyp-Muster ähnelt der Fabrikmethode, legt aber den Schwerpunkt auf die Initialisierung der erstellten Prototypen. Die Fabrikmethode erzeugt verschiedene Objekte, indem sie die Objekterzeugung an Subklassen delegiert.

Meine nächsten Artikel über Design Patterns sind den strukturellen Mustern gewidmet. Ich beginne mit dem Adaptor-Muster, das auf zwei Arten in C++ umgesetzt werden kann: Mehrfachvererbung und Delegation. (map)