Skip to content

Singleton Pattern is No Option

LIU Hao edited this page Jul 1, 2023 · 6 revisions

The singleton pattern has been both famous and infamous for many years. In this article I will show you a tiny example why it is bad.

Suppose I have a fictional program that serves as a user database. There are some necessary classes, such as User_Data_Manager, Client_Connection_Manager, and Configuration_Manager. I know these are horrific names but meh let's ignore that for now.

The design is basing on a reasonable assumption that data are centralized. For example, when I need data about a specific user, I can do:

void
serve_request(uint32_t user_id)
  {
    shared_ptr<User> user = User_Manager::instance().find_user_by_id(user_id);
    // ... ...
  }

So what's the problem about this? One may expect a critical long-running process can be reconfigured without being restarted (sudo systemctl daemon-reload is a good example). This is usually served by a reload() function:

void
reload_all_modules()
  {
    Configuration_Manager::instance().reload();
    User_Data_Manager::instance().reload();
    Client_Connection_Manager::instance().reload();
  }

Reloading a module can fail or throw exceptions. What if User_Data_Manager::reload() throws an exception? It might leave User_Data_Manager in an inconsistent and unusable state. Even if we can implement every submodule with strong exception safety in mind, the entire reload_all_modules() function still does not guarantee strong exception safety; any exceptions thrown in the middle will render the entire system inconsistent.

The paradigm to address such issues is to copy-and-swap:

void
reload_all_modules()
  {
    Configuration_Manager::instance_two().reload();
    User_Data_Manager::instance_two().reload();
    Client_Connection_Manager::instance_two().reload();

    Configuration_Manager::instance().swap(Configuration_Manager::instance_two());  // noexcept
    User_Data_Manager::instance().swap(User_Data_Manager::instance_two());  // noexcept
    Client_Connection_Manager::instance().swap(Client_Connection_Manager::instance_two());  // noexcept
  }

And... they are violating the singleton pattern, because there are now two objects for each of them!

The conclusion is that singleton pattern is no option. Instead of having a class have two instances, it's much intuitive to have:

extern Configuration_Manager configuration_manager;
extern User_Data_Manager user_data_manager;
extern Client_Connection_Manager client_connection_manager;

void
reload_all_modules()
  {
    Configuration_Manager new_configuration_manager;
    User_Data_Manager new_user_data_manager;
    Client_Connection_Manager new_client_connection_manager;

    configuration_manager.swap(new_configuration_manager);  // noexcept
    user_data_manager.swap(new_user_data_manager);  // noexcept
    client_connection_manager.swap(new_client_connection_manager);  // noexcept
  }

No, no singleton pattern any more.

Clone this wiki locally