Factory Design Pattern

Factory Pattern is creational pattern that delegate the instantiation of objects to a separate method or class.

The Factory design pattern provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that’ll be created. This pattern helps to decouple the client code from the concrete classes it needs to instantiate.

Core Components

The Factory pattern has three main components:

  1. Product: This is the interface that all objects created by the factory must implement.

  2. Concrete Products: These are the actual implementations of the Product interface. The factory will create these objects.

  3. Creator (Factory) : This class declares the method that returns a Product object.

Use cases

  • When you need to create objects without exposing instantiation logic
  • When your code needs to work with multiple related classes
  • When object creation involves complex logic

Example

  1. Create an Interface (Product)
public interface Shape {
    void draw();
}
  1. Concrete Implementations (Concrete Products)
public class Circle implements Shape {
    public void draw() {
        System.out.println("Drawing a Circle");
    }
}

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

public class Square implements Shape {
    public void draw() {
        System.out.println("Drawing a Square");
    }
}
  1. Factory Class (Creator)
public class ShapeFactory {

    // Factory method
    public Shape getShape(String shapeType) {
        if (shapeType == null) {
            return null;
        }

        switch (shapeType.toLowerCase()) {
            case "circle":
                return new Circle();
            case "rectangle":
                return new Rectangle();
            case "square":
                return new Square();
            default:
                throw new IllegalArgumentException("Unknown shape type: " + shapeType);
        }
    }
}
  1. Client Code
public class Main {
    public static void main(String[] args) {
        ShapeFactory factory = new ShapeFactory();

        Shape shape1 = factory.getShape("circle");
        shape1.draw();

        Shape shape2 = factory.getShape("rectangle");
        shape2.draw();

        Shape shape3 = factory.getShape("square");
        shape3.draw();
    }
}

Kotlin Implementation

Using sealed classes improves type safety and ensures all cases are handled at compile time.

sealed class ShapeType {
    object Circle : ShapeType()
    object Square : ShapeType()
    object Rectangle : ShapeType()
}

interface Shape {
    fun draw()

    // Creator (Factory Method)
    companion object {
        fun create(type: ShapeType): Shape = when (type) {
            ShapeType.Circle -> Circle()
            ShapeType.Square -> Square()
            ShapeType.Rectangle -> Rectangle()
        }
    }
}

// concrete creators
class Circle : Shape {
    override fun draw() = println("Drawing a Circle")
}

class Square : Shape {
    override fun draw() = println("Drawing a Square")
}

class Rectangle : Shape {
    override fun draw() = println("Drawing a Rectangle")
}

// client code
fun main() {
    val shape = Shape.create(ShapeType.Rectangle)
    shape.draw()
}

Drawbacks

  • Code can become bloated with many conditionals
  • Not ideal for few object types

We can use Abstract Factory Pattern to fix code bloat.

Abstract Factory Design Pattern

The Abstract Factory Pattern is an extension of the Factory Method Pattern. Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes.

Core Components

The Abstract Factory pattern consists of four main roles:

  1. Abstract Product: These are the interfaces for each type of product in the family (e.g., Button, Checkbox).

  2. Concrete Product: These are the actual implementations of the Abstract Products, grouped by their specific variant (e.g., AndroidButton, iOSButton).

  3. Abstract Factory: This is an interface that declares the creation methods for each distinct product in the product family.

  4. Concrete Factory: These classes implement the Abstract Factory interface, with each one corresponding to a specific variant or family of products. They are responsible for creating the concrete products.

Example

  1. Product Interfaces
public interface Button {
    void click();
}

public interface Checkbox {
    void check();
}
  1. Concrete Products
public class AndroidButton implements Button {
    public void click() {
        System.out.println("Android Button Clicked");
    }
}

public class AndroidCheckbox implements Checkbox {
    public void check() {
        System.out.println("Android Checkbox Checked");
    }
}

public class IOSButton implements Button {
    public void click() {
        System.out.println("iOS Button Clicked");
    }
}

public class IOSCheckbox implements Checkbox {
    public void check() {
        System.out.println("iOS Checkbox Checked");
    }
}
  1. Abstract Factory Interface
public interface UIFactory {
    Button createButton();
    Checkbox createCheckbox();
}
  1. Concrete Factories
public class AndroidUiFactory implements UIFactory {
    @Override
    public Button createButton() {
        return new AndroidButton();
    }
    @Override
    public Checkbox createCheckbox() {
        return new AndroidCheckbox();
    }
}

public class IOSUiFactory implements UIFactory {
    @Override
    public Button createButton() {
        return new IOSButton();
    }
    @Override
    public Checkbox createCheckbox() {
        return new IOSCheckbox();
    }
}
  1. Client Code
public class Application {
    private Button button;
    private Checkbox checkbox;

    public Application(UIFactory factory) {
        this.button = factory.createButton();
        this.checkbox = factory.createCheckbox();
    }

    public void render() {
        button.click();
        checkbox.check();
    }

    public static void main(String[] args) {
        UIFactory factory;
        String osName = "iOS"; // This could be determined at runtime, like Platform.getName();

        if (osName.toLowerCase().contains("iOS", true)) {
            factory = new IOSUiFactory();
        } else {
            factory = new AndroidUiFactory();
        }

        Application app = new Application(factory);
        app.render();
    }
}

Kotlin Implementation

Instead of creating new factory instances, we can use use object for singleton-like factories.

// Product Interfaces
// We use a sealed interface to restrict the possible implementations of Button and Checkbox
// This allows for exhaustive 'when' expressions in consuming code.
sealed interface Button {
    fun click()
}

sealed interface Checkbox {
    fun check()
}

// Concrete Products
class AndroidButton : Button {
    override fun click() {
        println("Android Button Clicked")
    }
}

class AndroidCheckbox : Checkbox {
    override fun check() {
        println("Android Checkbox Checked")
    }
}

class IOSButton : Button {
    override fun click() {
        println("iOS Button Clicked")
    }
}

class IOSCheckbox : Checkbox {
    override fun check() {
        println("iOS Checkbox Checked")
    }
}

// Abstract Factory Interface
sealed interface UIFactory {
    fun createButton(): Button
    fun createCheckbox(): Checkbox
}

// Concrete Factories
// We use 'object' declarations for stateless, singleton factories.
// This is more concise and thread-safe than a class with a companion object.
object AndroidUiFactory : UIFactory {
    override fun createButton(): Button = AndroidButton()
    override fun createCheckbox(): Checkbox = AndroidCheckbox()
}

object IOSUiFactory : UIFactory {
    override fun createButton(): Button = IOSButton()
    override fun createCheckbox(): Checkbox = IOSCheckbox()
}

// Client Code
// Remains similar to the Java version.
class Application(private val factory: UIFactory) {
    private val button: Button = factory.createButton()
    private val checkbox: Checkbox = factory.createCheckbox()

    fun render() {
        button.click()
        checkbox.check()
    }
}

fun main() {
    val osName = "iOS" // This would be determined dynamically in a real app

    val factory: UIFactory = when (osName.lowercase()) {
        "ios" -> IOSUiFactory
        else -> AndroidUiFactory
    }

    val app = Application(factory)
    app.render()
}

Abstract Factory vs Factory Method

Feature Abstract Factory Factory Method
Purpose Creates entire families of related objects. Creates a single object.
Structure A “factory of factories” where a factory class creates multiple types of products. A single factory method in a superclass that subclasses override to create objects.
Complexity More complex, as it involves multiple interfaces and classes. Simpler, often implemented with just a few classes.