py4u guide

Hands-on Tutorial: Factory Method Design Pattern in Python

In software development, creating objects is a fundamental task. However, as applications grow, hardcoding object instantiation logic directly into client code can lead to tight coupling, reduced flexibility, and difficulty maintaining or extending the codebase. This is where **design patterns**—proven solutions to common software design problems—come to the rescue. The **Factory Method Design Pattern** is a creational pattern that addresses these issues by abstracting the object creation process. It delegates the responsibility of instantiating objects to subclasses, allowing the client code to work with abstract interfaces rather than concrete implementations. This promotes loose coupling, scalability, and adherence to the **Open/Closed Principle** (open for extension, closed for modification). In this tutorial, we’ll dive deep into the Factory Method pattern: its components, real-world analogies, practical implementation in Python, and when to use it. By the end, you’ll be able to apply this pattern to write cleaner, more maintainable code.

Table of Contents

  1. What is the Factory Method Design Pattern?
  2. Key Components of Factory Method
  3. When to Use the Factory Method Pattern
  4. Real-World Analogy
  5. Hands-on Implementation in Python
  6. Advantages and Disadvantages
  7. Comparison with Other Patterns
  8. Conclusion
  9. References

What is the Factory Method Design Pattern?

The Factory Method Design Pattern is a creational design pattern that defines an interface (or abstract class) for creating objects but delegates the actual instantiation to its subclasses. In other words, it defers object creation to child classes, allowing them to decide which class to instantiate.

The pattern is formally defined in the Gang of Four (GoF) book as:

“Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.”

At its core, the Factory Method pattern decouples the client code from the specific classes it needs to instantiate. Instead of directly creating objects (e.g., WordDocument()), the client relies on a “factory” to handle object creation, making the code more flexible and easier to extend.

Key Components of Factory Method

To implement the Factory Method pattern, you’ll need four main components:

1. Product

  • An interface (or abstract class) defining the common methods that all concrete products must implement.
  • Represents the objects the factory method creates.

2. Concrete Product

  • A concrete class that implements the Product interface.
  • These are the actual objects created by the factory method.

3. Creator

  • An abstract class (or interface) that declares the factory method (a method responsible for creating Product objects).
  • The factory method is typically abstract, forcing subclasses to implement it. The creator may also provide a default implementation of the factory method.

4. Concrete Creator

  • A concrete class that extends the Creator and overrides the factory method to return an instance of a Concrete Product.

When to Use Factory Method

Use the Factory Method pattern when:

  • A class cannot anticipate the type of objects it needs to create at runtime.
  • A class wants its subclasses to specify the objects it creates.
  • You want to localize the logic for creating objects (adheres to the Single Responsibility Principle).
  • You need to enforce the Open/Closed Principle (easily add new product types without modifying existing client code).

Real-World Analogy

Think of a furniture manufacturing company. The company (Creator) has a factory that produces furniture (Product). However, the factory doesn’t decide whether to make chairs, tables, or sofas—it delegates that decision to specialized departments (Concrete Creators):

  • Creator: The main factory (abstract class with a create_furniture() method).
  • Concrete Creators: Chair Department, Table Department (subclasses that override create_furniture() to make chairs or tables).
  • Product: Furniture (interface with methods like assemble(), paint()).
  • Concrete Products: Chair, Table (implement Furniture and define specific assembly/painting logic).

This way, the main factory doesn’t need to know how to make each furniture type; the specialized departments handle that. Adding a new product (e.g., Sofa) only requires a new SofaDepartment (Concrete Creator) and Sofa (Concrete Product)—no changes to the main factory.

Hands-on Implementation in Python

Let’s implement the Factory Method pattern with a practical example. We’ll build a document editor that can create and manage different types of documents (e.g., Word, PDF).

Problem Statement

Suppose we want an editor that can open and save documents. Without the Factory Method pattern, the client code might directly instantiate WordDocument or PdfDocument based on user input, leading to tight coupling and conditional logic bloat.

Without Factory Method: The Problem

Here’s how the code might look without the Factory Method pattern:

class WordDocument:
    def open(self):
        print("Opening Word document...")
    
    def save(self):
        print("Saving Word document...")

class PdfDocument:
    def open(self):
        print("Opening PDF document...")
    
    def save(self):
        print("Saving PDF document...")

# Client code
def main():
    document_type = input("Enter document type (word/pdf): ").lower()
    
    # Conditional logic to create documents (problematic!)
    if document_type == "word":
        doc = WordDocument()
    elif document_type == "pdf":
        doc = PdfDocument()
    else:
        raise ValueError("Unsupported document type")
    
    doc.open()
    doc.save()

if __name__ == "__main__":
    main()

Issues with this approach:

  • The client code is tightly coupled to WordDocument and PdfDocument.
  • Adding a new document type (e.g., Excel) requires modifying the client’s conditional logic (violates Open/Closed Principle).
  • The client handles object creation, mixing creation logic with business logic (violates Single Responsibility Principle).

With Factory Method: The Solution

Let’s refactor this using the Factory Method pattern to address these issues.

Step 1: Define the Product Interface

First, create an abstract Document class (Product) with common methods (open(), save()).

from abc import ABC, abstractmethod

class Document(ABC):
    @abstractmethod
    def open(self):
        pass
    
    @abstractmethod
    def save(self):
        pass

Step 2: Implement Concrete Products

Next, create concrete document types that implement the Document interface.

class WordDocument(Document):
    def open(self):
        print("Opening Word document...")
    
    def save(self):
        print("Saving Word document...")

class PdfDocument(Document):
    def open(self):
        print("Opening PDF document...")
    
    def save(self):
        print("Saving PDF document...")

# Add a new product later (e.g., ExcelDocument) without changing existing code!
class ExcelDocument(Document):
    def open(self):
        print("Opening Excel document...")
    
    def save(self):
        print("Saving Excel document...")

Step 3: Define the Creator (Abstract Factory)

Create an abstract DocumentEditor class (Creator) with a factory method create_document(). This method will be overridden by subclasses to create specific documents.

class DocumentEditor(ABC):
    @abstractmethod
    def create_document(self) -> Document:
        """Factory method: Subclasses must implement this to create a Document."""
        pass
    
    def edit_document(self):
        """Client method that uses the factory method to create and work with a Document."""
        document = self.create_document()
        document.open()
        document.save()

Step 4: Implement Concrete Creators

Create subclasses of DocumentEditor (Concrete Creators) that override create_document() to return specific Document instances.

class WordEditor(DocumentEditor):
    def create_document(self) -> Document:
        return WordDocument()

class PdfEditor(DocumentEditor):
    def create_document(self) -> Document:
        return PdfDocument()

class ExcelEditor(DocumentEditor):
    def create_document(self) -> Document:
        return ExcelDocument()

Step 5: Client Code

Now, the client code uses the DocumentEditor subclasses instead of directly instantiating documents. Adding a new document type only requires a new Concrete Creator and Concrete Product.

def main():
    editor_type = input("Enter editor type (word/pdf/excel): ").lower()
    
    # Choose the appropriate editor (Concrete Creator)
    if editor_type == "word":
        editor = WordEditor()
    elif editor_type == "pdf":
        editor = PdfEditor()
    elif editor_type == "excel":
        editor = ExcelEditor()
    else:
        raise ValueError("Unsupported editor type")
    
    # Edit the document (client doesn't directly create the Document!)
    editor.edit_document()

if __name__ == "__main__":
    main()

How It Works

  • The client code selects an editor (Concrete Creator) based on input.
  • The editor calls create_document() (factory method) to instantiate the appropriate Document (Concrete Product).
  • The client never directly creates a Document—it delegates creation to the editor.

Advantages and Disadvantages

Advantages

  • Loose Coupling: Client code is decoupled from concrete product classes.
  • Open/Closed Principle: Add new products by creating new Concrete Product and Concrete Creator classes (no changes to existing code).
  • Single Responsibility Principle: Object creation logic is isolated in factory methods.
  • Flexibility: Subclasses control the type of objects created, making the code more adaptable.

Disadvantages

  • Increased Complexity: Requires creating multiple new classes (abstract products, concrete products, creators), which can overcomplicate simple scenarios.
  • Overkill for Simple Cases: If you only have one product type, the pattern adds unnecessary layers.

Comparison with Other Patterns

Factory Method vs. Simple Factory

  • Simple Factory: A concrete class with a static method that creates objects based on input (e.g., DocumentFactory.create_document("word")). It does not use inheritance—all logic is in one class.
  • Factory Method: Uses inheritance; subclasses override the factory method to create objects. More flexible for extension.

Factory Method vs. Abstract Factory

  • Factory Method: Creates one type of product (e.g., documents).
  • Abstract Factory: Creates families of related products (e.g., documents + document templates + export tools).

Conclusion

The Factory Method Design Pattern is a powerful tool for managing object creation in a flexible, maintainable way. By delegating instantiation to subclasses, it promotes loose coupling, adheres to design principles like Open/Closed and Single Responsibility, and makes it easy to extend your code with new product types.

Use it when you need to decouple object creation from client code or when subclasses should control the type of objects created. Avoid it for simple scenarios where the overhead of extra classes isn’t justified.

References