Disclaimer: This blog is not a guide on how to implement the Factory Pattern. It simply uses it as a basic example for a use case of generics.
Let’s start with the problem we are trying to solve - using the Factory Pattern to help us create objects.
We typically might start with something like this:
import abc
class BaseObject(abc.ABC):
...
class ObjectFactory(abc.ABC):
def create(self, *args, **kwargs) -> BaseObject:
...
class MyCustomObject1(BaseObject):
...
class MyCustomObject2(BaseObject):
...
factory = ObjectFactory()
instance: MyCustomObject1 = factory.create() # uh-oh
Immediately, we encounter our first problem: How can I decide which type of BaseObject I want to create?
Someone who has just learned the foundations of Object Oriented Programming would like think about creating a child factory for each desired type explicitly. For example:
# Make the factory abstract
class BaseObjectFactory(abc.ABC):
@abc.abstractmethod
def create(self, *args, **kwargs) -> BaseObject:
raise NotImplementedError()
# Build a factory for MyCustomObject1
class MyCustomObject1Factory(BaseObjectFactory):
@abc.abstractmethod
def create(self, *args, **kwargs) -> BaseObject:
return MyCustomObject1(*args, **kwargs)
# *Build a factory for MyCustomObject2*
...
# Instantiate our Factories
my_custom_object_1_factory = MyCustomObject1Factory()
instance: MyCustomObject1 = my_custom_object_1_factory.create()
But clearly, this is defeating the key purpose of the factory pattern… to help us build objects! This can easily over complicate things. Besides, a single factory should typically be abstract enough to handle multiple types.
This is where generics come in. Rather than explicitly defining a new factory for each object type, we can make the factory produce objects of a dynamic type T.
from typing import TypeVar, Generic
T = TypeVar("T")
# Make the factory utilise generics
class ObjectFactory(Generic[T]):
def create(self, *args, **kwargs) -> T:
raise T(*args, **kwargs)
# Instantiate our Factories
my_custom_object_1_factory = ObjectFactory[MyCustomObject1]()
instance: MyCustomObject1 = my_custom_object_1_factory.create()
And optionally, if you want maintain the factory create type hint to be bound to BaseObject, simply update the TypeVar definition:
T = TypeVar("T", bound=BaseObject)
Okay, so this has simplified things a bit. We no longer need to define a new factory for each object type!
But wait! If you run this code, you will encounter a TypeError: 'TypeVar' object is not callable , and using the debugger will clearly show you why:
Inspecting T reveals that T does not point to MyCustomObject1, but instead points to the TypeVar that we declared earlier.