Factory
new SmsNotifier(apiKey, senderId) appeared in 23 files. Then the telecom regulator
required a registered entity id on every commercial SMS, and the constructor needed a
third argument. That was 23 edits, in 23 files, owned by five teams. Two were missed. They
compiled, because the new argument had been made optional to get the release out, and
those two code paths sent messages that the operator silently dropped.
Twenty three places knew how to build a notifier. One should have.
Many parts of your code need a notifier for a channel: sms, email or push. Each kind is constructed differently. Where would you put the knowledge of which class to create and what arguments it needs?
The idea
You say “one masala dosa”. You do not walk into the kitchen, pick a tawa, and work out which cook is free. The counter takes the name of what you want and hands back the finished thing. If the kitchen changes its tawa supplier tomorrow, you order the same way.
A factory is a function or method whose job is to create objects, so that the code asking
for one does not call new on a specific class.
Before: everyone builds their own
class SmsNotifier {
constructor(private readonly senderId: string) {}
send(to: string, text: string): string {
return `SMS from ${this.senderId} to ${to}: ${text}`
}
}
class EmailNotifier {
send(to: string, text: string): string {
return `Email to ${to}: ${text}`
}
}
function confirmOrder(channel: string, to: string): string {
const text = 'Order confirmed'
if (channel === 'sms') return new SmsNotifier('INDGEK').send(to, text)
if (channel === 'email') return new EmailNotifier().send(to, text)
throw new Error(`Unknown channel: ${channel}`)
}
console.log(confirmOrder('sms', '9810012345'))
// > SMS from INDGEK to 9810012345: Order confirmedconfirmOrder knows every notifier class and every constructor argument. So does
cancelOrder, and sendOtp, and twenty more. That knowledge is duplicated 23 times, and
channel is a plain string, so 'emial' gets through the compiler.
After: one place builds, everyone else asks
interface Notifier {
send(to: string, text: string): string
}
class SmsNotifier implements Notifier {
constructor(private readonly senderId: string) {}
send(to: string, text: string): string {
return `SMS from ${this.senderId} to ${to}: ${text}`
}
}
class EmailNotifier implements Notifier {
send(to: string, text: string): string {
return `Email to ${to}: ${text}`
}
}
class PushNotifier implements Notifier {
send(to: string, text: string): string {
return `Push to ${to}: ${text}`
}
}
type Channel = 'sms' | 'email' | 'push'
function createNotifier(channel: Channel): Notifier {
switch (channel) {
case 'sms':
return new SmsNotifier('INDGEK')
case 'email':
return new EmailNotifier()
case 'push':
return new PushNotifier()
}
}
function confirmOrder(channel: Channel, to: string): string {
return createNotifier(channel).send(to, 'Order confirmed')
}
console.log(confirmOrder('sms', '9810012345'))
// > SMS from INDGEK to 9810012345: Order confirmed
console.log(confirmOrder('push', 'asha')) // Push to asha: Order confirmed
createNotifier('fax')
// Error: Argument of type '"fax"' is not assignable to parameter
// of type 'Channel'.createNotifier is the factory. It returns the Notifier interface, so callers cannot
depend on which class they got. The regulator’s new argument is now a one line change
inside it.
Two TypeScript details make this safer than the Java version. Channel is a union of
literals, so a typo is a compile error. And the switch has no default. TypeScript can
see that all three cases return, so the function is complete. Add 'whatsapp' to
Channel and the function stops compiling until you handle it.
The registry version
The switch still has to be edited for each new channel. A lookup table removes even
that, and teaches a useful type on the way.
const creators: Record<Channel, () => Notifier> = {
sms: () => new SmsNotifier('INDGEK'),
email: () => new EmailNotifier(),
push: () => new PushNotifier(),
}
function notifierFor(channel: Channel): Notifier {
return creators[channel]()
}
const email = notifierFor('email')
console.log(email.send('asha@example.com', 'Order confirmed'))
// > Email to asha@example.com: Order confirmedRecord<Channel, () => Notifier> is an object with exactly one key for every member of
Channel, each holding a function that returns a Notifier. Leave one out and the
object does not compile. Each entry is a function, not an instance, so a notifier is
created only when someone asks for it.
The textbook pattern: Factory Method
What you have seen so far is usually called a simple factory. The pattern in the original book is slightly different, and interviewers like to check that you know the difference. In Factory Method, creation is a method that subclasses override.
interface Notifier {
send(to: string, text: string): string
}
class SmsNotifier implements Notifier {
send(to: string, text: string): string {
return `SMS to ${to}: ${text}`
}
}
class PushNotifier implements Notifier {
send(to: string, text: string): string {
return `Push to ${to}: ${text}`
}
}
abstract class OrderFlow {
// The factory method. Each subclass decides what to create.
protected abstract createNotifier(): Notifier
confirm(to: string, orderId: number): string {
const notifier = this.createNotifier()
return notifier.send(to, `Order ${orderId} confirmed`)
}
}
class AppOrderFlow extends OrderFlow {
protected override createNotifier(): Notifier {
return new PushNotifier()
}
}
class PhoneOrderFlow extends OrderFlow {
protected override createNotifier(): Notifier {
return new SmsNotifier()
}
}
console.log(new AppOrderFlow().confirm('asha', 42))
// > Push to asha: Order 42 confirmed
console.log(new PhoneOrderFlow().confirm('9810012345', 43))
// > SMS to 9810012345: Order 43 confirmedOrderFlow.confirm is written once and uses a notifier without knowing which. The
subclass fills in the one step that differs. There is no switch anywhere. The choice is
made by which subclass you created.
In TypeScript you will write the simple factory ten times for every time you write this one. Know both, and reach for the function first.
Three names, often blurred. A simple factory is one function with a switch or a table.
Factory Method moves creation into a method that subclasses override. Abstract Factory
is an object that creates a whole family of related things that must match, like a
DarkThemeFactory that makes a dark button, a dark input and a dark menu. If you are
asked which one you used, say which, and say why the simpler one was enough.
createUser() that contains only return new User() hides nothing and decides nothing.
A factory earns its place when there is a choice to make (which class), or construction
is involved enough that you do not want it repeated (config, credentials, defaults).
With neither, new User() is clearer.
type Channel = 'sms' | 'email' | 'push'function createNotifier(channel: Channel): Notifierswitch (channel) { case ...: return ... }Record<Channel, () => Notifier>() => new EmailNotifier()creators[channel]()protected abstract createNotifier(): NotifierCheckpoint
1. Why does createNotifier return the Notifier interface and not SmsNotifier | EmailNotifier | PushNotifier?
2. You add "whatsapp" to the Channel union and forget the factory. With Record<Channel, () => Notifier>, what happens?
3. What separates Factory Method from a simple factory function?
A factory puts object creation in one place, so the rest of the code asks for what it wants by name and never calls new on a concrete class. Callers depend on an interface and on the factory, and only the factory knows which class to build and what arguments it needs. In TypeScript the usual form is a function that takes a union of literal names and returns the interface, either with an exhaustive switch or a Record lookup table, and the compiler tells me if I add a kind and forget to handle it. The textbook Factory Method is a bit different: creation is an abstract method that subclasses override. I use a factory when there is a real choice of class or the construction is complicated, and I do not wrap a single new call in one.
