Patterns in Software Development: The Alternatives to the Singleton Pattern

After an introduction to the Singleton pattern, considered the most controversial design pattern from the book “Design Patterns: Elements of Reusable Object-Oriented Software” and a discussion of its advantages and disadvantages, only one question remains: what are the alternatives to the singleton. pattern? I’d like to present two alternative patterns today: the monostate pattern and dependency injection.



The Monostat pattern is similar to the Singleton pattern and is very popular in Python. While the singleton pattern guarantees that only one instance of a class exists, the monostate pattern ensures that all instances of a class have the same state. The monostat pattern is also known as the Borg idiom, referring to the Borg from the sci-fi series Star Trek, which share a common brain or memory.

In the monostat pattern, all data elements are static. This means that the instances of a class use the same data. However, the member functions to access the data are not static. The instance users are unaware of the singleton-like behavior of the class.

// monostate.cpp

#include 
#include 
#include 

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 teleBook;
 
};

std::unordered_map 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';
    
}

Any instance of the class Monostate share the same condition:



The probably more obvious alternative to the singleton pattern is dependency injection.

The Singleton pattern has a number of serious drawbacks, which I described in my article "Pattern in Software Development: Pros and Cons of the Singleton Pattern". The singleton pattern is therefore seen by many as an anti-pattern and is unlikely to be found in a new edition of the book "Design Patterns: Elements of Reusable Object-Oriented Software". Dependency injection is instead a likely replacement candidate for the future.

The essence of dependency injection is that when an object or function (client) is initialized from a central instance, it is assigned the service it depends on. In this case, the developer does not know anything about the construction of the service. The client is completely separate from the service to be used, which is injected by an injector. This is in contrast to the singleton pattern, where the client itself creates the service when it is needed.

int client(){
  
  ...

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

  ...

}

Dependency injection is a form of inversion of control. It is not the client that creates and invokes the service, but rather the injector that injects the service into the client.

There are three types of dependency injection used in C++:

  • constructor injection
  • setter injection
  • Template parameter injection

In the following program dependencyInjection.cpp I use constructor and setter injection

// dependencyInjection.cpp

#include 
#include 
#include 

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 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 log): logger(log) {}   // (1)
    void doSomething() {
        logger->write("Message");
    }
    void setLogger(std::shared_ptr log) {         // (2)
        logger = log;
    }
private:
    std::shared_ptr logger;
};
        

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

the client cl requires a logger function. First the logger SimpleLogger injected via the constructor (line 1), after which the logger is replaced by the more powerful logger TimeLogger replaced. that setter-Member function allows to inject the new logger. The client is completely disconnected from the logger. It only supports interfaces for injecting loggers.

Here follows the output of the program:



There are many examples of dependency injection based on template parameters in the Standard Template Library. I mention the containers here as an example.

  • STL's containers use a default allocator. This can be replaced by your own allocator.
  • The ordered associative container uses std::less as a sorting criterion. Alternatively, another sorting criterion can also be used.
  • The unordered associative containers need a hash function and an equality function. Both are template parameters and can therefore be replaced.

Finally, I will make a few brief assessments of the three remaining generational patterns.

Abstract factory

With the abstract factory, you can create families of related objects without having to specify their concrete classes. A typical example is an IDE theme composed of many related objects. For example, each IDE theme has different widgets such as checkboxes, sliders, buttons, radio buttons, etc. A specific IDE theme typically has different factory methods for the different widgets. A customer can change the IDE theme and thus the widgets while using the IDE.

Builder

The Builder pattern builds complex objects step by step. This pattern makes it possible to create different types and representations of an object with the same step-by-step construction process. The Builder pattern extracts the object construction code from its class and puts it into separate objects called builders. Not all steps in the build process need to be called, and a step can have more than one builder.

prototype

The prototype pattern creates objects by cloning an existing object. The article "Software development: design pattern factory method without problems" already puts the prototype pattern in the program factoryMethodWindowSlicingFixed.cpp around. The prototype pattern is similar to the factory method, but focuses on the initialization of the prototypes that are created. The factory method creates various objects by delegating object creation to subclasses.

My next articles on design patterns will be dedicated to structural patterns. I'll start with the Adapter pattern, which can be implemented in C++ in two ways: multiple inheritance and delegation.


(Map)

To the home page

Leave a Comment