Software Design Pattern By Example: Singleton
Explain Design Pattern in easy-to-understand example
(I know most of this blog’s readers are testers or automation/DevOps engineers. This one is about software design.)
Design Patterns are good practices of Object-Oriented (OO) Software Design. Applying appropriate design patterns will help to design your software better. However, design patterns are not easy to master, it was the case for me, and my daughter when she started an IT degree at Uni. I wrote a series of articles to help her learn Design Patterns quickly. My approach is to use easy-to-understand exercises, which I believe is an easier way to master design patterns. This article’s pattern is Singleton.
The implementation language will be C++, but you can try another OO language such as Java and Ruby.
Table of Contents:
· The Problem
· Analyse
· Sub-optimal Designs
∘ Static Method
∘ Static Member
· Singleton Pattern
∘ How does it work?
The Problem
In your application, the unique identifier of the machine (runs the program) is frequently used in the code. Considering getting a machine identifier is a relatively expensive operation (let’s say 1 second), how will you design the getMachineId()
function?
Analyse
1. Static Method
The getMachineId()
is a static member or method, as its value returned is always the same (i.e. static).
Static member vs Instance member
class Car {
public:
Car(std::string make, std::string model);
};class Sedan : Car {
public:
Sedan(std::string make, std::string model);
const static int number_of_wheels = 4;
};
With a static member, we can use it directly, without an instance (i.e. object).
std::cout << "Sedan has " << Sedan::number_of_wheels << " wheels\n";
As a comparison, here is how instance members are used.
Sedan my_car = new Sedan("Toyota", "Camry");
Sedan wife_car = new Sedan("Honda", "Accord");
std::cout << my_car.make << "\n";
std::cout << wife_car.model << "\n";
2. Not a constant
Because the machine ID is only available when the program runs on the client’s machine, we cannot hardcode it as a static constant.
3. Universally Unique Identifier (UUID)
UUID is a 128-bit number used to identify information in computer systems. UUID guarantees uniqueness, and for this reason, it has been widely used in the software. (maybe too much, especially in the .NET world, UUID has been used to identify web services, objects, …, etc)
For this exercise, we don’t need to worry about the implementation of getting Machine UUID, just returning a hard-coded UUID will do.
4. Test Harness
In the spirit of engineering (software design also known as software engineering), before designing potentially several solutions, we need to come up with a process (known as Test Harness) to verify them objectively.
Here is a test harness to benchmark the time took to call getMachineId()
5 times.
auto start_time = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 5; i++) {
std::cout << Constants::getMachineId() << std::endl;
}
auto finish_time = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> elapsed = finish_time - start_time;
std::cout << "Time took: " << elapsed.count() << " s" << std::endl;
Sub-optimal Designs
Let’s look at two straightforward designs.
Static Method
A common design is to encapsulate getting-machine-id in a static method, like the below:
class Constants {
public:
// a quite expensive operation
static std::string getMachineId() {
std::cout << "Loading Machine ID" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); //simulate
return "74AE4D56-83AC-ACE7-CE8C-3E2AF95B00BE";
}
};
However, this approach is not good. Because getMachineId()
is an expensive operation, calling this method (which always returns the same result) frequently is a time waste.
The sample output of our test harness:
[Static Method] Time took: 5.01563 s
Static Member
The standard approach to avoid calling a piece of code that returns an unchanged result is to store the result into a variable, in this case, a static member.
class Constants {
public:
static std::string machine_id;
static std::string getMachineId2() {
if (machine_id.empty())
machine_id = getMachineId(); // call actual getting UUID
return machine_id;
}
};std::string Constants::machine_id = "";
A sample output of the test harness:
[Static Member] Time took: 1.00011 s
Performance-wise, it is good. However, the design is still not optimal as it has some complications.
Cannot initialize static member in C++
For safe coding, we want to initialize the static member first. However,static std::string machine_id = ""
is not allowed in a C++ class definition, the error from the C++ compiler is: “Non-const static data member must be initialized out of line”.The initialization code outside the class definition does not look right
This statementstd::string Contants::machine_id = ""
` outside the class definition works, for computers, but maintenance programmers might not feel comfortable with it.
Some may say: we can use a static initializer, such as static { }
block of code inside Java class, which runs only one time before the constructor or main method is called. This is not good either, as it will add unnecessary delay to the program launch. (Static blocks are loaded first, users need to wait one extra second even though their day-to-day usage may have nothing to do with machine IDs).
There is a simple way: using Singleton Pattern.
Singleton Pattern
The idea of the Singleton Pattern is simple: there is only one instance of the class.
class Constants {
public:
static Constants& getInstance() {
static Constants instance; // Instantiated on first use.
return instance;
}
static std::string getMachineId() {
return getInstance().machine_id;
}private:
Constants() {
std::this_thread::sleep_for (std::chrono::seconds(1));
machine_id = "74AE4D56-83AC-ACE7-CE8C-3E2AF95B00BE";
}
std::string machine_id;
};
A sample output of the test harness:
for (int i = 0; i < 5; i++) {
std::cout << Constants::getMachineId() << std::endl;
}
Result:
[Singleton] Time took: 1.0023 s
How does it work?
Static Method → Instance Method, which returns the instant member machine_id
. The instant member is initialized once in the constructor. In other words, it is equivalent to Contants.getInstance().machine_id;
(if machine_id
is defined in `public` scope).
Constructor is `private`
This means we cannot create new objects ofConstants
usingnew Constants()
.Guaranteed only one instance
The only instance is defined in the static methodgetInstance
.The initialization code will be only executed once
Because there is only one instance, the code (for getting the machine ID) will be only executed once, and only whengetInstance
is called.
Related reading: