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.

Your answer

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

The picture to keep
The restaurant counter

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.

In one line: Ask for what you want by name. One place decides which class that means and how to build it.

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 confirmed

confirmOrder 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.

Figure 1. Callers know the factory and the interface. Only the factory knows the concrete classes and how each one is constructed.

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 confirmed

Record<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 confirmed

OrderFlow.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.

Simple factory, Factory Method, Abstract Factory

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.

A factory for one class

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.

TypeScript you just picked up
type Channel = 'sms' | 'email' | 'push'
The names a caller may ask for. Anything else fails to compile.
function createNotifier(channel: Channel): Notifier
Returns the interface, so the caller cannot rely on the concrete class.
switch (channel) { case ...: return ... }
With every member handled and no default, TypeScript knows the function always returns.
Record<Channel, () => Notifier>
An object type with one required key per member of Channel.
() => new EmailNotifier()
A function that creates on demand. Stored now, called later.
creators[channel]()
Look up the function by key, then call it.
protected abstract createNotifier(): Notifier
A blank for subclasses to fill in, not callable from outside.

Checkpoint

Checkpoint

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?

Say this in 60 seconds

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.

IndGeek provides solutions in the software field, and is a hub for ultimate Tech Knowledge.