Abstract Factory¶
Warning
The Abstract Factory is an awkward workaround for the lack of first-class functions and classes in less powerful programming languages. It is a poor fit for Python, where we can instead simply pass a class or a factory function when a library needs to create objects on our behalf.
– Brandon Rhodes: The Abstract Factory Pattern
The json module of the Python standard library is a good example
of a library that needs to instantiate objects on behalf of its caller. Let’s
look at a JSON string like this:
{
"Title": "Python basics",
"Language": "en",
"Authors": "Veit Schiele",
"License": "BSD-3-Clause",
"Publication date": "2021-10-28"
}
Normally, the json.load() function of the json
module generates Unicode objects for the strings and a Dict for the top-level
JSON object.
However, this default value is not satisfactory for the publication date, so we want to convert it into a date format:
>>> import json
>>> from datetime import datetime
>>> def convert_date(string):
... return datetime.strptime(string, "%Y-%m-%d").date()
...
>>> with open("books.json") as f:
... books = json.load(f)
... books["Publication date"] = convert_date(books["Publication date"])
... print(books)
...
{'Title': 'Python basics', 'Language': 'en', 'Authors': 'Veit Schiele', 'License': 'BSD-3-Clause', 'Publication date': datetime.date(2021, 10, 28)}
This simple factory was successfully executed: the date returned is of type
datetime.date.
Note
I chose the verb convert_date() as the name for this function, rather
than a noun like date_factory(), because it expresses what the function
does, rather than telling me what kind of function it is.
Some legacy languages only support passing class instances, not callable functions. With this restriction, any simple factory would have to go from a function to a method:
class DateFactory(object):
@staticmethod
def build_date(dict):
dict["Publication date"] = datetime.strptime(
dict["Publication date"], "%Y-%m-%d"
).date()
In traditional object-oriented programming, the word factory is the name for a
type of class that provides a method to create an object. If we could not pass a
Python class directly, but only object instances, the DateFactory class
could not be passed as an argument to the load() method. Instead,
DateFactory would have to be instantiated unnecessarily and then the
resulting object would have to be passed:
class Loader(object):
@staticmethod
def load(books_file, factory):
with open(books_file) as f:
books = json.load(f)
factory.build_date(books)
return books
>>> df = DateFactory()
>>> b = Loader.load("books.json", df)
>>> print(b)
{'Title': 'Python basics', 'Language': 'en', 'Authors': 'Veit Schiele', 'License': 'BSD-3-Clause', 'Publication date': datetime.date(2021, 10, 28)}
Note
Since Python classes provide static and class methods that can be called without an instance, we don’t need to instantiate the
DateFactoryclass first – we can simply pass it as an object.Languages that force you to declare the type of each method parameter in advance limit your future possibilities excessively.
Finally, the Abstract Factory design pattern aims to separate the
specification from the implementation by creating an abstract class. Your
abstract class would simply promise that the DateFactory argument for
load() will be a class that matches the required interface:
from abc import ABCMeta, abstractmethod
class AbstractFactory(metaclass=ABCMeta):
@abstractmethod
def build_date(self, dict):
pass
However, once the abstract class is present and DateFactory inherits
from it, the operations that take place at runtime are exactly the same as
before. The DateFactory methods are called with different arguments
that instruct them to create different types of objects without the caller
needing to know the details.