Card 06/ 09
Factory Method Subclasses vs Passing a Class in Python
In Python a class is already a callable that makes objects, so a parameter can replace the subclass.
In Python, a class is an object you can pass as an argument, and calling it creates an instance. That means a creator can receive the class it should create, or any function that returns a product, instead of having a subclass override a method. This card sets the two forms side by side and says what the shorter one loses.
The listing is the notification sender again, with the subclasses removed. OrderNotifier takes make_sender, anything that can be called with no arguments to produce a sender.
from collections.abc import Callable
from typing import Protocol
class Sender(Protocol):
def send(self, text: str) -> str: ...
class EmailSender:
def send(self, text: str) -> str:
return f"email: {text}"
class SmsSender:
def __init__(self, number: str = "+44 7700 900123") -> None:
self.number = number
def send(self, text: str) -> str:
return f"SMS to {self.number}: {text}"
class OrderNotifier:
def __init__(self, make_sender: Callable[[], Sender]) -> None:
self.make_sender = make_sender
def notify_shipped(self, order_id: str) -> None:
sender = self.make_sender()
print(sender.send(f"Order {order_id} has shipped."))
OrderNotifier(EmailSender).notify_shipped("A-1042")
OrderNotifier(SmsSender).notify_shipped("A-1042")
OrderNotifier(lambda: SmsSender("+44 7700 900456")).notify_shipped("A-1043")email: Order A-1042 has shipped.
SMS to +44 7700 900123: Order A-1042 has shipped.
SMS to +44 7700 900456: Order A-1043 has shipped.EmailSender and SmsSender are passed as they are, because calling a class is how Python creates an instance. The third call passes a lambda, a function written inline, which creates an SmsSender with a different number. No EmailOrderNotifier or SmsOrderNotifier exists, and a new channel needs only its sender class.
Subclass and factory methodwhen creation is part of a family of overrides
The subclass is a named place to override, so it can also change other steps, and a type checker and a reader see a class hierarchy. Frameworks use this form when the application must extend a base class anyway.
Pass the class or a callablewhen creation is the only thing that varies
There is no subclass per product, and any callable fits, whether a class, a lambda or functools.partial. The choice is made where the object is built, outside the class hierarchy, and it can change nothing except the one creation.
The pattern's roles survive in the short form: OrderNotifier is still the creator, and make_sender is still the step that decides the class. What has gone is the parallel hierarchy of creators. Most Python code takes this form, and so do Java codebases that pass a Supplier<Sender>; the subclass form is worth it when a subclass has other reasons to exist.
← → move through sections and cards