Factory Method Pattern
The Factory Method Pattern is a creational design pattern that provides a way to create objects without specifying their exact class. It’s particularly useful when you need flexibility in creating objects or when you want to decouple object creation from the code that uses the object. Let’s dive into this pattern using a real-world example.
The Problem: Rigid Object Creation
In traditional object-oriented programming, we often create objects directly using constructors:
document = WordDocument()
While this approach works for simple scenarios, it can lead to several issues as your application grows:
- Tight Coupling: The code that uses the object becomes tightly coupled to the specific class, making it difficult to switch to different implementations.
- Reduced Flexibility: Adding new types of objects requires modifying existing code, violating the Open/Closed Principle.
- Maintenance Challenges: As the number of object types increases, the code becomes harder to maintain and extend.
The Solution: Factory Method Pattern
The Factory Method Pattern addresses these issues by introducing a level of abstraction in object creation. Instead of directly instantiating objects, it suggests using a special factory method responsible for creating and returning the appropriate object based on given parameters or conditions.
Key Components
The Factory Method Pattern consists of four main components:
- Product: An interface or abstract class that defines the object the factory method creates.
- Concrete Product: The specific objects created by the Concrete Creators.
- Creator: An abstract class or interface that declares the factory method.
- Concrete Creator: A class that implements the Creator and overrides the factory method to return a specific type of object.
Product
The Product is an interface or abstract class that defines the common interface for all objects that can be created by the factory method. It represents the type of object that the factory will produce.
from abc import ABC, abstractmethod
class Document(ABC):
@abstractmethod
def open(self) -> str:
pass
@abstractmethod
def save(self) -> str:
pass
In this example, Document
is our Product. It defines two abstract methods, open()
and save()
, which all concrete documents must implement.
Concrete Product
Concrete Products are the specific implementations of the Product interface. These are the actual objects that will be created by the factory method.
class WordDocument(Document):
def open(self) -> str:
return "Opening Word document"
def save(self) -> str:
return "Saving Word document"
class PDFDocument(Document):
def open(self) -> str:
return "Opening PDF document"
def save(self) -> str:
return "Saving PDF document"
class ExcelDocument(Document):
def open(self) -> str:
return "Opening Excel document"
def save(self) -> str:
return "Saving Excel document"
Here, we have three Concrete Products: WordDocument
, PDFDocument
, and ExcelDocument
. Each implements the open()
and save()
methods defined in the Document interface.
Creator
The Creator is an abstract class or interface that declares the factory method. This method returns an object of the Product type.
from abc import ABC, abstractmethod
class DocumentCreator(ABC):
@abstractmethod
def create_document(self) -> Document:
pass
The DocumentCreator
class defines the create_document()
method, which will be implemented by Concrete Creators to produce specific types of documents.
Concrete Creator
Concrete Creators are classes that implement the Creator interface. They override the factory method to create and return a specific type of Product.
class WordDocumentCreator(DocumentCreator):
def create_document(self) -> Document:
return WordDocument()
class PDFDocumentCreator(DocumentCreator):
def create_document(self) -> Document:
return PDFDocument()
class ExcelDocumentCreator(DocumentCreator):
def create_document(self) -> Document:
return ExcelDocument()
Each Concrete Creator (WordDocumentCreator
, PDFDocumentCreator
, and ExcelDocumentCreator
) implements the create_document()
method to return its specific type of document.
Implementing the Factory Method Pattern
Now that we’ve defined all the components, let’s see how to use the Factory Method Pattern in practice.
def client_code(creator: DocumentCreator) -> None:
document = creator.create_document()
print(f"Created: {document.__class__.__name__}")
print(document.open())
print(document.save())
def main():
print("Creating a Word document:")
client_code(WordDocumentCreator())
print("\nCreating a PDF document:")
client_code(PDFDocumentCreator())
print("\nCreating an Excel document:")
client_code(ExcelDocumentCreator())
if __name__ == "__main__":
main()
In this implementation:
- We define a
client_code()
function that takes aDocumentCreator
as an argument. This function doesn’t know which specific type of document it’s working with; it only knows that it can callcreate_document()
to get aDocument
object.
When you run this code, you’ll see output like this:
Creating a Word document:
Created: WordDocument
Opening Word document
Saving Word document
Creating a PDF document:
Created: PDFDocument
Opening PDF document
Saving PDF document
Creating an Excel document:
Created: ExcelDocument
Opening Excel document
Saving Excel document
Advantages of the Factory Method Pattern
- Loose Coupling: The pattern separates the code that creates objects from the code that uses them, reducing dependencies and making the system more flexible.
- Open/Closed Principle : You can introduce new types of products (e.g., a
GoogleDocsDocument
) without breaking existing client code, adhering to the Open/Closed Principle. - Single Responsibility Principle: The object creation logic is centralized in the factory method, adhering to the Single Responsibility Principle.
- Flexibility in Object Creation: The pattern allows for runtime decisions on which objects to create based on conditions or parameters.
- Easier Testing: By using interfaces and dependency injection, the Factory Method Pattern makes it easier to mock objects for testing.
Considerations and Potential Drawbacks
While the Factory Method Pattern offers many benefits, it’s important to consider potential drawbacks:
- Increased Complexity: For simple scenarios with few product types, implementing the Factory Method Pattern might introduce unnecessary complexity.
- Class Proliferation: As the number of product types grows, you may end up with a large number of creator classes, potentially complicating the codebase.
- Overuse: Not every object creation scenario requires a factory method. Overusing the pattern can lead to over-engineering.
- Performance Overhead: In some cases, using factory methods might introduce a slight performance overhead compared to direct object instantiation.
Enjoy Reading This Article?
Here are some more articles you might like to read next: