This article explores the Factory Method design pattern and its implementation in Python. Design patterns became a popular topic in late 90s after the so-called Gang of Four (GoF: Gamma, Helm, Johson, and Vlissides) published their book Design Patterns: Elements of Reusable Object-Oriented Software.
The book describes design patterns as a core design solution to reoccurring problems in software and classifies each design pattern into categories according to the nature of the problem. Each pattern is given a name, a problem description, a design solution, and an explanation of the consequences of using it.
The GoF book describes Factory Method as a creational design pattern. Creational design patterns are related to the creation of objects, and Factory Method is a design pattern that creates objects with a common interface.
This is a recurrent problem that makes Factory Method one of the most widely used design patterns, and it’s very important to understand it and know how apply it.
By the end of this article, you will:
- Understand the components of Factory Method
- Recognize opportunities to use Factory Method in your applications
- Learn to modify existing code and improve its design by using the pattern
- Learn to identify opportunities where Factory Method is the appropriate design pattern
- Choose an appropriate implementation of Factory Method
- Know how to implement a reusable, general purpose solution of Factory Method
Free Bonus: 5 Thoughts On Python Mastery, a free course for Python developers that shows you the roadmap and the mindset you’ll need to take your Python skills to the next level.
Take the Quiz: Test your knowledge with our interactive “The Factory Method Pattern and Its Implementation in Python” quiz. You’ll receive a score upon completion to help you track your learning progress:
Interactive Quiz
The Factory Method Pattern and Its Implementation in PythonCheck your grasp of the Factory Method pattern in Python: when to use it, the roles involved, and how to implement a flexible object factory.
Introducing Factory Method
Factory Method is a creational design pattern used to create concrete implementations of a common interface.
It separates the process of creating an object from the code that depends on the interface of the object.
For example, an application requires an object with a specific interface to perform its tasks. The concrete implementation of the interface is identified by some parameter.
Instead of using a complex if/elif/else conditional structure to determine the concrete implementation, the application delegates that decision to a separate component that creates the concrete object. With this approach, the application code is simplified, making it more reusable and easier to maintain.
Imagine an application that needs to convert a Song object into its string representation using a specified format. Converting an object to a different representation is often called serializing. You’ll often see these requirements implemented in a single function or method that contains all the logic and implementation, like in the following code:
# In serializer_demo.py
import json
import xml.etree.ElementTree as et
class Song:
def __init__(self, song_id, title, artist):
self.song_id = song_id
self.title = title
self.artist = artist
class SongSerializer:
def serialize(self, song, format):
if format == 'JSON':
song_info = {
'id': song.song_id,
'title': song.title,
'artist': song.artist
}
return json.dumps(song_info)
elif format == 'XML':
song_info = et.Element('song', attrib={'id': song.song_id})
title = et.SubElement(song_info, 'title')
title.text = song.title
artist = et.SubElement(song_info, 'artist')
artist.text = song.artist
return et.tostring(song_info, encoding='unicode')
else:
raise ValueError(format)
In the example above, you have a basic Song class to represent a song and a SongSerializer class that can convert a song object into its string representation according to the value of the format parameter.
The .serialize() method supports two different formats: JSON and XML. Any other format specified is not supported, so a ValueError exception is raised.
Let’s use the Python interactive shell to see how the code works:
>>> import serializer_demo as sd
>>> song = sd.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = sd.SongSerializer()
>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'
>>> serializer.serialize(song, 'XML')
'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'
>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "./serializer_demo.py", line 30, in serialize
raise ValueError(format)
ValueError: YAML
You create a song object and a serializer, and you convert the song to its string representation by using the .serialize() method. The method takes the song object as a parameter, as well as a string value representing the format you want. The last call uses YAML as the format, which is not supported by the serializer, so a ValueError exception is raised.
This example is short and simplified, but it still has a lot of complexity. There are three logical or execution paths depending on the value of the format parameter. This may not seem like a big deal, and you’ve probably seen code with more complexity than this, but the above example is still pretty hard to maintain.
The Problems With Complex Conditional Code
The example above exhibits all the problems you’ll find in complex logical code. Complex logical code uses if/elif/else structures to change the behavior of an application. Using if/elif/else conditional structures makes the code harder to read, harder to understand, and harder to maintain.
The code above might not seem hard to read or understand, but wait till you see the final code in this section!
Nevertheless, the code above is hard to maintain because it is doing too much. The single responsibility principle states that a module, a class, or even a method should have a single, well-defined responsibility. It should do just one thing and have only one reason to change.
The .serialize() method in SongSerializer will require changes for many different reasons. This increases the risk of introducing new defects or breaking existing functionality when changes are made. Let’s take a look at all the situations that will require modifications to the implementation:
-
When a new format is introduced: The method will have to change to implement the serialization to that format.
-
When the
Songobject changes: Adding or removing properties to theSongclass will require the implementation to change in order to accommodate the new structure. -
When the string representation for a format changes (plain JSON vs JSON API): The
.serialize()method will have to change if the desired string representation for a format changes because the representation is hard-coded in the.serialize()method implementation.
The ideal situation would be if any of those changes in requirements could be implemented without changing the .serialize() method. Let’s see how you can do that in the following sections.
Looking for a Common Interface
The first step when you see complex conditional code in an application is to identify the common goal of each of the execution paths (or logical paths).
Code that uses if/elif/else usually has a common goal that is implemented in different ways in each logical path. The code above converts a song object to its string representation using a different format in each logical path.
Based on the goal, you look for a common interface that can be used to replace each of the paths. The example above requires an interface that takes a song object and returns a string.
Once you have a common interface, you provide separate implementations for each logical path. In the example above, you will provide an implementation to serialize to JSON and another for XML.
Then, you provide a separate component that decides the concrete implementation to use based on the specified format. This component evaluates the value of format and returns the concrete implementation identified by its value.
In the following sections, you will learn how to make changes to existing code without changing the behavior. This is referred to as refactoring the code.
Martin Fowler in his book Refactoring: Improving the Design of Existing Code defines refactoring as “the process of changing a software system in such a way that does not alter the external behavior of the code yet improves its internal structure.” If you’d like to see refactoring in action, check out the Real Python Code Conversation Refactoring: Prepare Your Code to Get Help.
Let’s begin refactoring the code to achieve the desired structure that uses the Factory Method design pattern.
Refactoring Code Into the Desired Interface
The desired interface is an object or a function that takes a Song object and returns a string representation.
The first step is to refactor one of the logical paths into this interface. You do this by adding a new method ._serialize_to_json() and moving the JSON serialization code to it. Then, you change the client to call it instead of having the implementation in the body of the if statement:
class SongSerializer:
def serialize(self, song, format):
if format == 'JSON':
return self._serialize_to_json(song)
# The rest of the code remains the same
def _serialize_to_json(self, song):
payload = {
'id': song.song_id,
'title': song.title,
'artist': song.artist
}
return json.dumps(payload)
Once you make this change, you can verify that the behavior has not changed. Then, you do the same for the XML option by introducing a new method ._serialize_to_xml(), moving the implementation to it, and modifying the elif path to call it.
The following example shows the refactored code:
class SongSerializer:
def serialize(self, song, format):
if format == 'JSON':
return self._serialize_to_json(song)
elif format == 'XML':
return self._serialize_to_xml(song)
else:
raise ValueError(format)
def _serialize_to_json(self, song):
payload = {
'id': song.song_id,
'title': song.title,
'artist': song.artist
}
return json.dumps(payload)
def _serialize_to_xml(self, song):
song_element = et.Element('song', attrib={'id': song.song_id})
title = et.SubElement(song_element, 'title')
title.text = song.title
artist = et