Creational Design Patterns are a set of design patterns that provide ways to create objects while abstracting the instantiation process. They include 5 patterns Builder, Factory/Factory Method/Abstract Factory, Prototype and Singleton, and help enhance code flexibility and maintainability in software development
Builder Design Pattern
Some objects are simple and can be created in a single constructor call, other objects require a LOT of ceremony to create
Having an object with 10 constructor arguments is not productive, similarly having numerous constructors each one taking a different number of parameters can be confusing(Telescoping Constructor Antipattern)
Builder pattern enables an API for constructing an object step by step
Suppose we want to build a complex object Computer(abstract) it can be concretely of two types Gaming Computer and Professional Computer, this doesn't have a public constructor(as it would have an obscene number of arguments) but the public functions all need to be called to build it piecewise
The Builder design pattern elegantly solves the problem in this code by enabling the step-by-step construction of complex computer objects while keeping the construction process abstracted from the final object.
The abstract ComputerBuilder class defines a blueprint for builders, which concrete builders like GamingComputerBuilder and ProfessionalComputerBuilder implement to create specific computer configurations.
The ComputerDirector class acts as the orchestrator, using the builder to construct computers with precise attributes.
This separation of concerns enhances code maintainability and flexibility, allowing for easy addition of new builders for different computer configurations in the future, making it a valuable pattern for building complex objects systematically and efficiently.
Note:
Builder should only be used when there are a lot of arguments(>5) for initialization and many of them are optional
It's a common practice to implement the builder design pattern with Fluent Interface allowing us to chain the build commands. In the director ie computerBuilder->buildCpu()->buildGpu()->buildRam()->buildStorage()
Note: SQL builder, JSON object builder all use builder design patterns
Factory Design Pattern
The Factory Design Pattern is used in software development to abstract and centralize the creation of objects, solving the problem of dynamic object instantiation.
This pattern is ideal for scenarios where different objects must be created based on runtime conditions
-
Suppose we want to create instances of different vehicle types (Car, Bike, and Truck) based on a given string, such as "Car," "Bike," or "Truck."
Without the Factory Design Pattern, you might have to create these objects directly in the client code using constructors. This approach would also require modifying client code whenever new vehicle types are introduced and we would not be able to reuse and encapsulate creation logic
We can move this modification to the factory instead (it will still violate OCP here but still better than doing this on the client side )
The Factory Design Pattern provides an elegant solution to this problem. The VehicleFactory
class acts as a factory for creating instances of Vehicle
subclasses. By calling the createVehicle
method with a string representing the desired vehicle type, such as "Car," "Bike," or "Truck," the factory returns a concrete instance of the appropriate vehicle class.
This abstraction allows for flexible and scalable object creation without exposing the client code to the intricacies of constructing objects.
If new vehicle types are added in the future, you can extend the factory without modifying the existing client code.
This pattern promotes code reusability, maintainability, and separation of concerns, making it a valuable tool for managing object creation in software development.
Factory vs. Factory Method
Factory design pattern provides a centralized way to create objects, returning instances of concrete classes.
Factory Method pattern is a variation of Factory, allowing subclasses/corresponding factories to create specific object types. ie earlier a single factory was creating all concrete objects, now each concrete object's creation is delegated to its unique factory which encapsulates its special creation logic
Now if a new vehicle type is added, instead of adding an if-else in the centralised factory, we would need to create a subclass, a factory for this new vehicle type first only this factory will be able to give me the new vehicle concrete type now
Factory design pattern is suitable for centralized object creation, while Factory Method promotes extensibility (OCP is not violated) by deferring object creation to subclasses/factories.
Choose Factory for uniform object creation and Factory Method when different subclasses need to customize object creation.
Factory vs. Factory Method vs. Abstract Factory
- The Factory pattern centralizes object creation, encapsulating logic in separate methods, while the Factory Method delegates object creation to subclasses for customization. The Abstract Factory pattern creates families of related objects via abstract factory methods, ensuring compatibility among them.
Factory Pattern: Choose when you need a centralized approach to create objects with a consistent interface.
Factory Method Pattern: Choose this when you have related classes, and each subclass/corresponding factory should customize object creation, providing much more flexibility and extensibility to instance creation.
Abstract Factory Pattern: Choose when you require compatible families of objects created via families of factories
Prototype
All about object copying, used when it is easier to copy an existing instance and modify it instead of entirely creating a new one from scratch
A partially or fully constructed object that you make a copy of is called a Prototype, we have to copy it (deep copy) and customise it
For example, we want to create a system for employee records, each contact detail has many common things, so we can create different contacts with the same contact prototype
-
Output:
name: John Doe works at street: Some common street city: London suite: 0
name: John Doe works at street: Some common street city: London suite: 5
name: Jane Doe works at street: Differnt Steert city: London suite: 0
name: Jane Doe works at street: Differnt Steert city: Delhi suite: 0
Singleton Design Pattern
The Singleton pattern is used when we want to ensure that there is only one instance of a class throughout the lifetime of an application. This instance is shared and can be accessed from any part of the code.
To implement the Singleton pattern, we need to consider the following key components:
Private Constructor: The class should have a private constructor to prevent external instantiation.
Static Instance: A static member variable that holds the single instance of the class.
Static Access Method: A static method that provides access to the instance. This method should ensure that only one instance is created and returned.
Thread Safety: In multi-threaded environments, we need to ensure that the instance creation is thread-safe.
For example, let's look at a practical implementation of the Singleton pattern in C++ using a Logger class as an example.
-
"new instance created for a singleton !!!!": This message signifies the creation of a single instance of the
Logger
class during the entire program execution, although there are two instance calls and three threads, a key characteristic of the Singleton pattern."Both instances same": This message confirms that both
logger
andlogger2
pointers point to the same instance of theLogger
class, demonstrating the Singleton pattern's single instance constraint."Logs by main user" and "Logs again by main user": These log messages illustrate that the main thread is using the same shared
Logger
instance for logging. The Singleton pattern ensures that the single instance is accessible from different parts of the program."Logs by user 1" and "Logs by user 2": These log messages show that two separate threads (
t1
andt2
) also use the same sharedLogger
instance for logging. The Singleton pattern guarantees global access and consistency of the instance across different program components.
How to ensure singleton is thread-safe?
Double-checked locking is an optimization technique used in multi-threaded programming to reduce the overhead of acquiring locks and improve performance when initializing a shared resource like a Singleton. It's often used in scenarios where you want to check if an instance has been created before acquiring a lock to create it.
Standard Locking:
In multi-threaded programs, when multiple threads can access and potentially create a shared resource (e.g., a Singleton), it's essential to ensure that only one thread initializes the resource. This is typically achieved by using locks or mutexes to provide thread safety.
Double-Checked Locking:
First Check: Before acquiring a lock, a thread checks if the shared resource has already been initialized. If it has, the thread can simply use the existing instance without acquiring the lock. This step is the "double check."
Acquire Lock: If the shared resource has not been initialized (i.e., it's null or in an uninitialized state), the thread enters a critical section where it acquires a lock. This prevents other threads from simultaneously initializing the resource.
Second Check: After acquiring the lock, the thread should again check if the resource has been initialized, as another thread might have initialized it while waiting for the lock. If the resource is still uninitialized, the thread initializes it.
Release Lock: Finally, the thread releases the lock, allowing other threads to access the initialized resource concurrently.
Other ways of creating a singleton
Eager Initialization:
Description: Eager initialization creates the Singleton instance at the time when the class is loaded or when it's first requested.
C++ Implementation: In C++, eager initialization can be achieved by declaring a static member of the Singleton class and initializing it immediately when declaring it.
Lazy (Late) Initialization:
Description: Lazy initialization creates the Singleton instance only when it's first requested
C++ Implementation: same as the code which used double-checked locking
Additional Notes
-
Unlike Builder which is used for the piecewise creation of objects, factory generally consists of the wholesale creation of objects
The Builder pattern differs from the Abstract Factory pattern and the Factory Method pattern, in that the intention of those two is to enable polymorphism, while the intention of the builder pattern is to find a solution to the Telescoping Constructor anti-pattern. To solve the telescoping constructor problem, instead of using numerous constructors, the builder pattern uses a builder, that is another object that receives each initialization parameter step by step and then returns the resulting constructed object at once.
Footnotes: