Concepts / Creational patterns / Factory method and simple factory

Factory method and simple factory

One place that decides which concrete class to create.

01The intuition

The problem it solves

A school notice board sends notices by email, SMS or a printed slip, depending on what each parent chose. The first version decides which class to build at every place that sends a notice.

def send_fee_notice(parent, text):
    if parent.channel == "email":
        n = EmailNotifier(parent.email, retries=3)
    elif parent.channel == "sms":
        n = SmsNotifier(parent.phone, sender_id="SCHOOL")
    else:
        n = PrintNotifier(parent.address)
    n.send(text)

def send_holiday_notice(parent, text):
    if parent.channel == "email":          # the same ladder, copied
        n = EmailNotifier(parent.email, retries=3)
    # ...

The decision "which concrete class, with which constructor arguments" is copied into every function that sends anything. Adding a WhatsApp channel means finding every copy. Changing the email retry count means finding every copy. The functions that should only care about sending are full of building.

A factory is one place whose only job is to decide which concrete class to create and how. Callers ask the factory for "a notifier for this parent" and never say a class name.

Build it one piece at a time

Step 1: move the ladder into one function. This is the simple factory. Nothing clever, but there is now exactly one copy.

def notifier_for(parent) -> Notifier:
    if parent.channel == "email":
        return EmailNotifier(parent.email, retries=3)
    if parent.channel == "sms":
        return SmsNotifier(parent.phone, sender_id="SCHOOL")
    return PrintNotifier(parent.address)

def send_fee_notice(parent, text):
    notifier_for(parent).send(text)

Step 2: return the interface, not a class. The return type is Notifier, the promise every channel keeps. Callers compile against the promise, so a new channel cannot break them.

Step 3: replace the ladder with a registry. A dictionary from channel name to a small builder function means adding WhatsApp is a registration, not an edit of the factory.

from typing import Callable

class NotifierFactory:
    def __init__(self):
        self._builders: dict[str, Callable[[object], Notifier]] = {}
    def register(self, channel: str, builder: Callable[[object], Notifier]) -> None:
        self._builders[channel] = builder
    def create(self, parent) -> Notifier:
        try:
            return self._builders[parent.channel](parent)
        except KeyError:
            raise ValueError(f"no notifier for channel {parent.channel}")

factory = NotifierFactory()
factory.register("email", lambda p: EmailNotifier(p.email, retries=3))
factory.register("sms", lambda p: SmsNotifier(p.phone, sender_id="SCHOOL"))
factory.register("whatsapp", lambda p: WhatsAppNotifier(p.phone))

Step 4: the factory method, when subclasses decide. Sometimes the choice depends on which kind of object is doing the creating. Then the base class calls an abstract create and each subclass fills it in. Use this form only when there is already a hierarchy; otherwise the registry is simpler.

from abc import ABC, abstractmethod

class NoticeSender(ABC):
    def send(self, parent, text):
        self.make_notifier(parent).send(text)
    @abstractmethod
    def make_notifier(self, parent) -> Notifier: ...

class UrgentSender(NoticeSender):
    def make_notifier(self, parent) -> Notifier:
        return SmsNotifier(parent.phone, sender_id="URGENT")
StepPlaces that name EmailNotifierAdding WhatsApp editsCallers know concrete classes
Startone per sending functionevery functionyes
1 and 2onethe factoryno
3one registration linenothing existingno

Why it works

Construction is the one moment a program must name a concrete class. A factory quarantines that moment, so the rest of the program talks only to interfaces, which is what dependency inversion and the open-closed principle need. The invariant is "concrete class names appear in the factory and in tests, nowhere else". Constructor details such as retry counts and sender ids are set once, so they cannot drift between copies.

Where it breaks: a factory that takes a growing list of flags to decide between twenty classes is the old ladder with a new name; the registry form fixes that. And a factory for a class with one implementation and no construction logic is ceremony; just call the constructor.

The trade-off you must say out loud

A factory adds a hop between "I want a notifier" and the class that is made, and readers must find the registry to learn what exists. Say why you have one: "construction varies by channel and I do not want every sender to repeat that choice". The follow-up is usually "where are the registrations", and the answer is at startup next to the other wiring, or in each channel's module if teams own channels.

Where it shows up in the 100 Google problems

  • Design a parking lot: a VehicleFactory turns the licence-plate scan into a Car, Bike or Truck; the entrance gate never names those classes.
  • Design a notification service: channels are built by a registry keyed on the user's preference, which is how a new channel is one registration.
  • Design a logging framework: sinks and formatters are created from a configuration file by name, which is a registry factory.
  • Design a JSON parser: the first character of a token decides which node class to build; one method holds that decision.

02Used in these problems