The Factory Pattern in JavaScript
Below is verified information on the Factory Pattern in JavaScript, drawn from reputable sources such as MDN Web Docs, Refactoring Guru - Factory Method, and Addy Osmani - Learning JavaScript Design Patterns.
On this page
The Factory Pattern in JavaScript
Below is verified information about the Factory Pattern in JavaScript, based on reputable sources such as MDN Web Docs, Refactoring Guru - Factory Method, and Addy Osmani - Learning JavaScript Design Patterns.
1. Definition
The Factory Pattern is a creational design pattern.
It encapsulates the object-creation process, letting you create objects without specifying their exact class.
You only work with a "factory" - which decides what type of object to create based on parameters or conditions.
In short: the Factory Pattern separates object-creation logic from object usage, ensuring your software stays extensible and maintainable.
2. When Should You Use the Factory Pattern?
- When you need to create many objects that share the same interface but differ in their internal details.
- When the object-creation logic is complex or varies from case to case.
- When you want to reduce direct dependency on concrete classes (reduce coupling).
3. How to Implement the Factory Pattern in JavaScript
Illustrative Example
abstract class Creator {
public abstract factoryMethod(): Product;
public someOperation(): string {
// Call the factory method to create a Product object.
const product = this.factoryMethod();
// Now, use the product.
return `Creator: The same creator's code has just worked with ${product.operation()}`;
}
}
class ConcreteCreator1 extends Creator {
public factoryMethod(): Product {
return new ConcreteProduct1();
}
}
class ConcreteCreator2 extends Creator {
public factoryMethod(): Product {
return new ConcreteProduct2();
}
}
interface Product {
operation(): string;
}
class ConcreteProduct1 implements Product {
public operation(): string {
return '{Result of the ConcreteProduct1}';
}
}
class ConcreteProduct2 implements Product {
public operation(): string {
return '{Result of the ConcreteProduct2}';
}
}
function clientCode(creator: Creator) {
// ...
console.log('Client: I\'m not aware of the creator\'s class, but it still works.');
console.log(creator.someOperation());
// ...
}
console.log('App: Launched with the ConcreteCreator1.');
clientCode(new ConcreteCreator1());
console.log('');
console.log('App: Launched with the ConcreteCreator2.');
clientCode(new ConcreteCreator2());
4. Pros
- Increased extensibility: easily add new object types without touching the code that uses the factory.
- Hides creation logic: however complex, it stays inside the factory, invisible to the calling code.
- Reduces coupling (decoupling): code isn't tightly bound to concrete classes.
5. Cons
- More classes/factory functions: with too many object types, the factory code can become complex.
- Harder to read if overused: with only 1-2 simple object types, a factory is sometimes unnecessary.
6. Real-World Applications
💡 When Should You Use the Factory Method?
a. When you don't know in advance exactly what type of object and dependencies your code will work with
- Meaning: sometimes you write code without being able to determine ahead of time exactly what object type is needed (e.g.,
DogorCat, a squareButtonor a round one). - Solution: the Factory Method separates the code that creates a product (object) from the code that uses that product.
- Benefit: to add a new product, you just create a new subclass and override the factory method - no need to touch existing code.
Example:
// Interface for animals
class Animal {
speak() {}
}
class Dog extends Animal {
speak() { console.log("Woof!"); }
}
class Cat extends Animal {
speak() { console.log("Meow!"); }
}
// Factory Method
function createAnimal(type) {
if (type === "dog") return new Dog();
if (type === "cat") return new Cat();
throw new Error("Unknown type");
}
// Usage
const animal1 = createAnimal("dog");
animal1.speak(); // Woof!
const animal2 = createAnimal("cat");
animal2.speak(); // Meow!
b. When you want users of your library or framework to be able to easily extend its internal components
- Inheritance is the simplest way to extend a library's default behavior.
- The problem: how does the framework know to use your new subclass instead of the default component?
- The solution: gather all the component-creation logic into a single factory method and allow it to be overridden.
Example:
// Base component
class Button {
render() { console.log("Square Button"); }
}
// An extended component
class RoundButton extends Button {
render() { console.log("Round Button"); }
}
// The base UI framework
class UIFramework {
createButton() {
return new Button();
}
renderButton() {
this.createButton().render();
}
}
// A framework subclass that extends it
class UIWithRoundButtons extends UIFramework {
createButton() {
return new RoundButton();
}
}
// Usage
const ui = new UIWithRoundButtons();
ui.renderButton(); // Round Button
c. When you want to save system resources by reusing objects instead of constantly creating new ones
- This commonly happens when those objects are "heavy," like database connections, filesystem handles, or network resources.
- To reuse them, you need to:
- Create somewhere to store the objects already created (an object pool).
- When one is needed, check whether a "free" object exists to return.
- If none exists, create a new one and store it in the pool.
- If this logic is scattered everywhere, the code becomes messy and duplicated.
- The Factory Method lets you put all this logic in one single place, giving you centralized, clean control over reusing objects.
Example:
class DBConnection {
constructor(id) {
this.id = id;
this.busy = false;
}
}
// An Object Pool using the Factory Method
class DBConnectionFactory {
constructor() {
this.pool = [];
this.counter = 0;
}
getConnection() {
// Find a free connection
const freeConn = this.pool.find(conn => !conn.busy);
if (freeConn) {
freeConn.busy = true;
return freeConn;
}
// If none exists, create a new one
const conn = new DBConnection(++this.counter);
conn.busy = true;
this.pool.push(conn);
return conn;
}
releaseConnection(conn) {
conn.busy = false;
}
}
// Usage
const factory = new DBConnectionFactory();
const conn1 = factory.getConnection();
console.log(conn1.id); // 1
factory.releaseConnection(conn1);
const conn2 = factory.getConnection();
console.log(conn2.id); // 1 (reused)
