In software engineering, Creational Design Patterns deal with object creation mechanisms, i.e. try to create objects in a manner suitable to the situation. The basic or ordinary form of object creation could result in design problems or added complexity to the design. In this article of the Creational Design Patterns, we’re going to take a look at the much-hated & commonly asked design pattern in a programming interview. That is Singleton Design Pattern in Modern C++ which criticizes for its extensibility & testability. I will also cover the Multiton Design Pattern which quite contrary to Singleton.
By the way, If you haven’t check out my other articles on Creational Design Patterns, then here is the list:
The code snippets you see throughout this series of articles are simplified not sophisticated. So you often see me not using keywords like override, final, public(while inheritance) just to make code compact & consumable(most of the time) in single standard screen size.
I also prefer struct instead of class just to save line by not writing “public:” sometimes and also miss virtual destructor, constructor, copy constructor, prefix std::, deleting dynamic memory, intentionally. I also consider myself a pragmatic person who wants to convey an idea in the simplest way possible rather than the standard way or using Jargons.
Note:
To ensure one & only one instance of a class exist at any point in time.
/* country.txt
Japan
1000000
India
2000000
America
123500
*/
class SingletonDatabase {
std::map<std::string, int32_t> m_country;
SingletonDatabase() {
std::ifstream ifs("country.txt");
std::string city, population;
while (getline(ifs, city)) {
getline(ifs, population);
m_country[city] = stoi(population);
}
}
public:
SingletonDatabase(SingletonDatabase const &) = delete;
SingletonDatabase &operator=(SingletonDatabase const &) = delete;
static SingletonDatabase &get() {
static SingletonDatabase db;
return db;
}
int32_t get_population(const std::string &name) { return m_country[name]; }
};
int main() {
SingletonDatabase::get().get_population("Japan");
return EXIT_SUCCESS;
}
Some of the things to note here from the design perspective are:
struct SingletonRecordFinder {
static int32_t total_population(const vector<string>& countries) {
int32_t result = 0;
for (auto &country : countries)
result += SingletonDatabase::get().get_population(country);
return result;
}
};
vector<string> countries= {"Japan", "India"}; // Strongly tied to data base entries
TEST(1000000 + 2000000, SingletonRecordFinder::total_population(countries));
struct Database { // Dependency
virtual int32_t get_population(const string& country) = 0;
};
class SingletonDatabase : Database {
map<string, int32_t> m_countries;
SingletonDatabase() {
ifstream ifs("countries.txt");
string city, population;
while (getline(ifs, city)) {
getline(ifs, population);
m_countries[city] = stoi(population);
}
}
public:
SingletonDatabase(SingletonDatabase const &) = delete;
SingletonDatabase &operator=(SingletonDatabase const &) = delete;
static SingletonDatabase &get() {
static SingletonDatabase db;
return db;
}
int32_t get_population(const string &country) { return m_countries[country]; }
};
class DummyDatabase : public Database {
map<string, int32_t> m_countries;
public:
DummyDatabase() : m_countries{{"alpha", 1}, {"beta", 2}, {"gamma", 3}} {}
int32_t get_population(const string &country) { return m_countries[country]; }
};
/* Testing class ------------------------------------------------------------ */
class ConfigurableRecordFinder {
Database& m_db; // Dependency Injection
public:
ConfigurableRecordFinder(Database &db) : m_db{db} {}
int32_t total_population(const vector<string> &countries) {
int32_t result = 0;
for (auto &country : countries)
result += m_db.get_population(country);
return result;
}
};
/* ------------------------------------------------------------------------- */
int main() {
DummyDatabase db;
ConfigurableRecordFinder rf(db);
rf.total_population({"Japan", "India", "America"});
return EXIT_SUCCESS;
}
Due to Dependency Injection i.e. Database interface, our both following issues are resolved:
enum class Importance { PRIMARY, SECONDARY, TERTIARY };
template <typename T, typename Key = std::string>
struct Multiton {
static shared_ptr<T> get(const Key &key) {
if (const auto it = m_instances.find(key); it != m_instances.end()) { // C++17
return it->second;
}
return m_instances[key] = make_shared<T>();
}
private:
static map<Key, shared_ptr<T>> m_instances;
};
template <typename T, typename Key>
map<Key, shared_ptr<T>> Multiton<T, Key>::m_instances; // Just initialization of static data member
struct Printer {
Printer() { cout << "Total instances so far = " << ++InstCnt << endl; }
private:
static int InstCnt;
};
int Printer::InstCnt = 0;
int main() {
using mt = Multiton<Printer, Importance>;
auto main = mt::get(Importance::PRIMARY);
auto aux = mt::get(Importance::SECONDARY);
auto aux2 = mt::get(Importance::SECONDARY); // Will not create additional instances
return EXIT_SUCCESS;
}
What is so bad about the Singleton Design Pattern?
What is the correct way to implement Singleton Design Pattern?
The right way to implement Singleton is by dependency injection, So instead of directly depending on a singleton, you might want to consider it depending on an abstraction(e.g. an interface). I would also encourage you to use synchronization primitives(like a mutex, semaphores, etc) to control access.
When should you use the Singleton Design Pattern?
Previously published at http://www.vishalchovatiya.com/singleton-design-pattern-in-modern-cpp/