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.
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
The four roles of Factory Method
Text description of the diagram
The diagram is one column of four rows, read from the top.
- NotificationService is the creator. Its operation, notify_shipped(), calls create_sender(), the factory method, and uses only the Sender interface.
- EmailNotifications and SmsNotifications are the concrete creators. An arrow labelled extend points from them up to NotificationService: each one overrides create_sender().
- 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.
- 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, withsend(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 throughSender. - The concrete creators override the factory method.
SmsNotifications.create_sender()returns anSmsSenderconfigured 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
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
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")) Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
| Criterion | Factory Method | Registry of factories | Simple factory | Static factory method |
|---|---|---|---|---|
| Who chooses the class | The subclass of the creator | Whoever registered the name | One function, with a condition or a table | The method, for one type |
| Adding a product | A product class and a creator subclass | A product class and one register() call | An edit to the function | Not its job |
| Can other code add products | Yes, by subclassing | Yes, by registering | No | No |
| Example in this lesson | create_sender() in notify.py | senders.create(name) | The original if chain | List.of, dict.fromkeys, NewSmsSender |
| When to choose | A framework or base class whose own code needs objects that users of the framework choose | Plug-ins, configuration files and tests that add or swap products by name | A small, closed set of products owned by one team | Readable, 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.
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(())
} 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 storesSupplier<Sender>values in aTreeMap, which keeps the names sorted, andputIfAbsentrefuses a taken name in one call. The operation is callednotifyShippedbecauseObjectalready has a finalnotify()method. - TypeScript. An
abstractclass and aMapof functions. Node.js 24 runs a.tsfile by replacing its type annotations with spaces, without checking them, and only syntax that can simply be erased is accepted: parameter properties andenumdeclarations need a flag. The file declares its fields in the class body for that reason, and type checking stays the job oftscin a project. - Go. There are no subclasses, so the creator holds its factory method as a function field,
newSender, andEmailNotifications()andSmsNotifications(id)are constructor functions that fill it in. The registry is a map of functions. Go does not specify the order in which arangeloop visits a map, soKnown()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 nameunique_ptras the default way to say who owns an object. The registry lives in a function-localstatic, which is created the first timefactories()is called, so no other global can use it before it exists. - Rust. A trait with a required
create_sender()and a providednotify_shipped()is Factory Method without inheritance: every implementation gets the operation and supplies the factory. The registry is an ordinary value thatmainowns, not a global, so tests can each build their own, andBTreeMapkeeps 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:
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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:
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");
} 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.
| Criterion | Python | Java | C++ |
|---|---|---|---|
| Which method runs | The subclass's override | The subclass's override | The base class's own version |
| What goes wrong | Attributes the override reads may not exist yet | Fields the override reads still hold their defaults | The override is never called |
| Symptom in the run | AttributeError | A text from "null", or a NullPointerException | An SMS service that sends email |
| Fix shown | Assign before calling the base __init__ | Assign before super(), for fields without initializers | A 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
Supplierin Java, astd::functionin 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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
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_ptrfactories 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
- Design Patterns: Elements of Reusable Object-Oriented Software (Factory Method) (Addison-Wesley (Pearson))
- C++ Core Guidelines: C.50, C.82, R.20, R.21 and F.26 (Standard C++ Foundation)
- JEP 513: Flexible Constructor Bodies (OpenJDK)
- List (Java SE 25): unmodifiable lists and their static factory methods (Oracle)
- Rust API Guidelines: constructors are static, inherent methods (C-CTOR) (The Rust Project)
- Effective Go: constructors and composite literals (The Go Authors)
- Built-in types: dict.fromkeys (Python Software Foundation)
- abc: abstract base classes (Python Software Foundation)
- Node.js v24: modules, TypeScript (type stripping) (OpenJS Foundation)
- The Go Programming Language Specification (for statements with range clause) (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress