Exploring Creational Design Patterns in Real-Life Applications: From Singleton to Prototype

Creational design patterns in Java provide ways to create objects while abstracting the instantiation process. This article delves into the Singleton, Factory Method, Abstract Factory, Builder, and Prototype patterns, demonstrating their application in real-life scenarios.

1. Singleton Pattern

When to Use:

  • When a single instance of a class must control access to shared resources.
  • When you want to ensure that a single instance of a class exists throughout the application lifecycle.

Why to Use:

  • To control concurrent access to a resource.
  • To prevent multiple instances that could lead to inconsistent states or errors.

Example Use Case:

  • Logging Service: Ensures that only one instance writes logs to avoid conflicts.
  • Configuration Settings: Shared across different parts of an application.

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        // private constructor to prevent instantiation
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}        

2. Factory Method Pattern

When to Use:

  • When the exact type of object to be created is determined by subclasses.
  • When a class cannot anticipate the type of objects it needs to create beforehand.
  • When you want to localize the logic to instantiate a particular class.

Why to Use:

  • To achieve loose coupling by reducing the dependency on concrete classes.
  • To delegate the responsibility of instantiation to subclasses, making the code more flexible and scalable.

Example Use Case:

  • Document Creation: Creating different types of documents (Word, PDF, etc.) based on the input type.

abstract class Document {
    public abstract void open();
}

class WordDocument extends Document {
    @Override
    public void open() {
        System.out.println("Opening Word document");
    }
}

class PDFDocument extends Document {
    @Override
    public void open() {
        System.out.println("Opening PDF document");
    }
}

abstract class DocumentFactory {
    public abstract Document createDocument(String type);
}

class ConcreteDocumentFactory extends DocumentFactory {
    @Override
    public Document createDocument(String type) {
        if (type.equals("Word")) {
            return new WordDocument();
        } else if (type.equals("PDF")) {
            return new PDFDocument();
        } else {
            throw new IllegalArgumentException("Unknown document type");
        }
    }
}        

3. Abstract Factory Pattern

When to Use:

  • When a system needs to be independent of how its products are created, composed, and represented.
  • When a system should be configured with one of multiple families of products.
  • When families of related product objects are designed to be used together, and you want to enforce this constraint.

Why to Use:

  • To provide a way to encapsulate a group of individual factories with a common goal.
  • To ensure that the client code works with any of the product families, abstracting the creation process.

Example Use Case:

  • Cross-Platform UI: A UI toolkit that supports different look-and-feel standards (Windows, macOS, Linux).

interface Button {
    void paint();
}

class WindowsButton implements Button {
    public void paint() {
        System.out.println("Rendering a button in a Windows style");
    }
}

class MacOSButton implements Button {
    public void paint() {
        System.out.println("Rendering a button in a macOS style");
    }
}

interface GUIFactory {
    Button createButton();
}

class WindowsFactory implements GUIFactory {
    public Button createButton() {
        return new WindowsButton();
    }
}

class MacOSFactory implements GUIFactory {
    public Button createButton() {
        return new MacOSButton();
    }
}        

4. Builder Pattern

When to Use:

  • When you need to construct a complex object step by step.
  • When you need to create different representations of a complex object.

Why to Use:

  • To provide a clear separation between the construction and representation of an object.
  • To simplify the creation of complex objects that require numerous parameters or steps.

Example Use Case:

  • Creating Complex Objects: Such as vehicles or houses where various parts need to be built in a specific sequence or configuration.

class Car {
    private String engine;
    private String wheels;
    private String body;

    private Car(CarBuilder builder) {
        this.engine = builder.engine;
        this.wheels = builder.wheels;
        this.body = builder.body;
    }

    public static class CarBuilder {
        private String engine;
        private String wheels;
        private String body;

        public CarBuilder setEngine(String engine) {
            this.engine = engine;
            return this;
        }

        public CarBuilder setWheels(String wheels) {
            this.wheels = wheels;
            return this;
        }

        public CarBuilder setBody(String body) {
            this.body = body;
            return this;
        }

        public Car build() {
            return new Car(this);
        }
    }
}        

5. Prototype Pattern

When to Use:

  • When creating an instance is costly in terms of time or resources, and you want to create new instances by copying existing ones.
  • When a system should be independent of how its products are created, composed, and represented.
  • When instances of a class have only a few different state configurations, and you want to avoid creating subclasses for each configuration.

Why to Use:

  • To reduce the cost of creating objects by copying existing instances.
  • To avoid the complexity of creating new objects through traditional methods.

Example Use Case:

  • Cloning Complex Objects: Creating new user sessions in a web application by copying an existing session prototype.

interface Prototype extends Cloneable {
    Prototype clone();
}

class UserSession implements Prototype {
    private String userName;
    private String userPreferences;

    public UserSession(String userName, String userPreferences) {
        this.userName = userName;
        this.userPreferences = userPreferences;
    }

    @Override
    public Prototype clone() {
        return new UserSession(userName, userPreferences);
    }

    @Override
    public String toString() {
        return "UserSession [userName=" + userName + ", userPreferences=" + userPreferences + "]";
    }
}        

Summary

  • Singleton: Use when you need a single instance across the system to ensure consistency and control access to shared resources.
  • Factory Method: Use when you need a class to delegate the responsibility of instantiating objects to its subclasses.
  • Abstract Factory: Use when you need to create families of related or dependent objects without specifying their concrete classes.
  • Builder: Use when you need to construct complex objects step by step, with the flexibility to create different representations.
  • Prototype: Use when creating an instance is costly or when you need to create new objects by copying existing ones.

要查看或添加评论,请登录

Irfan Ahmed Mohammad的更多文章

  • Exploring Structural Design Patterns in Java: From Adapter to Flyweight

    Exploring Structural Design Patterns in Java: From Adapter to Flyweight

    Introduction: In the realm of software design, Structural Design Patterns play a crucial role in organizing code that…

  • System Design Essentials: The Role of API Gateway

    System Design Essentials: The Role of API Gateway

    In the realm of system design interviews, understanding the pivotal role of API Gateways (AGs) is not just advantageous…

    2 条评论
  • System Design Essentials: The Role of Load Balancers

    System Design Essentials: The Role of Load Balancers

    In system design interviews, think of Load Balancers (LB) as gatekeepers at the entrance of a network. They're like…

    1 条评论
  • 268. Missing Number

    268. Missing Number

    Code: class Solution: def missingNumber(self, nums: List[int]) -> int: n = len(nums) res = n…

  • 190. Reverse Bits

    190. Reverse Bits

    Code class Solution: def reverseBits(self, n: int) - int: res = 0 for i in range(32):…

  • 191. Number of 1 Bits

    191. Number of 1 Bits

    Code class Solution: def hammingWeight(self, n: int) -> int: while n: n &= (n-1)…

  • 128. Longest Consecutive Sequence

    128. Longest Consecutive Sequence

    Code class Solution: def longestConsecutive(self, nums: List[int]) -> int: seen = set(nums) res = 0…

  • 238. Product of Array Except Self

    238. Product of Array Except Self

    Code class Solution: def productExceptSelf(self, nums: List[int]) -> List[int]: res = [1]*len(nums)…

  • 49. Group Anagrams

    49. Group Anagrams

    Approach 1 Using Sorted string as key class Solution def groupAnagrams(self, strs: List[str]) -> List[List[str]]:…

    1 条评论
  • 217. Contains Duplicate

    217. Contains Duplicate

    Code class Solution def containsDuplicate(self, nums: List[int]) -> bool: hash_set = set() for item…

社区洞察

其他会员也浏览了