Creational Design Patterns in Java

·

6 min read

Table of contents

No heading

No headings in the article.

Creational design patterns are a fundamental aspect of software design, focusing on object creation mechanisms. Instead of directly instantiating objects using new, these patterns provide techniques to create objects in a flexible and reusable manner. In this blog, we’ll explore Builder, Simple Factory, Factory Method, Prototype, Singleton, Abstract Factory, and Object Pool design patterns in Java with code examples, use cases, and potential drawbacks.

1. Builder Pattern

Definition:

The Builder Pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

Explanation:

  • Problem: Complex objects often require many optional or conditional attributes, leading to long and error-prone constructors.

  • Solution: The Builder Pattern provides a step-by-step way to construct an object, ensuring clarity and immutability.

  • Code Example:

      class Car {
          private String engine;
          private int wheels;
          private String color;
    
          public void setEngine(String engine) { this.engine = engine; }
          public void setWheels(int wheels) { this.wheels = wheels; }
          public void setColor(String color) { this.color = color; }
    
          @Override
          public String toString() {
              return "Car [Engine=" + engine + ", Wheels=" + wheels + ", Color=" + color + "]";
          }
      }
    
      class CarBuilder {
          private Car car;
    
          public CarBuilder() { car = new Car(); }
    
          public CarBuilder setEngine(String engine) {
              car.setEngine(engine);
              return this;
          }
    
          public CarBuilder setWheels(int wheels) {
              car.setWheels(wheels);
              return this;
          }
    
          public CarBuilder setColor(String color) {
              car.setColor(color);
              return this;
          }
    
          public Car build() { return car; }
      }
    
      public class BuilderExample {
          public static void main(String[] args) {
              Car car = new CarBuilder()
                      .setEngine("V8")
                      .setWheels(4)
                      .setColor("Red")
                      .build();
              System.out.println(car);
          }
      }
    

    Use Cases:

    • When constructing objects with many optional or varying attributes.

    • Examples: Building complex UI components, configuring database connections.

Drawbacks:

  • Requires a separate builder class, increasing complexity.

2. Simple Factory Pattern

Definition:

The Simple Factory Pattern centralizes object creation in one class, encapsulating the creation logic.

Explanation:

  • Problem: Direct instantiation using new leads to tight coupling between client code and concrete classes.

  • Solution: A factory class handles object creation, allowing the client to focus on usage.

Code Example:

    interface Shape {
        void draw();
    }

    class Circle implements Shape {
        @Override
        public void draw() { System.out.println("Drawing a Circle"); }
    }

    class Rectangle implements Shape {
        @Override
        public void draw() { System.out.println("Drawing a Rectangle"); }
    }

    class ShapeFactory {
        public static Shape getShape(String shapeType) {
            if (shapeType.equalsIgnoreCase("CIRCLE")) {
                return new Circle();
            } else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
                return new Rectangle();
            }
            return null;
        }
    }

    public class SimpleFactoryExample {
        public static void main(String[] args) {
            Shape shape = ShapeFactory.getShape("CIRCLE");
            shape.draw();
        }
    }

Use Cases:

  • When creating a few related object types.

  • Examples: Shapes, report generators.

Drawbacks:

  • Violates the Open/Closed Principle if new types are frequently added.

3. Factory Method Pattern

  • Definition:

    Factory Method defines an interface for creating objects, but allows subclasses to decide the exact instantiation.

    Explanation:

    • Problem: Modifying a simple factory to accommodate new types leads to frequent changes in the factory code.

    • Solution: Factory Method delegates object creation to subclasses, promoting the Open/Closed Principle.

Code Example:

    interface Animal {
        void sound();
    }

    class Dog implements Animal {
        @Override
        public void sound() { System.out.println("Bark!"); }
    }

    class Cat implements Animal {
        @Override
        public void sound() { System.out.println("Meow!"); }
    }

    abstract class AnimalFactory {
        public abstract Animal createAnimal();
    }

    class DogFactory extends AnimalFactory {
        @Override
        public Animal createAnimal() { return new Dog(); }
    }

    class CatFactory extends AnimalFactory {
        @Override
        public Animal createAnimal() { return new Cat(); }
    }

    public class FactoryMethodExample {
        public static void main(String[] args) {
            AnimalFactory factory = new DogFactory();
            Animal animal = factory.createAnimal();
            animal.sound();
        }
    }

Use Cases:

  • When adding new object types without modifying existing code is important.

  • Examples: Loggers, parsers.

Drawbacks:

  • Increases complexity due to multiple factory classes.

    4. Prototype Pattern

  • Definition:

    Prototype creates new objects by copying an existing instance.

    Explanation:

    • Problem: Creating new instances from scratch can be resource-intensive.

    • Solution: Use the prototype to clone existing objects.

Code Example:

        class Prototype implements Cloneable {
            private String name;

            public Prototype(String name) { this.name = name; }

            @Override
            protected Object clone() throws CloneNotSupportedException {
                return super.clone();
            }

            @Override
            public String toString() { return "Prototype [Name=" + name + "]"; }
        }

        public class PrototypeExample {
            public static void main(String[] args) throws CloneNotSupportedException {
                Prototype original = new Prototype("Original");
                Prototype clone = (Prototype) original.clone();
                System.out.println(original);
                System.out.println(clone);
            }
        }

Use Cases:

  • For duplicating objects in an object pool or game engine.

Drawbacks:

  • Cloning may not work for objects with deep structures.

5. Singleton Pattern

Definition:

The Singleton Pattern ensures a class has only one instance and provides a global point of access to it.

Code Example:

        class Singleton {
            private static Singleton instance;

            private Singleton() {}

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

        public class SingletonExample {
            public static void main(String[] args) {
                Singleton s1 = Singleton.getInstance();
                Singleton s2 = Singleton.getInstance();
                System.out.println(s1 == s2); // true
            }
        }

Use Cases:

  • Managing resources like database connections, caches, configuration settings.

Drawbacks:

  • Difficult to test due to global state.

6. Abstract Factory Pattern

Definition:

Abstract Factory provides an interface for creating families of related objects without specifying their concrete classes.

Code Example:

        interface Chair {
            void sitOn();
        }

        class VictorianChair implements Chair {
            @Override
            public void sitOn() { System.out.println("Sitting on a Victorian chair."); }
        }

        class ModernChair implements Chair {
            @Override
            public void sitOn() { System.out.println("Sitting on a Modern chair."); }
        }

        interface FurnitureFactory {
            Chair createChair();
        }

        class VictorianFurnitureFactory implements FurnitureFactory {
            @Override
            public Chair createChair() { return new VictorianChair(); }
        }

        class ModernFurnitureFactory implements FurnitureFactory {
            @Override
            public Chair createChair() { return new ModernChair(); }
        }

        public class AbstractFactoryExample {
            public static void main(String[] args) {
                FurnitureFactory factory = new ModernFurnitureFactory();
                Chair chair = factory.createChair();
                chair.sitOn();
            }
        }

Use Cases:

  • GUI frameworks, where families of related components (buttons, checkboxes) must be created.

Drawbacks:

  • Complex hierarchy.

7. Object Pool Pattern

Definition:

Manages a pool of reusable objects to improve performance in resource-intensive operations.

Code Example:

        import java.util.Queue;
        import java.util.LinkedList;

        class ObjectPool {
            private Queue<Reusable> pool = new LinkedList<>();

            public ObjectPool(int size) {
                for (int i = 0; i < size; i++) {
                    pool.add(new Reusable());
                }
            }

            public Reusable acquire() { return pool.poll(); }

            public void release(Reusable obj) { pool.add(obj); }
        }

        class Reusable {}

        public class ObjectPoolExample {
            public static void main(String[] args) {
                ObjectPool pool = new ObjectPool(2);

                Reusable obj1 = pool.acquire();
                pool.release(obj1);
            }
        }

Use Cases:

  • Database connections, thread pools.

Drawbacks:

  • Adds complexity in pool management.

Creational design patterns simplify object creation and improve code flexibility, maintainability, and scalability. Each pattern, from Builder to Singleton, addresses specific design needs, offering clear benefits but also potential drawbacks. By carefully selecting the right pattern for your project, you can create more modular, maintainable software while avoiding unnecessary complexity. The key is to strike a balance between flexibility and simplicity, ensuring the chosen pattern aligns with the problem you're solving.