Creational Patterns Overview
Creational patterns deal with object creation mechanisms. They abstract the instantiation process, making it more flexible and reusable. Instead of using `new` directly, these patterns give you controlled, flexible ways to create objects.
Singleton Pattern
Ensures a class has only one instance and provides a global point of access to it. Useful for things like database connections, configuration managers, and logging services.
class DatabaseConnection {
static #instance = null;
#connection;
constructor() {
if (DatabaseConnection.#instance) {
throw new Error("Use DatabaseConnection.getInstance()");
}
this.#connection = this.#createConnection();
DatabaseConnection.#instance = this;
}
static getInstance() {
if (!DatabaseConnection.#instance) {
DatabaseConnection.#instance = new DatabaseConnection();
}
return DatabaseConnection.#instance;
}
#createConnection() {
console.log("Establishing database connection...");
return { host: "localhost", port: 5432, status: "connected" };
}
query(sql) {
console.log(`Executing: ${sql}`);
return [];
}
close() {
console.log("Connection closed.");
DatabaseConnection.#instance = null;
}
}
// Only one connection ever exists
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true
db1.query("SELECT * FROM users");
// db2.query(...) uses the same connection
Factory Method Pattern
Defines an interface for creating objects but lets subclasses decide which class to instantiate. The factory method lets a class defer instantiation to subclasses.
// Product interface
class Notification {
send(message) { throw new Error("Implement send()"); }
}
// Concrete Products
class EmailNotification extends Notification {
#email;
constructor(email) { super(); this.#email = email; }
send(message) {
console.log(`Email to ${this.#email}: ${message}`);
}
}
class SMSNotification extends Notification {
#phone;
constructor(phone) { super(); this.#phone = phone; }
send(message) {
console.log(`SMS to ${this.#phone}: ${message}`);
}
}
class PushNotification extends Notification {
#deviceId;
constructor(deviceId) { super(); this.#deviceId = deviceId; }
send(message) {
console.log(`Push to ${this.#deviceId}: ${message}`);
}
}
// Creator with factory method
class NotificationService {
createNotification(type, contact) {
switch (type) {
case "email": return new EmailNotification(contact);
case "sms": return new SMSNotification(contact);
case "push": return new PushNotification(contact);
default: throw new Error(`Unknown type: ${type}`);
}
}
notify(type, contact, message) {
const notification = this.createNotification(type, contact);
notification.send(message);
}
}
const service = new NotificationService();
service.notify("email", "user@example.com", "Welcome!");
service.notify("sms", "+1234567890", "Your code is 1234");
service.notify("push", "device-abc", "New message received");
Builder Pattern
Separates the construction of a complex object from its representation. You build the object step by step using the same construction process.
classQueryBuilder {
#table;
#conditions = [];
#orderBy;
#limit;
#fields = ["*"];
constructor(table) {
this.#table = table;
}
select(...fields) {
this.#fields = fields;
return this; // enables chaining
}
where(condition) {
this.#conditions.push(condition);
return this;
}
orderBy(field, direction = "ASC") {
this.#orderBy = `${field} ${direction}`;
return this;
}
limit(n) {
this.#limit = n;
return this;
}
build() {
let sql = `SELECT ${this.#fields.join(", ")} FROM ${this.#table}`;
if (this.#conditions.length > 0) {
sql += ` WHERE ${this.#conditions.join(" AND ")}`;
}
if (this.#orderBy) {
sql += ` ORDER BY ${this.#orderBy}`;
}
if (this.#limit) {
sql += ` LIMIT ${this.#limit}`;
}
return sql;
}
}
// Usage โ step-by-step construction
const query = new QueryBuilder("users")
.select("id", "name", "email")
.where("age > 18")
.where("status = 'active'")
.orderBy("name")
.limit(10)
.build();
console.log(query);
// SELECT id, name, email FROM users WHERE age > 18 AND status = 'active' ORDER BY name ASC LIMIT 10
Prototype Pattern
Creates new objects by cloning existing instances (prototypes). Useful when object creation is expensive and you want to avoid repeated initialization.
class Car {
constructor(model, color, engine) {
this.model = model;
this.color = color;
this.engine = engine;
this.features = [];
}
clone() {
const cloned = new Car(this.model, this.color, { ...this.engine });
cloned.features = [...this.features];
return cloned;
}
describe() {
return `${this.color} ${this.model} (${this.engine.type}) - ${this.features.join(", ")}`;
}
}
// Create a prototype
const baseModel = new Car("Tesla Model 3", "White", {
type: "Electric",
power: "283 hp",
range: "358 km"
});
baseModel.features = ["Autopilot", "Touchscreen", "Bluetooth"];
// Clone for different variants
const redModel = baseModel.clone();
redModel.color = "Red";
const blueModel = baseModel.clone();
blueModel.color = "Blue";
blueModel.features.push("Sunroof");
console.log(baseModel.describe());
// White Tesla Model 3 (Electric) - Autopilot, Touchscreen, Bluetooth
console.log(redModel.describe());
// Red Tesla Model 3 (Electric) - Autopilot, Touchscreen, Bluetooth
console.log(blueModel.describe());
// Blue Tesla Model 3 (Electric) - Autopilot, Touchscreen, Bluetooth, Sunroof
Abstract Factory Pattern
Provides an interface for creating families of related objects without specifying their concrete classes.
// Abstract Factory
class UIFactory {
createButton() { throw new Error("Implement me"); }
createInput() { throw new Error("Implement me"); }
}
// Concrete Factories
class WindowsFactory extends UIFactory {
createButton() { return { render: () => "Windows Button" }; }
createInput() { return { render: () => "Windows Input" }; }
}
class MacFactory extends UIFactory {
createButton() { return { render: () => "Mac Button" }; }
createInput() { return { render: () => "Mac Input" }; }
}
// Client code โ works with any factory
function buildUI(factory) {
const button = factory.createButton();
const input = factory.createInput();
return { button: button.render(), input: input.render() };
}
// Switch platforms without changing client code
const windowsUI = buildUI(new WindowsFactory());
const macUI = buildUI(new MacFactory());
console.log(windowsUI); // { button: "Windows Button", input: "Windows Input" }
console.log(macUI); // { button: "Mac Button", input: "Mac Input" }