Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Design Patterns (GoF and modern)  Module 2 – Creational patterns: Factory Method to Singleton

Factory Method: defer which class to create

Factory Method lets subclasses or a registry choose which class to create. Code in Python, Java, TypeScript, Go, C++ and Rust, and the constructor trap.

  • Beginner
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, Java 25, Go 1.27.1, Clang 21.0.0 and Rust 1.99.0
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Implement Factory Method with an overridable creator and with a registry of constructor functions
  • Distinguish Factory Method from a simple factory function and from static factory methods
  • Avoid calling overridable factory methods from constructors in Java and C++
  • Choose the idiomatic creation style in each of the six languages

Before you start

What you’ll learn · Before you start

What you will learn

  • Implement Factory Method with an overridable creator and with a registry of constructor functions
  • Distinguish Factory Method from a simple factory function and from static factory methods
  • Avoid calling overridable factory methods from constructors in Java and C++
  • Choose the idiomatic creation style in each of the six languages

Before you start

On this page

Factory Method is a creational design pattern for code that has to create an object without knowing its exact class. A base class, the creator, declares a method that returns an object of some interface, the product. Each subclass overrides that one method to choose the concrete class, and the rest of the creator’s code works with whatever the method returns. The 1994 catalogue by Gamma, Helm, Johnson and Vlissides, often called the Gang of Four book, puts it as letting subclasses “decide which class to instantiate”. Today the pattern shows up in two forms: the overridable method of the original, and a registry that maps a name to a function that builds the object.

You will build both forms in Python and then meet the same design in Java, TypeScript, Go, C++ and Rust, each program shown next to the output it printed. The lesson also separates Factory Method from two things that share its name, simple factory functions and static factory methods, and reproduces the trap waiting for anyone who calls a factory method from a constructor.

The problem: creating an object and using it in one place

Picture an order service that tells customers their parcel has shipped. The first version sends email, so the code that builds the message also builds an email client. Then the business wants SMS, then push notifications, then a webhook for a partner. If every one of those arrives as another elif channel == ... branch inside the method that writes and sends the message, that method changes for every new channel, its tests have to be run against every channel, and two teams who each add a channel in the same week edit the same lines.

The method is doing two jobs. One is deciding which class to create. The other is using the object: building the text, choosing the recipient, recording that the notice went out. The second job does not change when a channel is added; only the first does. Factory Method moves the first job behind a single method, so the code that uses the object is written once and never names a concrete class.

The four roles

Factory Method: concrete creators extend the creator and create concrete products, which implement the product interface.NotificationService (creator)notify_shipped() calls create_sender()and uses only SenderConcrete creatorsConcrete productsSender (product)send(to, text)EmailNotificationscreate_sender()SmsNotificationscreate_sender()EmailSenderSmsSenderextendcreateimplement

The four roles of Factory Method

Text description of the diagram

The diagram is one column of four rows, read from the top.

  1. NotificationService is the creator. Its operation, notify_shipped(), calls create_sender(), the factory method, and uses only the Sender interface.
  2. EmailNotifications and SmsNotifications are the concrete creators. An arrow labelled extend points from them up to NotificationService: each one overrides create_sender().
  3. EmailSender and SmsSender are the concrete products. An arrow labelled create points from the concrete creators down to them: EmailNotifications creates an EmailSender and SmsNotifications creates an SmsSender.
  4. Sender is the product, the interface with send(to, text). An arrow labelled implement points from the concrete products down to it.

Nothing connects NotificationService to EmailSender or SmsSender directly, so the creator never names a concrete product.

  • The product is the interface every created object has: here Sender, with send(to, text).
  • The concrete products are the classes behind it: EmailSender, SmsSender.
  • The creator declares the factory method, create_sender(), and contains the code that uses its result. Its operation, notify_shipped(), calls the factory method and then works only through Sender.
  • The concrete creators override the factory method. SmsNotifications.create_sender() returns an SmsSender configured with the sender ID it was given.

The arrow that matters is the one that is missing: the creator has no arrow to EmailSender or SmsSender. A new channel adds a product class and a creator class, and the creator’s tested code stays as it was.

Factory Method in Python

A creator whose subclasses choose the Sender Python · creator/notify.py
"""Factory Method: notify_shipped() is written once, against Sender; each subclass decides which
Sender to create."""

from abc import ABC, abstractmethod


class Sender(ABC):
    """The product: all that notify_shipped() needs from a channel."""

    @abstractmethod
    def send(self, to: str, text: str) -> str: ...


class EmailSender(Sender):
    def send(self, to: str, text: str) -> str:
        return f"email to {to}: {text}"


class SmsSender(Sender):
    def __init__(self, sender_id: str) -> None:
        self.sender_id = sender_id

    def send(self, to: str, text: str) -> str:
        return f"sms from {self.sender_id} to {to}: {text}"


class NotificationService(ABC):
    """The creator. create_sender() is the factory method; notify_shipped() never names a
    concrete Sender."""

    @abstractmethod
    def create_sender(self) -> Sender: ...

    def notify_shipped(self, to: str, order: int) -> None:
        sender = self.create_sender()
        print(sender.send(to, f"Order {order} has shipped"))


class EmailNotifications(NotificationService):
    def create_sender(self) -> Sender:
        return EmailSender()


class SmsNotifications(NotificationService):
    def __init__(self, sender_id: str) -> None:
        self.sender_id = sender_id

    def create_sender(self) -> Sender:
        return SmsSender(self.sender_id)


EmailNotifications().notify_shipped("asha@example.com", 1042)
SmsNotifications("SHOPIN").notify_shipped("+12025550100", 1042)

Output

email to asha@example.com: Order 1042 has shipped
sms from SHOPIN to +12025550100: Order 1042 has shipped

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 notify.py

NotificationService derives from ABC and marks create_sender() with @abstractmethod, so Python refuses to make an instance of the creator itself, or of a subclass that forgot to override the method: either attempt raises a TypeError that names the missing method. notify_shipped() is ordinary code that calls self.create_sender() and uses the result. Which class that is depends on the type of self, and nothing else.

Notice what each concrete creator holds. EmailNotifications needs nothing, while SmsNotifications keeps the sender ID that its product needs. A concrete creator is often the natural place for the configuration of its product, which is one reason the pattern is more than a lookup: the decision and the settings travel together.

The registry form: a name per product

Python classes are objects you can store and call, and so are functions. That makes a second form of the pattern cheap: keep a dictionary from a name to a factory, anything that returns a new product when it is called, and let any module add to it.

A registry of senders that another file extends

registry/main.py

"""A new channel joins the registry from this file; senders.py stays as it was."""

import json

import senders


class WebhookSender:
    def send(self, to: str, text: str) -> str:
        return f"POST {to} {json.dumps({'text': text}, separators=(',', ':'))}"


print("known:", senders.known())
senders.register("webhook", WebhookSender)
print("known:", senders.known())

for channel, to in [("sms", "+12025550100"), ("webhook", "https://hooks.example.com/orders")]:
    print(senders.create(channel).send(to, "Order 1042 has shipped"))

# Two mistakes the registry refuses: an unknown name, and a second sender under a name that is
# taken.
try:
    senders.create("fax")
except ValueError as error:
    print("error:", error)

try:
    senders.register("sms", WebhookSender)
except ValueError as error:
    print("error:", error)

Output

known: email, sms
known: email, sms, webhook
sms from SHOPIN to +12025550100: Order 1042 has shipped
POST https://hooks.example.com/orders {"text":"Order 1042 has shipped"}
error: unknown channel "fax" (known: email, sms, webhook)
error: channel "sms" is already registered

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 main.py

registry/senders.py

"""Senders and the registry that creates them by name. Adding a channel never edits create()."""

from collections.abc import Callable
from typing import Protocol


class Sender(Protocol):
    def send(self, to: str, text: str) -> str: ...


# Channel name -> a function that makes a configured Sender. A class is callable, so a class can
# be its own factory.
_FACTORIES: dict[str, Callable[[], Sender]] = {}


def register(name: str, factory: Callable[[], Sender]) -> None:
    """Adds a channel. A name can be taken only once, so a typo cannot silently replace a
    channel."""
    if name in _FACTORIES:
        raise ValueError(f'channel "{name}" is already registered')
    _FACTORIES[name] = factory


def known() -> str:
    return ", ".join(sorted(_FACTORIES))


def create(name: str) -> Sender:
    try:
        factory = _FACTORIES[name]
    except KeyError:
        raise ValueError(f'unknown channel "{name}" (known: {known()})') from None
    return factory()


class EmailSender:
    def send(self, to: str, text: str) -> str:
        return f"email to {to}: {text}"


class SmsSender:
    def __init__(self, sender_id: str) -> None:
        self.sender_id = sender_id

    def send(self, to: str, text: str) -> str:
        return f"sms from {self.sender_id} to {to}: {text}"


register("email", EmailSender)
register("sms", lambda: SmsSender("SHOPIN"))

senders.py knows email and SMS. main.py adds a webhook channel with one register() call, and create("webhook") returns a WebhookSender although senders.py has never heard of it. The SMS entry is a lambda because the registry stores factories, not classes: a factory can create an object that is already configured, here with the sender ID SHOPIN.

Two refusals are part of the design. An unknown name raises a ValueError that lists the names the registry knows, which turns a typo in a configuration file into a message that says what was meant. A name that is already taken is refused too, because a dictionary would otherwise let a second register("sms", ...) replace the first without a word. The registry is the open/closed principle applied to object creation; the LLD lesson on the open/closed principle grows the same idea into plug-in modules.

Factory Method, simple factories and static factory methods

Two other things are often called factories too, and an interviewer may ask you to tell them apart.

A simple factory is one function or method that decides which class to create, with a condition or a lookup, for example create_sender(kind) containing the original if chain. It is not in the catalogue, it is often the right amount of design, and it is the thing the registry form replaces when other code must add products.

A static factory method is a named way to build one type, written as a static method. Java’s documentation for List calls List.of and List.copyOf “static factory methods”; they return unmodifiable lists. Python spells the same idea as a class method: dict.fromkeys(keys) and datetime.fromtimestamp(seconds) are alternative constructors. Go uses plain functions named NewX, and Effective Go’s section on constructors builds a File with NewFile. Rust’s API guidelines say that constructors are static, inherent methods, so Vec::new() and File::open() are the norm. These methods can hide which class they return and can hand out cached objects, but nobody overrides them, so they do not let a subclass decide anything.

Four ways to create an object without a bare constructor call
Criterion Factory MethodRegistry of factoriesSimple factoryStatic factory method
Who chooses the class The subclass of the creatorWhoever registered the nameOne function, with a condition or a tableThe method, for one type
Adding a product A product class and a creator subclassA product class and one register() callAn edit to the functionNot its job
Can other code add products Yes, by subclassingYes, by registeringNoNo
Example in this lesson create_sender() in notify.pysenders.create(name)The original if chainList.of, dict.fromkeys, NewSmsSender
When to choose A framework or base class whose own code needs objects that users of the framework choosePlug-ins, configuration files and tests that add or swap products by nameA small, closed set of products owned by one teamReadable, well-named construction of a single type

One design in six languages

The same two forms, the creator with its factory method and the registry, written the way each language prefers. Every program prints exactly what the two Python examples print together.

The creator and the registry in Java, TypeScript, Go, C++ and Rust

Java · compiled/Notifications.java

// Factory Method in Java 25: an abstract creator whose subclasses override createSender(), then
// a registry of Supplier<Sender> behind a static factory method, Senders.create(name).
// Run: java Notifications.java
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Supplier;

public class Notifications {
    interface Sender {
        String send(String to, String text);
    }

    record EmailSender() implements Sender {
        public String send(String to, String text) {
            return "email to " + to + ": " + text;
        }
    }

    record SmsSender(String senderId) implements Sender {
        public String send(String to, String text) {
            return "sms from " + senderId + " to " + to + ": " + text;
        }
    }

    // The creator: notifyShipped() is final and written once; createSender() is the factory
    // method.
    abstract static class NotificationService {
        protected abstract Sender createSender();

        final void notifyShipped(String to, int order) {
            System.out.println(createSender().send(to, "Order " + order + " has shipped"));
        }
    }

    static final class EmailNotifications extends NotificationService {
        @Override
        protected Sender createSender() {
            return new EmailSender();
        }
    }

    static final class SmsNotifications extends NotificationService {
        private final String senderId;

        SmsNotifications(String senderId) {
            this.senderId = senderId;
        }

        @Override
        protected Sender createSender() {
            return new SmsSender(senderId);
        }
    }

    // The registry: a name maps to a Supplier, so a new channel is one register() call. TreeMap
    // keeps names sorted.
    static final class Senders {
        private static final Map<String, Supplier<Sender>> FACTORIES = new TreeMap<>();

        static {
            register("email", EmailSender::new);
            register("sms", () -> new SmsSender("SHOPIN"));
        }

        private Senders() {}

        static void register(String name, Supplier<Sender> factory) {
            if (FACTORIES.putIfAbsent(name, factory) != null) {
                throw new IllegalArgumentException(
                        "channel \"" + name + "\" is already registered");
            }
        }

        static String known() {
            return String.join(", ", FACTORIES.keySet());
        }

        static Sender create(String name) {
            Supplier<Sender> factory = FACTORIES.get(name);
            if (factory == null) {
                throw new IllegalArgumentException(
                        "unknown channel \"" + name + "\" (known: " + known() + ")");
            }
            return factory.get();
        }
    }

    record WebhookSender() implements Sender {
        public String send(String to, String text) {
            return "POST " + to + " {\"text\":\"" + text + "\"}";
        }
    }

    public static void main(String[] args) {
        new EmailNotifications().notifyShipped("asha@example.com", 1042);
        new SmsNotifications("SHOPIN").notifyShipped("+12025550100", 1042);

        System.out.println("known: " + Senders.known());
        Senders.register("webhook", WebhookSender::new);
        System.out.println("known: " + Senders.known());
        System.out.println(Senders.create("sms").send("+12025550100", "Order 1042 has shipped"));
        String hook = "https://hooks.example.com/orders";
        System.out.println(Senders.create("webhook").send(hook, "Order 1042 has shipped"));
        try {
            Senders.create("fax");
        } catch (IllegalArgumentException e) {
            System.out.println("error: " + e.getMessage());
        }
        try {
            Senders.register("sms", WebhookSender::new);
        } catch (IllegalArgumentException e) {
            System.out.println("error: " + e.getMessage());
        }
    }
}

TypeScript · compiled/notifications.ts

// Factory Method in TypeScript: an abstract creator, then a Map of factory functions. Only
// erasable syntax (no parameter properties or enums), so Node.js runs the file as it is: node
// notifications.ts
interface Sender {
  send(to: string, text: string): string;
}

class EmailSender implements Sender {
  send(to: string, text: string): string {
    return `email to ${to}: ${text}`;
  }
}

class SmsSender implements Sender {
  readonly senderId: string;
  constructor(senderId: string) {
    this.senderId = senderId;
  }
  send(to: string, text: string): string {
    return `sms from ${this.senderId} to ${to}: ${text}`;
  }
}

// The creator: notifyShipped() is written once; createSender() is the factory method.
abstract class NotificationService {
  protected abstract createSender(): Sender;

  notifyShipped(to: string, order: number): void {
    console.log(this.createSender().send(to, `Order ${order} has shipped`));
  }
}

class EmailNotifications extends NotificationService {
  protected createSender(): Sender {
    return new EmailSender();
  }
}

class SmsNotifications extends NotificationService {
  readonly senderId: string;
  constructor(senderId: string) {
    super();
    this.senderId = senderId;
  }
  protected createSender(): Sender {
    return new SmsSender(this.senderId);
  }
}

// The registry: channel name -> a function that makes a configured Sender.
const factories = new Map<string, () => Sender>();

function register(name: string, factory: () => Sender): void {
  if (factories.has(name)) throw new Error(`channel "${name}" is already registered`);
  factories.set(name, factory);
}

const known = (): string => [...factories.keys()].sort().join(", ");

function create(name: string): Sender {
  const factory = factories.get(name);
  if (!factory) throw new Error(`unknown channel "${name}" (known: ${known()})`);
  return factory();
}

register("email", () => new EmailSender());
register("sms", () => new SmsSender("SHOPIN"));

class WebhookSender implements Sender {
  send(to: string, text: string): string {
    return `POST ${to} ${JSON.stringify({ text })}`;
  }
}

new EmailNotifications().notifyShipped("asha@example.com", 1042);
new SmsNotifications("SHOPIN").notifyShipped("+12025550100", 1042);

console.log(`known: ${known()}`);
register("webhook", () => new WebhookSender());
console.log(`known: ${known()}`);
console.log(create("sms").send("+12025550100", "Order 1042 has shipped"));
console.log(
  create("webhook").send("https://hooks.example.com/orders", "Order 1042 has shipped"),
);
for (const mistake of [() => create("fax"), () => register("sms", () => new WebhookSender())]) {
  try {
    mistake();
  } catch (error) {
    console.log(`error: ${(error as Error).message}`);
  }
}

Go · compiled/notifications.go

// Factory Method in Go: there are no subclasses, so the creator holds its factory method as a
// function field, and constructors are NewX functions. The registry maps a name to such a
// function.
// Run: go run notifications.go
package main

import (
	"encoding/json"
	"fmt"
	"slices"
	"strings"
)

type Sender interface {
	Send(to, text string) string
}

type EmailSender struct{}

func (EmailSender) Send(to, text string) string { return "email to " + to + ": " + text }

type SmsSender struct{ senderID string }

func NewSmsSender(senderID string) *SmsSender { return &SmsSender{senderID: senderID} }

func (s *SmsSender) Send(to, text string) string {
	return "sms from " + s.senderID + " to " + to + ": " + text
}

// NotificationService is the creator: NotifyShipped is written once and calls whatever
// newSender makes.
type NotificationService struct {
	newSender func() Sender
}

func (n NotificationService) NotifyShipped(to string, order int) {
	fmt.Println(n.newSender().Send(to, fmt.Sprintf("Order %d has shipped", order)))
}

func EmailNotifications() NotificationService {
	return NotificationService{newSender: func() Sender { return EmailSender{} }}
}

func SmsNotifications(senderID string) NotificationService {
	return NotificationService{newSender: func() Sender { return NewSmsSender(senderID) }}
}

// The registry: channel name -> a function that makes a configured Sender.
var factories = map[string]func() Sender{}

func Register(name string, factory func() Sender) error {
	if _, taken := factories[name]; taken {
		return fmt.Errorf("channel %q is already registered", name)
	}
	factories[name] = factory
	return nil
}

func Known() string {
	names := make([]string, 0, len(factories))
	for name := range factories {
		names = append(names, name)
	}
	slices.Sort(names) // map iteration order is unspecified
	return strings.Join(names, ", ")
}

func Create(name string) (Sender, error) {
	factory, ok := factories[name]
	if !ok {
		return nil, fmt.Errorf("unknown channel %q (known: %s)", name, Known())
	}
	return factory(), nil
}

type WebhookSender struct{}

func (WebhookSender) Send(to, text string) string {
	body, _ := json.Marshal(map[string]string{"text": text})
	return "POST " + to + " " + string(body)
}

func must(err error) {
	if err != nil {
		panic(err)
	}
}

func main() {
	must(Register("email", func() Sender { return EmailSender{} }))
	must(Register("sms", func() Sender { return NewSmsSender("SHOPIN") }))

	EmailNotifications().NotifyShipped("asha@example.com", 1042)
	SmsNotifications("SHOPIN").NotifyShipped("+12025550100", 1042)

	fmt.Println("known:", Known())
	must(Register("webhook", func() Sender { return WebhookSender{} }))
	fmt.Println("known:", Known())
	deliveries := [][2]string{{"sms", "+12025550100"}, {"webhook", "https://hooks.example.com/orders"}}
	for _, d := range deliveries {
		sender, err := Create(d[0])
		must(err)
		fmt.Println(sender.Send(d[1], "Order 1042 has shipped"))
	}
	if _, err := Create("fax"); err != nil {
		fmt.Println("error:", err)
	}
	if err := Register("sms", func() Sender { return WebhookSender{} }); err != nil {
		fmt.Println("error:", err)
	}
}

C++ · compiled/notifications.cpp

// Factory Method in C++23: the factory method returns std::unique_ptr<Sender>, so the caller
// owns the product (C++ Core Guidelines R.20, F.26). The registry maps a name to a
// std::function that makes one.
// Build: clang++ -std=c++23 -Wall -Wextra notifications.cpp -o notifications
#include <format>
#include <functional>
#include <map>
#include <memory>
#include <print>
#include <stdexcept>
#include <string>
#include <utility>

struct Sender {
    virtual ~Sender() = default;
    virtual std::string send(const std::string& to, const std::string& text) const = 0;
};

struct EmailSender final : Sender {
    std::string send(const std::string& to, const std::string& text) const override {
        return "email to " + to + ": " + text;
    }
};

class SmsSender final : public Sender {
public:
    explicit SmsSender(std::string sender_id) : sender_id_{std::move(sender_id)} {}
    std::string send(const std::string& to, const std::string& text) const override {
        return "sms from " + sender_id_ + " to " + to + ": " + text;
    }

private:
    std::string sender_id_;
};

// The creator: notify_shipped() is written once; create_sender() is the factory method.
class NotificationService {
public:
    virtual ~NotificationService() = default;
    void notify_shipped(const std::string& to, int order) const {
        std::println("{}",
                     create_sender()->send(to, std::format("Order {} has shipped", order)));
    }

protected:
    virtual std::unique_ptr<Sender> create_sender() const = 0;
};

class EmailNotifications final : public NotificationService {
protected:
    std::unique_ptr<Sender> create_sender() const override {
        return std::make_unique<EmailSender>();
    }
};

class SmsNotifications final : public NotificationService {
public:
    explicit SmsNotifications(std::string sender_id) : sender_id_{std::move(sender_id)} {}

protected:
    std::unique_ptr<Sender> create_sender() const override {
        return std::make_unique<SmsSender>(sender_id_);
    }

private:
    std::string sender_id_;
};

// The registry: std::map keeps the names sorted.
using Factory = std::function<std::unique_ptr<Sender>()>;

std::map<std::string, Factory>& factories() {
    static std::map<std::string, Factory> registry;  // created on first use
    return registry;
}

void register_channel(const std::string& name, Factory factory) {
    if (!factories().try_emplace(name, std::move(factory)).second)
        throw std::invalid_argument(std::format("channel \"{}\" is already registered", name));
}

std::string known() {
    std::string names;
    for (const auto& [name, factory] : factories()) names += (names.empty() ? "" : ", ") + name;
    return names;
}

std::unique_ptr<Sender> create(const std::string& name) {
    auto found = factories().find(name);
    if (found == factories().end())
        throw std::invalid_argument(
            std::format("unknown channel \"{}\" (known: {})", name, known()));
    return found->second();
}

struct WebhookSender final : Sender {
    std::string send(const std::string& to, const std::string& text) const override {
        return "POST " + to + " {\"text\":\"" + text + "\"}";
    }
};

int main() {
    register_channel("email", [] { return std::make_unique<EmailSender>(); });
    register_channel("sms", [] { return std::make_unique<SmsSender>("SHOPIN"); });

    EmailNotifications{}.notify_shipped("asha@example.com", 1042);
    SmsNotifications{"SHOPIN"}.notify_shipped("+12025550100", 1042);

    std::println("known: {}", known());
    register_channel("webhook", [] { return std::make_unique<WebhookSender>(); });
    std::println("known: {}", known());
    std::println("{}", create("sms")->send("+12025550100", "Order 1042 has shipped"));
    std::println("{}", create("webhook")->send("https://hooks.example.com/orders",
                                               "Order 1042 has shipped"));
    try {
        create("fax");
    } catch (const std::invalid_argument& e) {
        std::println("error: {}", e.what());
    }
    try {
        register_channel("sms", [] { return std::make_unique<WebhookSender>(); });
    } catch (const std::invalid_argument& e) {
        std::println("error: {}", e.what());
    }
}

Rust · compiled/notifications.rs

// Factory Method in Rust: a trait with a required create_sender() and a provided
// notify_shipped() that calls it. Constructors are associated functions (new). The registry is
// a value you own, not a global: a BTreeMap from a name to a factory function.
// Build: rustc --edition 2024 notifications.rs
use std::collections::BTreeMap;

trait Sender {
    fn send(&self, to: &str, text: &str) -> String;
}

struct EmailSender;

impl Sender for EmailSender {
    fn send(&self, to: &str, text: &str) -> String {
        format!("email to {to}: {text}")
    }
}

struct SmsSender {
    sender_id: String,
}

impl SmsSender {
    fn new(sender_id: &str) -> Self {
        SmsSender {
            sender_id: sender_id.to_string(),
        }
    }
}

impl Sender for SmsSender {
    fn send(&self, to: &str, text: &str) -> String {
        format!("sms from {} to {to}: {text}", self.sender_id)
    }
}

// The creator: notify_shipped() is written once, in the trait; each implementation supplies
// create_sender().
trait NotificationService {
    fn create_sender(&self) -> Box<dyn Sender>;

    fn notify_shipped(&self, to: &str, order: u32) {
        println!(
            "{}",
            self.create_sender()
                .send(to, &format!("Order {order} has shipped"))
        );
    }
}

struct EmailNotifications;

impl NotificationService for EmailNotifications {
    fn create_sender(&self) -> Box<dyn Sender> {
        Box::new(EmailSender)
    }
}

struct SmsNotifications {
    sender_id: String,
}

impl NotificationService for SmsNotifications {
    fn create_sender(&self) -> Box<dyn Sender> {
        Box::new(SmsSender::new(&self.sender_id))
    }
}

// The registry: BTreeMap keeps the names sorted.
type Factory = fn() -> Box<dyn Sender>;

#[derive(Default)]
struct Registry {
    factories: BTreeMap<String, Factory>,
}

impl Registry {
    fn register(&mut self, name: &str, factory: Factory) -> Result<(), String> {
        if self.factories.contains_key(name) {
            return Err(format!("channel \"{name}\" is already registered"));
        }
        self.factories.insert(name.to_string(), factory);
        Ok(())
    }

    fn known(&self) -> String {
        self.factories
            .keys()
            .cloned()
            .collect::<Vec<_>>()
            .join(", ")
    }

    fn create(&self, name: &str) -> Result<Box<dyn Sender>, String> {
        match self.factories.get(name) {
            Some(factory) => Ok(factory()),
            None => Err(format!(
                "unknown channel \"{name}\" (known: {})",
                self.known()
            )),
        }
    }
}

struct WebhookSender;

impl Sender for WebhookSender {
    fn send(&self, to: &str, text: &str) -> String {
        format!("POST {to} {{\"text\":\"{text}\"}}")
    }
}

fn main() -> Result<(), String> {
    let mut registry = Registry::default();
    registry.register("email", || Box::new(EmailSender))?;
    registry.register("sms", || Box::new(SmsSender::new("SHOPIN")))?;

    EmailNotifications.notify_shipped("asha@example.com", 1042);
    SmsNotifications {
        sender_id: "SHOPIN".to_string(),
    }
    .notify_shipped("+12025550100", 1042);

    println!("known: {}", registry.known());
    registry.register("webhook", || Box::new(WebhookSender))?;
    println!("known: {}", registry.known());
    println!(
        "{}",
        registry
            .create("sms")?
            .send("+12025550100", "Order 1042 has shipped")
    );
    println!(
        "{}",
        registry
            .create("webhook")?
            .send("https://hooks.example.com/orders", "Order 1042 has shipped")
    );
    if let Err(error) = registry.create("fax") {
        println!("error: {error}");
    }
    if let Err(error) = registry.register("sms", || Box::new(WebhookSender)) {
        println!("error: {error}");
    }
    Ok(())
}
All five built and run with the pinned toolchains

compiled/languages_session.py

from session import run, versions

# The creator and the registry in five more languages: each program must print what the two
# Python examples print.
versions("javac --version", "node --version", "go version", "clang++ --version", "rustc --version")
run("""
java Notifications.java
node notifications.ts
go run notifications.go
clang++ -std=c++23 -O1 -g -Wall -Wextra -fsanitize=address,undefined notifications.cpp -o fm
./fm
rustc --edition 2024 -o notifications_rs notifications.rs && ./notifications_rs
""")

Output

# javac 25.0.4.1
# v24.21.0
# go version go1.27.1 darwin/arm64
# Apple clang version 21.0.0 (clang-2100.3.34.2)
# rustc 1.99.0 (b940084d7)
$ java Notifications.java
email to asha@example.com: Order 1042 has shipped
sms from SHOPIN to +12025550100: Order 1042 has shipped
known: email, sms
known: email, sms, webhook
sms from SHOPIN to +12025550100: Order 1042 has shipped
POST https://hooks.example.com/orders {"text":"Order 1042 has shipped"}
error: unknown channel "fax" (known: email, sms, webhook)
error: channel "sms" is already registered
$ node notifications.ts
email to asha@example.com: Order 1042 has shipped
sms from SHOPIN to +12025550100: Order 1042 has shipped
known: email, sms
known: email, sms, webhook
sms from SHOPIN to +12025550100: Order 1042 has shipped
POST https://hooks.example.com/orders {"text":"Order 1042 has shipped"}
error: unknown channel "fax" (known: email, sms, webhook)
error: channel "sms" is already registered
$ go run notifications.go
email to asha@example.com: Order 1042 has shipped
sms from SHOPIN to +12025550100: Order 1042 has shipped
known: email, sms
known: email, sms, webhook
sms from SHOPIN to +12025550100: Order 1042 has shipped
POST https://hooks.example.com/orders {"text":"Order 1042 has shipped"}
error: unknown channel "fax" (known: email, sms, webhook)
error: channel "sms" is already registered
$ clang++ -std=c++23 -O1 -g -Wall -Wextra -fsanitize=address,undefined notifications.cpp -o fm
$ ./fm
email to asha@example.com: Order 1042 has shipped
sms from SHOPIN to +12025550100: Order 1042 has shipped
known: email, sms
known: email, sms, webhook
sms from SHOPIN to +12025550100: Order 1042 has shipped
POST https://hooks.example.com/orders {"text":"Order 1042 has shipped"}
error: unknown channel "fax" (known: email, sms, webhook)
error: channel "sms" is already registered
$ rustc --edition 2024 -o notifications_rs notifications.rs && ./notifications_rs
email to asha@example.com: Order 1042 has shipped
sms from SHOPIN to +12025550100: Order 1042 has shipped
known: email, sms
known: email, sms, webhook
sms from SHOPIN to +12025550100: Order 1042 has shipped
POST https://hooks.example.com/orders {"text":"Order 1042 has shipped"}
error: unknown channel "fax" (known: email, sms, webhook)
error: channel "sms" is already registered

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 languages_session.py

compiled/session.py

"""Prints a terminal session: "$ " and each command, then everything it printed (standard output
and standard error together), and its exit status when that is not 0.

The toolchains are the pinned ones: Java 25 (Eclipse Temurin 25.0.4), Node.js 24.21.0 and Go
1.27.1 from the mise installations next to the Python that runs this file, Rust 1.99.0 from
its rustup toolchain, and the system's Apple clang. learn:verify cannot let a sandboxed run
read the home folder those installations live in, so the sessions of this folder are recorded
with LEARN_VERIFY_NETWORK=allow. They read only this folder and the toolchains, write only
inside this folder and the run's own temporary home, and never use the network.

A command that fails and is not listed in expect_failure makes the session exit with status
1, so a run that could not build a program never becomes a record."""

import os
import pwd
import re
import signal
import subprocess
import sys
from pathlib import Path

# .../installs/python/3.14.8/bin/python3 -> .../installs
INSTALLS = Path(sys.executable).resolve().parents[3]
REAL_HOME = Path(pwd.getpwuid(os.getuid()).pw_dir)
TOOLCHAINS = [
    INSTALLS / "java" / "temurin-25.0.4+101.0.LTS" / "bin",
    INSTALLS / "node" / "24.21.0" / "bin",
    INSTALLS / "go" / "1.27.1" / "bin",
    REAL_HOME / ".rustup" / "toolchains" / "1.99.0-aarch64-apple-darwin" / "bin",
]
# Version lines that end with a build date (rustc's) are shown without it: pages show no dates.
BUILD_DATE = re.compile(r"\s+\d{4}-\d{2}-\d{2}(?=\))")


def _env() -> dict[str, str]:
    env = dict(os.environ)
    env["PATH"] = os.pathsep.join([*map(str, TOOLCHAINS), env.get("PATH", "")])
    env["GOTOOLCHAIN"] = "local"  # never download another Go
    env["GOTELEMETRY"] = "off"
    env["GOFLAGS"] = "-trimpath"
    return env


def _shell(command: str) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        command, shell=True, env=_env(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
    )


def versions(*commands: str) -> None:
    """Prints the first line of each version command as a comment."""
    for command in commands:
        done = _shell(command)
        lines = done.stdout.splitlines()
        if done.returncode != 0 or not lines:
            sys.exit(f"{command} did not run: the pinned toolchain is missing here")
        print("#", BUILD_DATE.sub("", lines[0]))


def run(script: str, expect_failure: tuple[str, ...] = ()) -> None:
    """Runs each line of the script as one shell command; only the commands in expect_failure
    may fail."""
    failed = 0
    for command in (line.strip() for line in script.strip().splitlines()):
        print(f"$ {command}")
        done = _shell(command)
        print(done.stdout, end="")
        if done.returncode < 0:
            print(f"[stopped by signal {signal.Signals(-done.returncode).name}]")
        elif done.returncode != 0:
            print(f"[exit status {done.returncode}]")
        if done.returncode != 0 and command not in expect_failure:
            failed += 1
    if failed:
        sys.exit(1)

What each version shows about its language:

  • Java. The creator’s operation is final, so a subclass can change which sender is made but not how a notice is sent; the products are records. The registry stores Supplier<Sender> values in a TreeMap, which keeps the names sorted, and putIfAbsent refuses a taken name in one call. The operation is called notifyShipped because Object already has a final notify() method.
  • TypeScript. An abstract class and a Map of functions. Node.js 24 runs a .ts file by replacing its type annotations with spaces, without checking them, and only syntax that can simply be erased is accepted: parameter properties and enum declarations need a flag. The file declares its fields in the class body for that reason, and type checking stays the job of tsc in a project.
  • Go. There are no subclasses, so the creator holds its factory method as a function field, newSender, and EmailNotifications() and SmsNotifications(id) are constructor functions that fill it in. The registry is a map of functions. Go does not specify the order in which a range loop visits a map, so Known() sorts the names before printing them.
  • C++. The factory method returns std::unique_ptr<Sender>, so the caller owns the new object and nothing leaks; the Core Guidelines name unique_ptr as the default way to say who owns an object. The registry lives in a function-local static, which is created the first time factories() is called, so no other global can use it before it exists.
  • Rust. A trait with a required create_sender() and a provided notify_shipped() is Factory Method without inheritance: every implementation gets the operation and supplies the factory. The registry is an ordinary value that main owns, not a global, so tests can each build their own, and BTreeMap keeps the names sorted.

The constructor trap

A tempting shortcut is to call the factory method from the creator’s constructor and keep the product in a field. It goes wrong in every one of these languages, each in its own way. Python first:

A factory method called from __init__ Python · half_built/half_built.py
"""Python dispatches to the override even inside __init__, but the subclass may not have set its
attributes yet."""


class SmsSender:
    def __init__(self, sender_id: str) -> None:
        self.sender_id = sender_id

    def send(self, to: str, text: str) -> str:
        return f"sms from {self.sender_id} to {to}: {text}"


class NotificationService:
    def __init__(self) -> None:
        self.sender = self.create_sender()  # runs the subclass's create_sender()

    def create_sender(self):
        raise NotImplementedError

    def notify_shipped(self, to: str, order: int) -> None:
        print(self.sender.send(to, f"Order {order} has shipped"))


class SmsNotifications(NotificationService):
    def __init__(self, sender_id: str) -> None:
        super().__init__()  # create_sender() runs here, before sender_id exists
        self.sender_id = sender_id

    def create_sender(self) -> SmsSender:
        return SmsSender(self.sender_id)


class FixedSmsNotifications(SmsNotifications):
    def __init__(self, sender_id: str) -> None:
        self.sender_id = sender_id  # set what create_sender() reads first
        NotificationService.__init__(self)


try:
    SmsNotifications("SHOPIN").notify_shipped("+12025550100", 1042)
except AttributeError as error:
    print("AttributeError:", error)

FixedSmsNotifications("SHOPIN").notify_shipped("+12025550100", 1042)

Output

AttributeError: 'SmsNotifications' object has no attribute 'sender_id'
sms from SHOPIN to +12025550100: Order 1042 has shipped

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 half_built.py

Python dispatches the call to the subclass’s create_sender() even while the base __init__ is running, but SmsNotifications sets sender_id only after super().__init__() returns, so the attribute does not exist yet. The fixed class assigns what the factory method reads before it runs the base initialiser.

Java and C++ go wrong in other ways:

The same shortcut in Java 25 and C++

Java · compiled/Trap.java

// A constructor that calls an overridable factory method sees a half-built subclass.
// Run: java Trap.java
import java.util.ArrayList;
import java.util.List;

public class Trap {
    interface Sender {
        String send(String to, String text);
    }

    record SmsSender(String senderId) implements Sender {
        public String send(String to, String text) {
            return "sms from " + senderId + " to " + to + ": " + text;
        }
    }

    abstract static class NotificationService {
        private final Sender sender;

        NotificationService() {
            // runs the subclass's override before the subclass's constructor body
            sender = createSender();
        }

        protected abstract Sender createSender();

        final void notifyShipped(String to, int order) {
            System.out.println(sender.send(to, "Order " + order + " has shipped"));
        }
    }

    // 1. super() runs first, so senderId is still null when createSender() reads it.
    static final class SmsNotifications extends NotificationService {
        private final String senderId;

        SmsNotifications(String senderId) {
            super();
            this.senderId = senderId;
        }

        @Override
        protected Sender createSender() {
            return new SmsSender(senderId);
        }
    }

    // 2. Java 25 (JEP 513): a constructor may assign its own fields before super(...).
    static final class EarlySmsNotifications extends NotificationService {
        private final String senderId;

        EarlySmsNotifications(String senderId) {
            this.senderId = senderId;
            super();
        }

        @Override
        protected Sender createSender() {
            return new SmsSender(senderId);
        }
    }

    // 3. A field with an initializer cannot be assigned early: its initializer runs after
    //    super() returns.
    static final class AuditedSmsNotifications extends NotificationService {
        private final List<String> audit = new ArrayList<>();

        @Override
        protected Sender createSender() {
            audit.add("sms sender created");
            return new SmsSender("SHOPIN");
        }
    }

    public static void main(String[] args) {
        System.out.print("1. super() first:             ");
        new SmsNotifications("SHOPIN").notifyShipped("+12025550100", 1042);
        System.out.print("2. field set before super():  ");
        new EarlySmsNotifications("SHOPIN").notifyShipped("+12025550100", 1042);
        System.out.print("3. field with an initializer: ");
        try {
            new AuditedSmsNotifications().notifyShipped("+12025550100", 1042);
        } catch (NullPointerException e) {
            System.out.println("NullPointerException: " + e.getMessage());
        }
    }
}

C++ · compiled/trap.cpp

// A virtual call inside a constructor runs the version of the class being constructed, never
// the override (C++ Core Guidelines C.82). A factory function that finishes the object first
// gets the override (C.50).
// Build: clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined trap.cpp -o trap
#include <memory>
#include <print>
#include <string>
#include <utility>

struct Sender {
    virtual ~Sender() = default;
    virtual std::string send(const std::string& to, const std::string& text) const = 0;
};

struct EmailSender final : Sender {
    std::string send(const std::string& to, const std::string& text) const override {
        return "email to " + to + ": " + text;
    }
};

struct SmsSender final : Sender {
    std::string send(const std::string& to, const std::string& text) const override {
        return "sms from SHOPIN to " + to + ": " + text;
    }
};

// 1. The base constructor calls create_sender(). While it runs, the object is a
//    NotificationService, so the base's own create_sender() is called, whatever the derived
//    class overrides.
class NotificationService {
public:
    NotificationService() : sender_{create_sender()} {}
    virtual ~NotificationService() = default;
    void notify_shipped(const std::string& to) const {
        std::println("{}", sender_->send(to, "Order 1042 has shipped"));
    }

protected:
    virtual std::unique_ptr<Sender> create_sender() const {
        return std::make_unique<EmailSender>();
    }

private:
    std::unique_ptr<Sender> sender_;
};

class SmsNotifications final : public NotificationService {
protected:
    std::unique_ptr<Sender> create_sender() const override {
        return std::make_unique<SmsSender>();
    }
};

// 2. A factory function builds the whole object first, then asks it for its sender: virtual
//    dispatch now reaches the override.
class Service {
public:
    template <class T>
    static std::unique_ptr<T> create() {
        auto service = std::make_unique<T>();
        Service& base =
            *service;  // the name is looked up in Service; the call still reaches T's override
        base.sender_ = base.create_sender();
        return service;
    }
    virtual ~Service() = default;
    void notify_shipped(const std::string& to) const {
        std::println("{}", sender_->send(to, "Order 1042 has shipped"));
    }

protected:
    virtual std::unique_ptr<Sender> create_sender() const {
        return std::make_unique<EmailSender>();
    }

private:
    std::unique_ptr<Sender> sender_;
};

class SmsService final : public Service {
protected:
    std::unique_ptr<Sender> create_sender() const override {
        return std::make_unique<SmsSender>();
    }
};

int main() {
    std::print("1. built by the constructor: ");
    SmsNotifications{}.notify_shipped("+12025550100");
    std::print("2. built by create():        ");
    Service::create<SmsService>()->notify_shipped("+12025550100");
}
Both programs built and run

compiled/trap_session.py

from session import run

# A factory method called from a constructor: Java runs the override on a half-built object, C++
# runs the base's own version, and a C++ factory function that finishes the object first gets
# the override.
run("""
java Trap.java
clang++ -std=c++23 -O1 -g -Wall -Wextra -fsanitize=address,undefined trap.cpp -o trap && ./trap
""")

Output

$ java Trap.java
1. super() first:             sms from null to +12025550100: Order 1042 has shipped
2. field set before super():  sms from SHOPIN to +12025550100: Order 1042 has shipped
3. field with an initializer: NullPointerException: Cannot invoke "java.util.List.add(Object)" because "this.audit" is null
$ clang++ -std=c++23 -O1 -g -Wall -Wextra -fsanitize=address,undefined trap.cpp -o trap && ./trap
1. built by the constructor: email to +12025550100: Order 1042 has shipped
2. built by create():        sms from SHOPIN to +12025550100: Order 1042 has shipped

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trap_session.py

compiled/session.py

"""Prints a terminal session: "$ " and each command, then everything it printed (standard output
and standard error together), and its exit status when that is not 0.

The toolchains are the pinned ones: Java 25 (Eclipse Temurin 25.0.4), Node.js 24.21.0 and Go
1.27.1 from the mise installations next to the Python that runs this file, Rust 1.99.0 from
its rustup toolchain, and the system's Apple clang. learn:verify cannot let a sandboxed run
read the home folder those installations live in, so the sessions of this folder are recorded
with LEARN_VERIFY_NETWORK=allow. They read only this folder and the toolchains, write only
inside this folder and the run's own temporary home, and never use the network.

A command that fails and is not listed in expect_failure makes the session exit with status
1, so a run that could not build a program never becomes a record."""

import os
import pwd
import re
import signal
import subprocess
import sys
from pathlib import Path

# .../installs/python/3.14.8/bin/python3 -> .../installs
INSTALLS = Path(sys.executable).resolve().parents[3]
REAL_HOME = Path(pwd.getpwuid(os.getuid()).pw_dir)
TOOLCHAINS = [
    INSTALLS / "java" / "temurin-25.0.4+101.0.LTS" / "bin",
    INSTALLS / "node" / "24.21.0" / "bin",
    INSTALLS / "go" / "1.27.1" / "bin",
    REAL_HOME / ".rustup" / "toolchains" / "1.99.0-aarch64-apple-darwin" / "bin",
]
# Version lines that end with a build date (rustc's) are shown without it: pages show no dates.
BUILD_DATE = re.compile(r"\s+\d{4}-\d{2}-\d{2}(?=\))")


def _env() -> dict[str, str]:
    env = dict(os.environ)
    env["PATH"] = os.pathsep.join([*map(str, TOOLCHAINS), env.get("PATH", "")])
    env["GOTOOLCHAIN"] = "local"  # never download another Go
    env["GOTELEMETRY"] = "off"
    env["GOFLAGS"] = "-trimpath"
    return env


def _shell(command: str) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        command, shell=True, env=_env(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
    )


def versions(*commands: str) -> None:
    """Prints the first line of each version command as a comment."""
    for command in commands:
        done = _shell(command)
        lines = done.stdout.splitlines()
        if done.returncode != 0 or not lines:
            sys.exit(f"{command} did not run: the pinned toolchain is missing here")
        print("#", BUILD_DATE.sub("", lines[0]))


def run(script: str, expect_failure: tuple[str, ...] = ()) -> None:
    """Runs each line of the script as one shell command; only the commands in expect_failure
    may fail."""
    failed = 0
    for command in (line.strip() for line in script.strip().splitlines()):
        print(f"$ {command}")
        done = _shell(command)
        print(done.stdout, end="")
        if done.returncode < 0:
            print(f"[stopped by signal {signal.Signals(-done.returncode).name}]")
        elif done.returncode != 0:
            print(f"[exit status {done.returncode}]")
        if done.returncode != 0 and command not in expect_failure:
            failed += 1
    if failed:
        sys.exit(1)

In Java the call reaches the override, but super() runs before the subclass’s constructor body, so the override reads senderId while it still holds its default value, null, and the customer gets a text from “null”. JEP 513, final in Java 25, lets a constructor assign its own fields before calling super(...), and case 2 shows that this repairs the example. It has a limit that case 3 shows: a field declared with an initializer, such as audit = new ArrayList<>(), cannot be assigned early and is still unset when the superclass constructor calls the override, so audit.add(...) throws a NullPointerException whose message names this.audit.

C++ never reaches the override at all. While the base constructor runs, the object is still a base object, so the call goes to the base class’s own create_sender(), and an SmsNotifications sends email. The Core Guidelines forbid the call (C.82: don’t call virtual functions in constructors and destructors) and warn that calling a pure virtual function this way is undefined behaviour. Their alternative (C.50) is a factory function: Service::create() builds the whole object first and then asks it for its sender, and the second line of output shows the override running.

Calling a factory method from the creator's constructor
Criterion PythonJavaC++
Which method runs The subclass's overrideThe subclass's overrideThe base class's own version
What goes wrong Attributes the override reads may not exist yetFields the override reads still hold their defaultsThe override is never called
Symptom in the run AttributeErrorA text from "null", or a NullPointerExceptionAn SMS service that sends email
Fix shown Assign before calling the base __init__Assign before super(), for fields without initializersA factory function that finishes the object first

The fix that works in every language is not to call an overridable method during construction. Create the product when the creator needs it, as notify.py does in notify_shipped(), or make the creator take the product as a constructor parameter and let a factory function decide what to pass.

When not to use it

  • There is one product and no second one in sight. Call the constructor. A creator hierarchy for a single class is indirection that every reader pays for. A test double counts as a second product, but a constructor parameter often serves a test better than a subclass.
  • The choice is made once, from configuration. A dictionary lookup or a simple factory at start-up is enough. The subclass form makes you write a creator class for every product class, two parallel hierarchies that grow together.
  • A function would do. All six languages can pass a factory function around: a Supplier in Java, a std::function in C++, a plain function or class elsewhere. Handing one to the creator as a constructor parameter gives the same flexibility without a subclass per product. That is dependency injection, and it is often the better default.
  • A registry is global state. A channel exists only if the module that registers it has been imported, two tests that register the same name collide, and finding every product means searching for register( calls. Keep the registry small, refuse duplicate names, give tests their own registry where you can (as the Rust version does), and make the error for an unknown name list the known ones.

Exercise: a parser registry

Exercise · Easy · Python

Replace a parser switch with a registry

parsers.py has five parser classes and a create_parser(extension) that chooses one with an if/elif chain. Every new format means another branch in that function. Turn it into a registry, so that a new format is one call from anywhere, and create_parser never changes again:

- register_parser(extension, factory) adds a format. factory is anything that returns a new parser when called with no arguments: a parser class, or a function. Registering an extension that is already taken raises ValueError, so a second .csv parser can never quietly replace the first. - create_parser(extension) looks the extension up and returns a new parser. The case of the extension does not matter: .CSV gives a CsvParser. An extension nobody registered raises ValueError with a message that names it in double quotes and lists the extensions it knows, for example no parser for ".xml" (known: .csv, .env, .ini, .json, .tsv). - The five built-in formats are registered in the module itself, with register_parser.

Leave the five parser classes exactly as they are; one sample test compares them with the originals. Another test checks that create_parser itself no longer mentions any extension. Run the sample tests when you are done.

Starter code · parsers.py

"""Parsers for five text formats, and create_parser(), which picks one by file extension."""

import json


class CsvParser:
    """Comma-separated values: a list of rows, each a list of strings."""

    def parse(self, text):
        return [line.split(",") for line in text.strip().splitlines()]


class TsvParser:
    """Tab-separated values: a list of rows, each a list of strings."""

    def parse(self, text):
        return [line.split("\t") for line in text.strip().splitlines()]


class JsonParser:
    """JSON: whatever the document holds."""

    def parse(self, text):
        return json.loads(text)


class IniParser:
    """INI: {section: {key: value}}; blank lines and lines that start with ; are skipped."""

    def parse(self, text):
        sections, current = {}, None
        for raw in text.splitlines():
            line = raw.strip()
            if not line or line.startswith(";"):
                continue
            if line.startswith("[") and line.endswith("]"):
                current = sections.setdefault(line[1:-1].strip(), {})
            else:
                key, _, value = line.partition("=")
                current[key.strip()] = value.strip()
        return sections


class EnvParser:
    """.env files: {NAME: value} from NAME=value lines; blank lines and lines that start with # are skipped."""

    def parse(self, text):
        values = {}
        for raw in text.splitlines():
            line = raw.strip()
            if line and not line.startswith("#"):
                name, _, value = line.partition("=")
                values[name.strip()] = value.strip()
        return values


# Your registry goes below this line. Leave the five parser classes above as they are.


def create_parser(extension):
    """Returns a new parser for a file extension such as ".csv"."""
    if extension == ".csv":
        return CsvParser()
    elif extension == ".tsv":
        return TsvParser()
    elif extension == ".json":
        return JsonParser()
    elif extension == ".ini":
        return IniParser()
    elif extension == ".env":
        return EnvParser()
    raise ValueError("unsupported file")


def register_parser(extension, factory):
    """Adds a parser for another extension: factory() must return a new parser."""
    raise NotImplementedError("register_parser() is not written yet")
The sample tests · test_parsers.py
import ast
import hashlib
from pathlib import Path

import parsers
from parsers import CsvParser, EnvParser, IniParser, JsonParser, TsvParser, create_parser, register_parser

PARSER_CLASSES = ["CsvParser", "TsvParser", "JsonParser", "IniParser", "EnvParser"]
PARSER_CLASSES_SHA256 = "fb1cabd6a86c38de526f36cb4200b2a8cf75db6cec2da68caccfcf2a2830855f"


def module_tree():
    return ast.parse(Path(parsers.__file__).read_text(encoding="utf-8"))


def test_built_in_extensions():
    """the five built-in extensions still give their parsers"""
    assert create_parser(".csv").parse("sku,qty\nA1,2\n") == [["sku", "qty"], ["A1", "2"]]
    assert create_parser(".json").parse('{"order": 1042}') == {"order": 1042}
    assert create_parser(".ini").parse("; shop\n[db]\nhost = localhost\n") == {"db": {"host": "localhost"}}
    assert isinstance(create_parser(".tsv"), TsvParser)
    assert isinstance(create_parser(".env"), EnvParser)


def test_a_new_parser_each_time():
    """each call returns a new parser object"""
    assert create_parser(".csv") is not create_parser(".csv")


def test_extensions_ignore_case():
    """.CSV and .Json work like .csv and .json"""
    assert isinstance(create_parser(".CSV"), CsvParser)
    assert isinstance(create_parser(".Json"), JsonParser)


def test_register_from_outside():
    """a parser registered from this test file is created by its extension"""

    class LinesParser:
        def parse(self, text):
            return text.splitlines()

    register_parser(".lines", LinesParser)
    parser = create_parser(".lines")
    assert isinstance(parser, LinesParser)
    assert parser.parse("first\nsecond") == ["first", "second"]


def test_unknown_extension():
    """an unknown extension raises ValueError that names it and the extensions it knows"""
    try:
        create_parser(".xml")
    except ValueError as error:
        message = str(error)
        assert '".xml"' in message, f"name the extension in quotes: {message}"
        for known in (".csv", ".env", ".ini", ".json", ".tsv"):
            assert known in message, f"list the known extensions: {message}"
    else:
        raise AssertionError('create_parser(".xml") should raise ValueError')


def test_a_taken_extension_is_refused():
    """registering .csv again raises ValueError and keeps the CSV parser"""

    class OtherCsv:
        def parse(self, text):
            return []

    try:
        register_parser(".csv", OtherCsv)
    except ValueError:
        pass
    else:
        raise AssertionError('register_parser(".csv", ...) should raise ValueError: .csv is taken')
    assert isinstance(create_parser(".csv"), CsvParser)


def test_create_parser_names_no_extension():
    """create_parser() looks the extension up: it no longer compares it with ".csv", ".tsv" and the rest"""
    create = next(n for n in module_tree().body if isinstance(n, ast.FunctionDef) and n.name == "create_parser")
    named = sorted({n.value for n in ast.walk(create) if isinstance(n, ast.Constant) and isinstance(n.value, str) and n.value.startswith(".")})
    assert not named, f"create_parser() still names {named}: register the parsers and look the extension up instead"


def test_parser_classes_unchanged():
    """the five parser classes are exactly as they were"""
    classes = {n.name: n for n in module_tree().body if isinstance(n, ast.ClassDef)}
    missing = [name for name in PARSER_CLASSES if name not in classes]
    assert not missing, f"keep the parser classes: {missing} are gone"
    digest = hashlib.sha256("".join(ast.dump(classes[name]) for name in PARSER_CLASSES).encode()).hexdigest()
    assert digest == PARSER_CLASSES_SHA256, "a parser class has changed: change only the factory part below them"
A hint

Keep a module-level dictionary from a lower-case extension to its factory. register_parser checks that the key is not taken before it adds it; create_parser reads registry[extension.lower()], turns the KeyError of an unknown key into the ValueError the tests expect, and calls the factory. The registrations of the five built-in classes go after the two functions, at the bottom of the file.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Python Online Compiler Paste the starter file and the tests into the compiler and run them outside the lesson.

Check yourself

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 In the Python example, which method is the factory method?

    Choose one answer.

    Show the answer to question 1

    Answer: create_sender(), the method each subclass overrides to choose the Sender

    The factory method is the overridable method that returns a product. notify_shipped() is the creator's operation: it is written once and calls create_sender(), whose override in EmailNotifications or SmsNotifications decides which class is made.

  2. Question 2 of 6 What does half_built.py print?

    What does this program print? Choose one answer.

    """Python dispatches to the override even inside __init__, but the subclass may not have set its
    attributes yet."""
    
    
    class SmsSender:
        def __init__(self, sender_id: str) -> None:
            self.sender_id = sender_id
    
        def send(self, to: str, text: str) -> str:
            return f"sms from {self.sender_id} to {to}: {text}"
    
    
    class NotificationService:
        def __init__(self) -> None:
            self.sender = self.create_sender()  # runs the subclass's create_sender()
    
        def create_sender(self):
            raise NotImplementedError
    
        def notify_shipped(self, to: str, order: int) -> None:
            print(self.sender.send(to, f"Order {order} has shipped"))
    
    
    class SmsNotifications(NotificationService):
        def __init__(self, sender_id: str) -> None:
            super().__init__()  # create_sender() runs here, before sender_id exists
            self.sender_id = sender_id
    
        def create_sender(self) -> SmsSender:
            return SmsSender(self.sender_id)
    
    
    class FixedSmsNotifications(SmsNotifications):
        def __init__(self, sender_id: str) -> None:
            self.sender_id = sender_id  # set what create_sender() reads first
            NotificationService.__init__(self)
    
    
    try:
        SmsNotifications("SHOPIN").notify_shipped("+12025550100", 1042)
    except AttributeError as error:
        print("AttributeError:", error)
    
    FixedSmsNotifications("SHOPIN").notify_shipped("+12025550100", 1042)
    Show the answer to question 2

    Answer: it prints

    AttributeError: 'SmsNotifications' object has no attribute 'sender_id'
    sms from SHOPIN to +12025550100: Order 1042 has shipped

    Python calls the subclass's create_sender() even inside the base __init__, but SmsNotifications sets sender_id only after super().__init__() returns, so the attribute does not exist yet. The fixed class sets sender_id first and then runs the base initialiser.

  3. Question 3 of 6 What does this Java 25 program print?

    Read the code, then choose one answer.

    abstract class Base {
        Base() { System.out.println(label()); }
        abstract String label();
    }
    final class Child extends Base {
        private final String name;
        Child(String name) { super(); this.name = name; }
        String label() { return "child " + name; }
    }
    // in main:
    new Child("A");
    Show the answer to question 3

    Answer: child null

    super() runs Base's constructor before Child's constructor body assigns name, and Java dispatches the call to Child's label(), which reads the field while it still holds its default value, null. Concatenating null with a string gives "null", so nothing is thrown. Writing this.name = name; before super(); (allowed since Java 25, JEP 513) prints "child A".

  4. Question 4 of 6 In C++, a base class constructor calls the virtual function create_sender(), which the derived class overrides. Which version runs when a derived object is constructed?

    Choose one answer.

    Show the answer to question 4

    Answer: The base class's own version, because the derived part does not exist yet

    While the base constructor runs, the object is still a base object, so the call is resolved to the base's own function (C++ Core Guidelines C.82). In trap.cpp this makes an SMS service send email. If the base version is pure virtual, the call is undefined behaviour. A factory function that builds the object first and then calls the function (C.50) gets the override.

  5. Question 5 of 6 Which of these is Factory Method as the Gang of Four describe it, rather than a static factory method?

    Choose one answer.

    Show the answer to question 5

    Answer: A createSender() that a subclass overrides and the base class's own code calls

    Static factory methods, alternative constructors and new functions name a way of building one type, and nobody overrides them. Factory Method is an overridable method that the creator's own code calls, so a subclass decides the class without the creator changing.

  6. Question 6 of 6 In the registry example, which of these changes need no edit to senders.py?

    Choose every answer that is right.

    Show the answer to question 6

    Answer:

    • Adding a webhook channel from another file
    • Registering a second, differently configured SMS sender under a new name such as "sms-alerts"

    New channels and new configurations are new register() calls, which any file can make. The error message and the SmsSender class live in senders.py, so changing them is an edit to that file.

Interview questions

Fresher · What is the difference between Factory Method and a simple factory function?

A simple factory function is one function that decides which class to create, usually with a lookup or a chain of conditions, and every caller goes through it. Factory Method is an overridable method of a creator class: the creator’s own code calls it, and each subclass overrides it to choose the product, so the choice moves into the type of the creator instead of a condition. The simple factory is easier to read and enough when the set of products is closed and owned by one team. Factory Method, or its registry form, fits when other code must add products without editing the creator. A good answer adds that static factory methods such as Java’s List.of are a third thing: they name a way to build one type and cannot be overridden.

Key takeaways

  • Factory Method puts the choice of a concrete class behind one overridable method, so the code that uses the object is written once against an interface.
  • The registry form maps a name to a factory function; a new product is one register() call from any file. Refuse taken names and list the known ones in the error for an unknown name.
  • Simple factories and static factory methods (List.of, dict.fromkeys, NewX, Vec::new) are useful, but nobody overrides them, so they are not the catalogue’s Factory Method.
  • Each language has its own form: abstract methods in Java and TypeScript, function fields in Go, unique_ptr factories in C++, trait methods and an owned registry in Rust.
  • Never call an overridable factory method from a constructor: Python may miss attributes, Java sees default values, and C++ calls the base version. Create on first use, pass the product in, or use a factory function.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.