John Roest

The Factory Method: Encapsulating Object Creation

The Factory Method: Encapsulating Object Creation

The Problem#

Object creation starts simple: call new, pass some parameters, get an object. As the codebase grows and requirements shift, that simplicity erodes.

Consider a system that processes different document types—PDFs, Word files, spreadsheets. The initial implementation might look like this:

public Document openDocument(String type) {
    if (type.equals("PDF")) {
        return new PdfDocument();
    } else if (type.equals("Word")) {
        return new WordDocument();
    } else if (type.equals("Spreadsheet")) {
        return new SpreadsheetDocument();
    } else {
        throw new IllegalArgumentException("Unknown document type");
    }
}

This has several structural problems:

  1. Tight coupling: This method knows about every concrete document type.
  2. Closed to extension: Every new document type requires modifying this method.
  3. Violates Open/Closed Principle: The function changes every time a new type is added.
  4. Testing complexity: Unit tests must account for every branch.

Every new feature forces changes to existing code, increasing the risk of regression.

The Factory Method Pattern#

The Factory Method pattern resolves this by delegating object creation to a method that subclasses override. The base class defines the interface for creation; subclasses decide what to instantiate.

Step 1: Define an Interface#

public interface Document {
    void open();
    void save();
    String getContent();
}

Step 2: Create Concrete Implementations#

public class PdfDocument implements Document {
    public void open() {
        System.out.println("Opening a PDF document.");
    }

    public void save() {
        System.out.println("Saving PDF document...");
    }

    public String getContent() {
        return "PDF content";
    }
}

public class WordDocument implements Document {
    public void open() {
        System.out.println("Opening a Word document.");
    }

    public void save() {
        System.out.println("Saving Word document...");
    }

    public String getContent() {
        return "Word content";
    }
}

Step 3: Define an Abstract Creator#

public abstract class DocumentFactory {
    // The Factory Method
    public abstract Document createDocument();

    // Template method that uses the factory method
    public Document openAndReadDocument() {
        Document doc = createDocument();
        doc.open();
        return doc;
    }
}

Step 4: Implement Concrete Factories#

public class PdfDocumentFactory extends DocumentFactory {
    @Override
    public Document createDocument() {
        return new PdfDocument();
    }
}

public class WordDocumentFactory extends DocumentFactory {
    @Override
    public Document createDocument() {
        return new WordDocument();
    }
}

Step 5: Use the Factory Method#

public class Application {
    public static void main(String[] args) {
        DocumentFactory factory = getDocumentFactoryForFile("report.pdf");
        Document doc = factory.createDocument();
        doc.open();
        System.out.println(doc.getContent());
        doc.save();
    }

    private static DocumentFactory getDocumentFactoryForFile(String filename) {
        if (filename.endsWith(".pdf")) {
            return new PdfDocumentFactory();
        } else if (filename.endsWith(".docx")) {
            return new WordDocumentFactory();
        } else {
            throw new IllegalArgumentException("Unsupported file type");
        }
    }
}

Factory Method vs. Simple Factory#

The original if-else chain is a Simple Factory—useful but limited:

public class DocumentSimpleFactory {
    public static Document createDocument(String type) {
        if (type.equals("PDF")) {
            return new PdfDocument();
        } else if (type.equals("Word")) {
            return new WordDocument();
        } else {
            throw new IllegalArgumentException("Unknown document type");
        }
    }
}

The Simple Factory centralizes creation but still requires modification when new types are added. The Factory Method eliminates this by moving the creation decision into a polymorphic hierarchy. New types are added by creating new factory subclasses, not by editing existing code.

Why It Works#

  1. Encapsulation: Object creation is hidden behind a defined interface.
  2. Open/Closed Principle: New document types are added without modifying existing code.
  3. Dependency Inversion: High-level code depends on abstractions, not concrete implementations.
  4. Single Responsibility: The responsibility for creating objects is separated from the code that uses them.

When to Use It#

  • When new object types will be introduced frequently
  • When you need to decouple the client from concrete implementations
  • When you are building a framework that requires extension points
  • When you want to provide library users with a way to extend internal components

Do not apply the Factory Method pattern to objects with stable, simple construction. Every layer of abstraction has a cost. The pattern is justified when extensibility is a genuine requirement—not a speculative one.

Common Pitfalls#

  1. Overuse: Simple, stable objects do not need factories. Direct instantiation is clearer.
  2. Complex hierarchies: Too many factory subclasses can make the system harder to navigate than the original if-else chain.
  3. Indirection cost: Each added layer of abstraction increases cognitive overhead. Apply the pattern where the benefit justifies that cost.

Conclusion#

The Factory Method pattern delegates object creation to subclasses, removing the need for conditionals in the client and keeping the system open to extension. The result is code that handles new types without touching what already works.