Factory
TLDR A "factory" is a pattern which encapsulates logic for creating new instances of classes.
Problem/context
Logic which determines which objects need to be created can be complex. That complexity can leak into other concerns in your code, making it harder to understand and harder to maintain.
This problem is found often in object-oriented codebases that make heavy use of polymorphism. In these codebases you often need to determine which implementation of an interface needs to be created, depending on configuration, user input and/or app state.
Concept
The core idea is to encapsulate the logic that determines when an object needs to be created.
Key characteristics
- Factories return instance of classes (as opposed to data).
- [My own definition] Factories always return new objects i.e. calling a factory twice should give you two objects. The results from the factory can be cached, but the factory itself should not cache.
- Factories get given all the information to construct the object up-front: either in the their constructor (if they're a class), as parameters to the factory function, or a combination of both. This is a key difference between patterns like Builders which configure new objects progressively.
Examples
A factory class that creates a new hero object according to player choices.
class PlayerHeroFactory {
private difficulty: GameDifficulty
constructor({ difficulty }: { difficulty: GameDifficulty }) {
this.difficulty = difficulty
}
get({ name, heroType }: PlayerChoices): PlayerHero {
const hardMode = this.difficulty === "hard"
const startingHealth = hardMode ? 50 : 100
switch (chosenHero) {
case "swordsman":
const weapon = hardMode ? "stick" : "sword"
return new Swordsman({ name, startingHealth, weapon })
case "magician":
const spell = hardMode ? "ignite" : "fireball"
return new Magician({ name, startingHealth, spell })
}
}
}
type GameDifficulty = "normal" | "hard"
A factory method in a board-game class that creates a new initial board depending on the starting number of players.
// Note: several class definitions have been left out for brevity
class BoardGame {
private board: Board
constructor({ numberOfPlayers }: { numberOfPlayers: number }) {
this.board = getInitialBoard({ numberOfPlayers })
}
private getInitialBoard({
numberOfPlayers,
}: {
numberOfPlayers: number
}): Board {
switch (numberOfPlayers) {
case 4:
return new FourPlayerBoard()
case 3:
return new ThreePlayerBoard()
case 2:
return new TwoPlayerBoard()
default:
throw new Error(
`Cannot create a new board for ${numberOfPlayers} players`,
)
}
}
}
A factory function which creates a new interceptor depending on application configuration.
// Note: several class definitions have been left out for brevity
function getLoggingInterceptor({
devMode,
remoteLogging,
}: AppConfig): Interceptor {
if (devMode) {
return new ConsoleLoggingInterceptor()
}
if (remoteLogging) {
return new RemoteLoggingInterceptor()
}
return LocalLoggingInterceptor()
}