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

  1. Since Python classes provide static and class methods that can be called without an instance, we don’t need to instantiate the DateFactory class first – we can simply pass it as an object.

  2. 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.