Creational pattern
Creational patterns are a category of design patterns in software engineering that deal with object creation mechanisms. The idea is to create objects in a way that is flexible, reusable, and decoupled from the code that uses them.
Goals
- Decouple the system from how its objects are created.
- Make it easier to manage object creation, especially in complex scenarios.
- Promote flexibility and reuse of code.
Singleton pattern
A design pattern that ensures a class has only one instance and provides a global point of access. It is commonly used to manage shared resources such as configuration settings, logging, or database connections.
Key Features
- Single Instance: Only one object of the class is created.
- Global Access: The instance can be accessed globally.
Use Cases
- Database Connections
- Logging Systems
- Configuration Management
- Thread Pools
- Caching
Cons
- Global State & Hidden Dependencies
The Singleton instance is globally accessible, so different parts of the application may depend on it and creates hidden dependencies, making it harder to understand and maintain the code. - Makes Unit Testing Difficult
It’s difficult to mock or replace the instance, reducing testability. - Thread-Safety Issues
You have to take care, that the instance is thread safe implemented. - Hard to Extend & Maintain
Singleton restricts instantiation, extending or modifying behavior can be hard. On changes, all parts of the application that depend on it might break. - Can Lead to Performance Issues
Once created, a singleton stays throughout the application's lifetime. That might lead to memory issues, if it holds a higher amount of data.
//Thread safe implementation
public class Singleton {
private static Singleton instance;
private Singleton() {} // Hide constructor
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Builder pattern
The Builder Pattern is a creational design pattern used to construct complex objects step by step. Instead of creating an object directly with a constructor (which can become unwieldy with many parameters), the Builder Pattern provides a more flexible and readable way to create objects.
Key Features
- Step-by-Step Object Creation
Build an object piece by piece. - Readable and Maintainable Code
Eliminates the need for long constructors. - Immutable Object Creation
Create immutable objects with controlled configuration. - Different Configurations
Different variations of the same object can be created using the same builder.
Use Cases
- Complex Object Creation (e.g., a car configuration).
- Fluent API with method chaining.
- Immutable Object Creation
Cons
- Increased Complexity
The Builder Pattern requires an additional builder class, which adds extra code and complexity. Don't use it for simple classes. - More Boilerplate Code
Plenty of extra classes and methods need to be written. This makes the codebase larger and harder to maintain. - Performance Overhead
Builder Pattern often involves multiple method calls, which can lead to slightly lower performance. - Harder to Debug
Because object creation is spread across multiple methods, debugging issues can be harder than using a simple constructor.
When to Avoid
- If the object has few attributes (a constructor is simpler).
- When performance is significant.
- If the object does not have many variations often, making a builder unnecessary.
class Car {
private int size;
private boolean electric;
private Car(carBuilder builder) {
this.size = builder.size;
this.electric = builder.electric;
}
public static class CarBuilder {
private int size;
private boolean electric;
public CarBuilder(int size) {
this.size = size;
}
public CarBuilder addElectric() {
this.electric = true;
return this;
}
public Car build() {
return new Car(this);
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
Car car = new Car.CarBuilder(4).addElectric();
System.out.println(car);
}
}
Prototype
It allows you to create new objects by copying an existing object, known as the prototype, rather than instantiating new ones from scratch.
Key Features
- Instead of using a constructor (like new in many languages), you create a new object by cloning an existing object.
- Reduces the need for subclassing.
- Hides the complexity of object creation.
- Speeds up instantiation by cloning rather than building from scratch.
Use cases
- You need to create many objects that are similar or identical.
- Object creation is costly (e.g., resource-heavy initialization).
- You want to keep your code flexible and avoid tight coupling with concrete classes.
Cons
- Cloning can be tricky, especially with deep copies (i.e., copying nested objects).
- Objects need to implement a cloning method (e.g., clone() in Java).
// Prototype Interface
interface Prototype extends Cloneable {
Prototype clone();
}
// Concrete Class implementing Prototype
class Car implements Prototype {
private String type;
private int wheels;
public Car(String type, int wheels) {
this.type = type;
this.wheels = wheels;
}
public void setType(String type) {
this.type = type;
}
public void setWheels(int wheels) {
this.wheels = wheels;
}
public void display() {
System.out.println("Vehicle Type: " + type + ", Wheels: " + wheels);
}
@Override
public Prototype clone() {
try {
return (Prototype) super.clone(); // shallow copy
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car("Car", 4);
car.display();
Car clonedCar = (Vehicle) car.clone();
clonedCar.display();
// Change clone's state
clonedCar.setType("Truck");
clonedCar.setWheels(6);
System.out.println("After modifying the clone:");
car.display(); // Original remains unchanged
clonedCar.display(); // Modified clone
}
}
Factory
The Factory Pattern is another creational design pattern that provides an interface for creating objects but allows subclasses or methods to decide which class to instantiate.
Instead of calling a constructor directly, you use a factory method that abstracts the instantiation logic.
Key Features
- Encapsulates object creation logic.
- Promotes loose coupling by hiding specific classes from the client.
- Makes it easy to introduce new product types without changing existing code.
Cons
- Adds Complexity
Introducing a factory adds extra layers of code (interfaces, abstract classes, factory classes). That can be overkill for simple object creations. - Harder to Trace
The constructor is not called directly. Therefore, it can be harder to tell at a glance what object is actually being created. This might make debugging more complex. - Requires More Classes
You often end up with more classes and interfaces (e.g., product interfaces, factory classes). That can make the codebase harder to maintain. - Less Flexibility in Dynamic Object Creation
If not careful, the factory can become a giant if-else or switch statement, which isn't much better than hard-coding constructors. - Violates Open/Closed Principle (Sometimes)
If your factory needs to be modified every time you add a new product, it's not really "open for extension and closed for modification." To avoid this, you'd need more abstraction (e.g., reflection, configuration, dependency injection), which adds even more complexity.
// Product Interface
interface Shape {
void draw();
}
// Rectangle Products
class Rectangle implements Shape {
public void draw() {
System.out.println("Drawing a Rectangle");
}
}
class Square implements Shape {
public void draw() {
System.out.println("Drawing a Square");
}
}
// Factory
class ShapeFactory {
public Shape getShape(String shapeType) {
if (shapeType == null) return null;
if (shapeType.equalsIgnoreCase("Rectangle")) return new Rectangle();
if (shapeType.equalsIgnoreCase("Square")) return new Square();
return null;
}
}
public class Main {
public static void main(String[] args) {
ShapeFactory factory = new ShapeFactory();
Shape shape1 = factory.getShape("Rectangle");
shape1.draw(); // Output: Drawing a Rectangle
Shape shape2 = factory.getShape("Square");
shape2.draw(); // Output: Drawing a Square
}
}
Abstract Factory
The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Imagine you want to create objects from different product families (e.g., Button, Checkbox) — and you would like to be able to switch between different themes or platforms (like Windows, Mac, or Linux) without changing your client code.
Key features
The Abstract Factory lets you create objects from different product families by providing a factory interface for each family and concrete implementations for each variation.
Structure
AbstractFactory
│
├── createButton() → Button
├── createCheckbox() → Checkbox
│
├── ConcreteFactoryWindows
│ ├── createButton() → WindowsButton
│ └── createCheckbox() → WindowsCheckbox
│
└── ConcreteFactoryMac
├── createButton() → MacButton
└── createCheckbox() → MacCheckbox
- AbstractFactory defines the interface.
- ConcreteFactoryWindows and ConcreteFactoryMac implement it.
- Your code uses AbstractFactory, not the concrete ones directly.
Pros
- Encapsulates object creation logic.
- Makes it easy to switch between different product families.
- Ensures consistency among products in the same family.
Cons
- Can become complex as the number of product types grows.
- Adding new product types means modifying the interface and all factories.