-
Notifications
You must be signed in to change notification settings - Fork 32
Singleton Pattern is No Option
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 to be able to be reconfigured without a restart (sudo systemctl daemon-reload
is a good example), which is usually served by a reload_XX()
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.