Understanding the Factory Method Pattern in Python Programming
Written on
The Factory Method pattern is a fundamental design pattern that helps address common issues in software design, leading to more manageable code in the long run. Familiarity with various design patterns allows developers to handle specific challenges more effectively.
A common hurdle when learning about design patterns is the overwhelming amount of information provided without a clear understanding of the practical problems they solve. In this article, I will break down the Factory Method pattern in a straightforward manner, elucidating the problem it addresses and demonstrating its implementation.
Example Without Using the Factory Method
Let's illustrate a scenario involving different document types: Resume and Report. Each document type will have its own constructor defined as follows:
from abc import ABC, abstractmethod
# Define the Product Interface class Document(ABC):
@abstractmethod
def create(self):
pass
# Create Concrete Products class Resume(Document):
def create(self):
return "Resume created"
class Report(Document):
def create(self):
return "Report created"
Next, we implement a function that selects the document type:
def create_document(document_type: str) -> Document:
if document_type == "resume":
return Resume()elif document_type == "report":
return Report()else:
raise ValueError(f"Unknown document type: {document_type}")
Later on, we'll see how the Factory Method pattern can enhance this function's implementation. The following code demonstrates how to utilize the document types:
def client_code(document_type: str):
document = create_document(document_type)
print(document.create())
if __name__ == "__main__":
print("Creating a Resume:")
client_code("resume")
print("nCreating a Report:")
client_code("report")
Using the Factory Method
In our refactored approach, we will maintain the constructors for each object:
from abc import ABC, abstractmethod
# Define the Product Interface class Document(ABC):
@abstractmethod
def create(self):
pass
# Create Concrete Products class Resume(Document):
def create(self):
return "Resume created"
class Report(Document):
def create(self):
return "Report created"
Now, we will decentralize the creation of each document type:
# Define the Creator Class with the Factory Method class DocumentCreator(ABC):
@abstractmethod
def factory_method(self):
passdef create_document(self):
# Call the factory method to get the product
document = self.factory_method()
return document.create()
# Implement Concrete Creators class ResumeCreator(DocumentCreator):
def factory_method(self):
return Resume()
class ReportCreator(DocumentCreator):
def factory_method(self):
return Report()
Adding a new document type becomes as simple as creating a new concrete creator class. The code for using these document types remains similar:
def client_code(creator: DocumentCreator):
print(creator.create_document())
if __name__ == "__main__":
print("Creating a Resume:")
resume_creator = ResumeCreator()
client_code(resume_creator)
print("nCreating a Report:")
report_creator = ReportCreator()
client_code(report_creator)
Explaining the Differences
The primary distinction is that without utilizing the Factory Method pattern, we are creating objects within the same scope where we define the logic of our program:
def create_document(document_type: str) -> Document:
if document_type == "resume":
return Resume()elif document_type == "report":
return Report()else:
raise ValueError(f"Unknown document type: {document_type}")
While this example is straightforward, modifying the logic when adding or removing document types could prove cumbersome in more complex scenarios. Additionally, the logic can become convoluted if multiple actions are required after each condition.
Comparison
- Without Factory Method: The logic for document creation is centralized in the create_document function. Although this is simpler, it reduces flexibility and complicates maintenance. Introducing a new document type necessitates changes to the create_document function, which can lead to bugs and requires a deep understanding of the entire function.
- With Factory Method: The creation logic is decentralized. Each concrete creator is responsible for its document type. Adding a new document type only requires implementing a new concrete creator class, adhering to the Open/Closed Principle and resulting in more modular and extensible code.
How to Recognize It?
A clear sign that the Factory Method pattern may be beneficial is the presence of if/else statements that determine which objects to create:
# Example def main_function():
if option == 1:
Object1 = Object_constructor()
action1()
action2()
elif option == 2:
Object2 = Object_constructor2()
action3()
action4()
else:
Object3 = Object_constructor3()
action5()
action6()
In this scenario, the logic is heavily dependent on the type of object being used. If new object types are introduced, the program's logic must be altered.
Whenever this pattern emerges, consider enhancing it with the Factory Method.
Conclusions
I aimed to present this design pattern in a unique way compared to other resources available online, simplifying the concept as much as possible.
It's beneficial to explore various sources when learning about design patterns, as different examples can help solidify your understanding.
I hope you found this article informative!
Thank you for your attention!