[definition, purpose, use cases, design, example, implementation]
Creational design patterns are concerned with the process of object creation. They provide various ways to create objects while hiding the complexities of the instantiation process.
A design pattern that ensures a class has only one instance and provides a global point of access to that instance.
This is useful when you want to control access to a shared resource, like a configuration object or a connection pool.
Single Instance: Ensures that only one instance of the class is created.Global Access: Provides a global point of access to the instance.Lazy Initialization: The instance is created only when it is needed (optional but common practice).Eager Initialization: The instance is created at the time of class loading. This is simple but can lead to unnecessary resource usage if the instance is never used.Let’s implement a simple Java class that behaves like a Singleton class
SingletonClass for which you want only one instance created (lazy initialization)public class SingletonClass {
public static SingletonClass instance;
// (eager initialization)
// public static SingletonClass instance = new SingletonClass();
// blocking the constructor so it's not accessible for multiple instantiations
private SingletonClass() {
}
/*
'synchronized' so that even when multiple threads try to access,
only one thread can instantiate it & the others will use that instance
*/
public static synchronized SingletonClass getInstance() {
// (lazy initialization)
// 'instance==null' check to ensure all the threads use the one instance that's already created
if (instance==null) {
instance = new SingletonClass();
}
return instance;
}
}
public static void main (String[] args) {
SingletonClass instance1 = SingletonClass.getInstance();
SingletonClass instance2 = SingletonClass.getInstance();
System.out.println(instance1 == instance2); // true, same instance
}
</br>
It provides an interface for creating objects, allowing subclasses to alter the type of objects that will be created.
Used in various scenarios where object creation requires flexibility, decoupling, or the creation logic can change based on specific conditions without the client having to know which subclasses to create.
Payment payment = PaymentFactory.getPaymentMethod("GooglePay");
payment.process();
MediaFile file = MediaFactory.getMediaFile("MP4");
file.play();
DocumentHandler handler = DocumentHandlerFactory.getHandler("v2");
Product interface: declares the interface of the objects that the factory method createsConcrete Product classes: these classes implement the product interfaceCreator (Factory) class: declares the factory method, which returns an object of type ProductConcrete Creator: implements the factory method to create instances of concrete product types.Let’s say we have different vehicles like Bike, Car, etc. We want the client to initiate instances as per required without having to know about the associated concrete classes.
Vehicle interfaceinterface Vehicle {
void drive();
}
Car, Bike, etc.public class Car implements Vehicle {
@Override
void drive() {
System.out.println("Driving a Car...");
}
}
public class Bike implements Vehicle {
@Override
void drive() {
System.out.println("Driving a Bike...");
}
}
Factory class with methods to decide the type of class for the object (object creation logic)public class VehicleFactory {
public Vehicle getVehicle(String vehicleName) {
if(vehicleName.equalsIgnoreCase("car")) {
return new Car();
}
else if(vehicleName.equalsIgnoreCase("bike")) {
return new Bike();
}
return null;
}
}
Client code to demonstrate the usage
public static void main (String[] args) {
VehicleFactory vehicleFactory = new VehicleFactory();
Vehicle vehicle1 = vehicleFactory.getVehicle("car");
Vehicle vehicle2 = vehicleFactory.getVehicle("bike");
vehicle1.drive(); // Driving a Car...
vehicle2.drive(); // Driving a Bike...
}
</br>
A design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes.
In factory method, we have an abstraction at product level.
Here, we have another layer of abstraction at the factory class to group the concrete products.
It’s useful when you need to create objects from several related classes without knowing their exact types.
Abstract Product interface: Declares the interface for a type of product.Concrete Product: Implements the abstract product interface.Abstract Factory interface: Declares the creation methods for each product type.Concrete Factory: Implements the creation methods for the specific product family.Client: Uses the factories to get instances of products but is unaware of the specific classes being created.Let’s take the same example as above - vehicles like Car, Bike but with subcategories of Luxury vehicles & Economic vehicles
Vehicle interfaceinterface Vehicle {
void move();
}
ConcreteProduct classespublic class EconomicCar implements Vehicle {
@Override
void move() {
System.out.println("Driving an Economic Car...");
}
}
public class LuxuryCar implements Vehicle {
@Override
void move() {
System.out.println("Driving a Luxury Car...");
}
}
public class EconomicBike implements Vehicle {
@Override
void move() {
System.out.println("Riding an Economic Bike...");
}
}
public class LuxuryBike implements Vehicle {
@Override
void move() {
System.out.println("Riding a Luxury Bike...");
}
}
VehicleFactory interfacepublic interface VehicleFactory {
Vehicle getVehicle(String vehicleName);
}
ConcreteVehicleFactory classes that group the productspublic class EconomicVehicleFactory implements VehicleFactory {
@Override
Vehicle getVehicle(String vehicleName) {
if (vehicleName.equalsIgnoreCase("car")) {
return new EconomicCar();
}
else if(vehicleName.equalsIgnoreCase("bike")) {
return new EconomicBike();
}
return null;
}
}
public class LuxuryVehicleFactory implements VehicleFactory {
@Override
Vehicle getVehicle(String vehicleName) {
if (vehicleName.equalsIgnoreCase("car")) {
return new LuxuryCar();
}
else if(vehicleName.equalsIgnoreCase("bike")) {
return new LuxuryBike();
}
return null;
}
}
Client code to demonstrate the usagepublic static void main (String[] args) {
VehicleFactory economicFactory = new EconomicVehicleFactory();
VehicleFactory luxuryFactory = new LuxuryVehicleFactory();
Vehicle vehicle1 = economicFactory.getVehicle("car");
Vehicle vehicle2 = luxuryFactory.getVehicle("car");
Vehicle vehicle3 = economicFactory.getVehicle("bike");
Vehicle vehicle4 = luxuryFactory.getVehicle("bike");
vehicle1.move(); // Driving an Economy Car...
vehicle2.move(); // Driving a Luxury Car...
vehicle3.move(); // Riding an Economic Bike...
vehicle4.move(); // Riding a Luxury Bike...
}
</br>
The pattern separates the construction of an object from its representation, allowing for more control over the object creation process.
When an object needs to be created with many possible configurations or when the construction process involves multiple steps.
Scenario: An object has several optional parameters that may or may not be used. Passing null or using multiple overloaded constructors can lead to unreadable code.
Scenario: When you want to build an object with many configurable properties while ensuring immutability. The builder pattern helps ensure that once an object is constructed, it cannot be modified.
Use Case: Creating immutable classes like Person, Book, or Bank Account, where you want to avoid setters and ensure the object is completely initialized once constructed.
Example: An immutable Person object with mandatory parameters (name, age) and optional parameters (address, phone number, etc.).
Scenario: You may need to build different representations of the same object depending on the context (e.g., different variations or versions of the same object).
Use Case: When you need multiple representations of an object, such as building Report Generators, Document Parsers, or HTML/JSON/XML builders.
Example: A Document object that can be represented as HTML, PDF, or plain text, with common elements like title, body, and footer.
Builder: A separate object that builds the desired object step by step.Product: The object that the builder constructs.Director: (Optional) An object that controls the construction process, ensuring that all necessary steps are performed.Imagine you want to build a house object, and it has multiple optional and mandatory fields. Instead of having a constructor with many parameters, you can use the builder pattern to construct the house step by step.
House class with its fields & also a static class HouseBuilder inside it that builds the object of House type.// Product class
public class House {
// Required parameters
private final String foundation;
private final String structure;
// Optional parameters
private final boolean hasGarage;
private final boolean hasSwimmingPool;
private final boolean hasGarden;
// Private constructor
private House(HouseBuilder builder) {
this.foundation = builder.foundation;
this.structure = builder.structure;
this.hasGarage = builder.hasGarage;
this.hasSwimmingPool = builder.hasSwimmingPool;
this.hasGarden = builder.hasGarden;
}
@Override
public String toString() {
return "House{" +
"foundation='" + foundation + '\'' +
", structure='" + structure + '\'' +
", hasGarage=" + hasGarage +
", hasSwimmingPool=" + hasSwimmingPool +
", hasGarden=" + hasGarden +
'}';
}
// Builder Class
public static class HouseBuilder {
// Required parameters
private final String foundation;
private final String structure;
// Optional parameters - initialize with default values
private boolean hasGarage = false;
private boolean hasSwimmingPool = false;
private boolean hasGarden = false;
// Builder constructor with required parameters
public HouseBuilder(String foundation, String structure) {
this.foundation = foundation;
this.structure = structure;
}
// Setters for optional parameters
public HouseBuilder setGarage(boolean hasGarage) {
this.hasGarage = hasGarage;
return this; // Return builder to allow chaining
}
public HouseBuilder setSwimmingPool(boolean hasSwimmingPool) {
this.hasSwimmingPool = hasSwimmingPool;
return this;
}
public HouseBuilder setGarden(boolean hasGarden) {
this.hasGarden = hasGarden;
return this;
}
// Build method to create the House object
public House build() {
return new House(this);
}
}
}
Client code to demonstrate the usagepublic class BuilderPatternExample {
public static void main(String[] args) {
// Using the builder to create a complex House object
House house = new House.HouseBuilder("Concrete", "Wood")
.setGarage(true)
.setSwimmingPool(false)
.setGarden(true)
.build();
System.out.println(house);
}
}
</br>
Explanation:
</br>
This pattern is useful when the cost of creating a new instance of an object is more expensive than copying an existing one.
Example: In scenarios where creating a new object is resource-intensive (due to expensive database calls, network requests, or complex initial setup), the Prototype pattern allows you to clone an existing object. This avoids unnecessary repetition of the heavy creation process.
Use Case: Creating a large number of similar objects in a graphic editor, where each object has similar properties, can be optimized by cloning.
Prototype Interface: This defines a method for cloning itself.Concrete Prototype: This is the class that implements the Prototype interface and provides the cloning functionality.Let’s illustrate this with a simple example involving shapes.
Prototype Interfacepublic interface Shape {
Shape clone();
void draw();
}
Concrete Prototype Classespublic class Circle implements Shape {
@Override
public Shape clone() {
return new Circle();
}
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}
public class Square implements Shape {
@Override
public Shape clone() {
return new Square();
}
@Override
public void draw() {
System.out.println("Drawing a Square");
}
}
public static void main(String[] args) {
// Create a circle and square
Shape circle = new Circle();
Shape square = new Square();
// Clone the shapes
Shape clonedCircle = circle.clone();
Shape clonedSquare = square.clone();
// Draw original and cloned shapes
circle.draw(); // Output: Drawing a Circle
clonedCircle.draw(); // Output: Drawing a Circle
square.draw(); // Output: Drawing a Square
clonedSquare.draw(); // Output: Drawing a Square
System.out.println(circle==clonedCircle); // false, different objects
}