Day 28: Design Patterns
Day 28: Design Patterns
1. Learning Objectives
By the end of this lesson, you will be able to:
- Understand what design patterns are and why they are useful
- Implement the Module pattern for encapsulation and organization
- Use the Observer pattern for event-driven communication
- Apply the Singleton pattern for shared state
- Implement the Factory pattern for object creation
- Use the Prototype pattern for object cloning
- Apply the Decorator pattern for adding behavior dynamically
- Recognize when to use each pattern in real-world applications
2. Theory
What are Design Patterns?
Design patterns are reusable solutions to common problems in software design. They are not code snippets but rather templates for how to solve problems. Patterns were popularized by the "Gang of Four" (GoF) book in 1994, but JavaScript's dynamic nature allows for simpler implementations.
Why Use Design Patterns?
- Proven solutions — avoid reinventing the wheel
- Common vocabulary — communicate design intent clearly
- Code maintainability — structured, predictable code
- Reusability — patterns transcend specific problems
Categories of Patterns
| Category | Purpose | Examples |
|---|---|---|
| Creational | Object creation mechanisms | Singleton, Factory, Prototype, Builder |
| Structural | Object composition | Decorator, Proxy, Adapter, Facade |
| Behavioral | Object communication | Observer, Strategy, Command, Iterator |
Module Pattern
The Module pattern provides encapsulation by creating private scope and exposing a public API. In modern JS, ES6 modules have largely replaced this, but the pattern is still useful for understanding scope.
// Module pattern using IIFE
const CounterModule = (function() {
let count = 0; // Private variable
function log(action) {
console.log(`${action}: count is now ${count}`);
}
// Public API
return {
increment() {
count++;
log("increment");
return count;
},
decrement() {
count--;
log("decrement");
return count;
},
getCount() {
return count;
},
reset() {
count = 0;
log("reset");
return count;
}
};
})();
console.log(CounterModule.increment()); // 1
console.log(CounterModule.getCount()); // 1
console.log(CounterModule.count); // undefined (private)
Revealing Module Pattern
A variation where all methods are defined privately and only references are exposed:
const UserModule = (function() {
let users = [];
function validate(name) {
return name && name.trim().length > 0;
}
function add(name) {
if (!validate(name)) return { success: false, error: "Invalid name" };
const user = { id: Date.now(), name: name.trim() };
users.push(user);
return { success: true, user };
}
function remove(id) {
const index = users.findIndex(u => u.id === id);
if (index === -1) return { success: false, error: "User not found" };
users.splice(index, 1);
return { success: true };
}
function getAll() {
return [...users]; // Return copy
}
// Reveal public references
return { add, remove, getAll };
})();
Observer Pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified. This is the foundation of event-driven programming.
class Subject {
constructor() {
this.observers = new Set();
}
subscribe(observer) {
this.observers.add(observer);
return () => this.observers.delete(observer); // Return unsubscribe
}
unsubscribe(observer) {
this.observers.delete(observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} received:`, data);
}
}
// Usage
const subject = new Subject();
const observer1 = new Observer("Observer 1");
const observer2 = new Observer("Observer 2");
const unsubscribe = subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify("Hello!"); // Both receive
unsubscribe(); // Observer 1 unsubscribes
subject.notify("Again"); // Only Observer 2 receives
Real-World: Event Emitter
class EventEmitter {
constructor() {
this.events = new Map();
}
on(event, listener) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(listener);
return () => this.off(event, listener);
}
off(event, listener) {
const listeners = this.events.get(event);
if (listeners) {
const index = listeners.indexOf(listener);
if (index !== -1) listeners.splice(index, 1);
}
}
emit(event, ...args) {
const listeners = this.events.get(event);
if (listeners) {
listeners.forEach(listener => listener(...args));
}
}
once(event, listener) {
const wrapper = (...args) => {
listener(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
// Usage
const emitter = new EventEmitter();
const unsub = emitter.on("data", (msg) => console.log("Data:", msg));
emitter.emit("data", "Hello"); // Logs
unsub();
emitter.emit("data", "World"); // No log
Singleton Pattern
The Singleton pattern ensures a class has only one instance and provides a global point of access to it.
class Database {
constructor() {
if (Database.instance) {
return Database.instance;
}
this.connection = null;
Database.instance = this;
}
connect(url) {
console.log(`Connecting to ${url}...`);
this.connection = url;
return this;
}
query(sql) {
console.log(`Executing query on ${this.connection}: ${sql}`);
return [];
}
}
// Freeze the instance to prevent modifications
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true
db1.connect("mongodb://localhost:27017");
db2.query("SELECT * FROM users"); // Uses db1's connection
Singleton with Module Pattern
const ConfigManager = (function() {
let instance;
function createInstance() {
return {
settings: {
theme: "light",
language: "en",
debug: false
},
get(key) {
return this.settings[key];
},
set(key, value) {
this.settings[key] = value;
},
getAll() {
return { ...this.settings };
}
};
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();
console.log(config1 === config2); // true
config1.set("theme", "dark");
console.log(config2.get("theme")); // "dark"
Factory Pattern
The Factory pattern provides an interface for creating objects without specifying their concrete classes.
// Simple factory function
function createUser(type, data) {
switch (type) {
case "admin":
return {
...data,
role: "admin",
permissions: ["read", "write", "delete"],
isAdmin: true
};
case "moderator":
return {
...data,
role: "moderator",
permissions: ["read", "write"],
isAdmin: false
};
case "user":
return {
...data,
role: "user",
permissions: ["read"],
isAdmin: false
};
default:
throw new Error(`Unknown user type: ${type}`);
}
}
const admin = createUser("admin", { name: "Alice", email: "alice@example.com" });
const user = createUser("user", { name: "Bob", email: "bob@example.com" });
console.log(admin.permissions); // ["read", "write", "delete"]
console.log(user.permissions); // ["read"]
Factory with Classes
class Button {
constructor(text, type) {
this.text = text;
this.type = type;
}
render() {
return `<button class="${this.type}">${this.text}</button>`;
}
}
class PrimaryButton extends Button {
constructor(text) {
super(text, "primary");
}
}
class SecondaryButton extends Button {
constructor(text) {
super(text, "secondary");
}
}
class DangerButton extends Button {
constructor(text) {
super(text, "danger");
}
}
class ButtonFactory {
static createButton(type, text) {
switch (type) {
case "primary": return new PrimaryButton(text);
case "secondary": return new SecondaryButton(text);
case "danger": return new DangerButton(text);
default: return new Button(text, "default");
}
}
}
const saveBtn = ButtonFactory.createButton("primary", "Save");
const deleteBtn = ButtonFactory.createButton("danger", "Delete");
console.log(saveBtn.render()); // <button class="primary">Save</button>
Prototype Pattern
The Prototype pattern creates new objects by cloning an existing object (the prototype). JavaScript's prototypal inheritance naturally supports this.
// Using Object.create for prototype-based cloning
const carPrototype = {
init(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
return this;
},
getInfo() {
return `${this.year} ${this.make} ${this.model}`;
},
clone() {
return Object.create(Object.getPrototypeOf(this),
Object.getOwnPropertyDescriptors(this));
}
};
const car1 = Object.create(carPrototype).init("Toyota", "Camry", 2020);
const car2 = car1.clone();
car2.year = 2024;
console.log(car1.getInfo()); // 2020 Toyota Camry
console.log(car2.getInfo()); // 2024 Toyota Camry
Using structuredClone (modern)
class ShoppingCart {
constructor(items = []) {
this.items = items;
}
addItem(item) {
this.items.push(item);
}
clone() {
return structuredClone(this);
}
}
const cart1 = new ShoppingCart([{ id: 1, name: "Laptop" }]);
const cart2 = cart1.clone();
cart2.addItem({ id: 2, name: "Mouse" });
console.log(cart1.items.length); // 1
console.log(cart2.items.length); // 2 (deep copy)
Decorator Pattern
The Decorator pattern allows adding behavior to objects dynamically without affecting other objects of the same class.
// Simple function decorator
function withLogging(fn) {
return function(...args) {
console.log(`Calling function with args: ${args}`);
const result = fn(...args);
console.log(`Result: ${result}`);
return result;
};
}
function add(a, b) {
return a + b;
}
const loggedAdd = withLogging(add);
loggedAdd(3, 5); // Logs: Calling function..., Result: 8
// Class decorator pattern (mixin-like)
class Coffee {
cost() { return 5; }
description() { return "Coffee"; }
}
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() { return this.coffee.cost() + 2; }
description() { return this.coffee.description() + ", Milk"; }
}
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() { return this.coffee.cost() + 1; }
description() { return this.coffee.description() + ", Sugar"; }
}
let myCoffee = new Coffee();
myCoffee = new MilkDecorator(myCoffee);
myCoffee = new SugarDecorator(myCoffee);
console.log(myCoffee.description()); // Coffee, Milk, Sugar
console.log(myCoffee.cost()); // 8 (5 + 2 + 1)
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
// Strategy objects
const pricingStrategies = {
regular(price) {
return price;
},
student(price) {
return price * 0.8; // 20% discount
},
premium(price) {
return price * 0.5; // 50% discount
},
holiday(price) {
return price * 0.85; // 15% seasonal discount
}
};
class PriceCalculator {
constructor(strategy = "regular") {
this.setStrategy(strategy);
}
setStrategy(strategy) {
if (typeof strategy === "string") {
this.strategy = pricingStrategies[strategy];
} else {
this.strategy = strategy;
}
}
calculate(price) {
return this.strategy(price);
}
}
const calculator = new PriceCalculator();
console.log(calculator.calculate(100)); // 100
calculator.setStrategy("student");
console.log(calculator.calculate(100)); // 80
calculator.setStrategy("premium");
console.log(calculator.calculate(100)); // 50
// Custom strategy
calculator.setStrategy((price) => price * 0.9);
console.log(calculator.calculate(100)); // 90
3. Code Examples
Example 1: Module Pattern — Todo Store
const TodoStore = (function() {
let todos = [];
let nextId = 1;
const listeners = new Set();
function notify() {
listeners.forEach(fn => fn(todos));
}
return {
add(title) {
const todo = { id: nextId++, title, completed: false, createdAt: new Date() };
todos = [...todos, todo]; // Immutable update
notify();
return todo;
},
toggle(id) {
todos = todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
notify();
},
remove(id) {
todos = todos.filter(t => t.id !== id);
notify();
},
getAll() {
return [...todos];
},
getStats() {
const total = todos.length;
const completed = todos.filter(t => t.completed).length;
return { total, completed, pending: total - completed };
},
subscribe(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
};
})();
Example 2: Observer Pattern — Notification System
// Notification system using Observer
class NotificationSystem {
constructor() {
this.channels = new Map();
}
subscribe(channel, user) {
if (!this.channels.has(channel)) {
this.channels.set(channel, new Set());
}
this.channels.get(channel).add(user);
return () => this.channels.get(channel)?.delete(user);
}
publish(channel, message) {
const subscribers = this.channels.get(channel);
if (subscribers) {
subscribers.forEach(user => {
user.receive(channel, message);
});
}
}
}
class User {
constructor(name) {
this.name = name;
}
receive(channel, message) {
console.log(`[${this.name}] ${channel}: ${message}`);
}
}
// Usage
const notifications = new NotificationSystem();
const alice = new User("Alice");
const bob = new User("Bob");
const unsubAlice = notifications.subscribe("general", alice);
notifications.subscribe("general", bob);
notifications.subscribe("updates", alice);
notifications.publish("general", "Hello everyone!"); // Both receive
notifications.publish("updates", "New version released!"); // Only Alice
unsubAlice();
notifications.publish("general", "Is anyone here?"); // Only Bob
Example 3: Factory Pattern — Payment Processor
// Payment processing with Factory
class CreditCardPayment {
constructor(details) {
this.cardNumber = details.cardNumber;
this.expiry = details.expiry;
this.cvv = details.cvv;
}
process(amount) {
console.log(`Processing $${amount} via Credit Card (${this.cardNumber.slice(-4)})`);
return { success: true, transactionId: `CC-${Date.now()}` };
}
}
class PayPalPayment {
constructor(details) {
this.email = details.email;
}
process(amount) {
console.log(`Processing $${amount} via PayPal (${this.email})`);
return { success: true, transactionId: `PP-${Date.now()}` };
}
}
class CryptoPayment {
constructor(details) {
this.walletAddress = details.walletAddress;
this.currency = details.currency || "BTC";
}
process(amount) {
console.log(`Processing $${amount} via ${this.currency} (${this.walletAddress.slice(0, 6)}...)`);
return { success: true, transactionId: `CR-${Date.now()}` };
}
}
class PaymentFactory {
static createProcessor(type, details) {
switch (type) {
case "credit": return new CreditCardPayment(details);
case "paypal": return new PayPalPayment(details);
case "crypto": return new CryptoPayment(details);
default: throw new Error(`Unknown payment type: ${type}`);
}
}
}
// Usage
const payment = PaymentFactory.createProcessor("paypal", { email: "user@example.com" });
const result = payment.process(99.99);
console.log(result);
Example 4: Decorator Pattern — Middleware Pipeline
// Express-like middleware using Decorator pattern
class RequestHandler {
constructor() {
this.middlewares = [];
}
use(middleware) {
this.middlewares.push(middleware);
return this; // For chaining
}
async handle(request) {
let index = 0;
const next = async () => {
if (index < this.middlewares.length) {
const middleware = this.middlewares[index++];
await middleware(request, next);
}
};
await next();
return request;
}
}
// Usage
const app = new RequestHandler();
// Logger middleware
app.use(async (req, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
await next();
});
// Auth middleware
app.use(async (req, next) => {
if (!req.headers?.authorization) {
req.error = "Unauthorized";
return;
}
req.user = { id: 1, name: "Alice" };
await next();
});
// Route handler
app.use(async (req, next) => {
if (req.error) {
req.response = { status: 401, body: { error: req.error } };
} else {
req.response = { status: 200, body: { message: `Hello ${req.user.name}` } };
}
});
// Test
app.handle({ method: "GET", url: "/api/users", headers: { authorization: "Bearer token" } })
.then(req => console.log(req.response));
Example 5: Combining Patterns — E-Commerce System
// Combining Factory, Singleton, Observer, and Module patterns
// Logger (Singleton)
const Logger = (function() {
let instance;
function createInstance() {
return {
logs: [],
log(level, message) {
const entry = { level, message, timestamp: new Date() };
this.logs.push(entry);
console.log(`[${level.toUpperCase()}] ${message}`);
},
info(m) { this.log("info", m); },
warn(m) { this.log("warn", m); },
error(m) { this.log("error", m); },
getLogs() { return [...this.logs]; }
};
}
return { getInstance: () => instance || (instance = createInstance()) };
})();
// Product Factory
class ProductFactory {
static create(type, data) {
switch (type) {
case "physical":
return { ...data, type: "physical", requiresShipping: true };
case "digital":
return { ...data, type: "digital", requiresShipping: false };
case "service":
return { ...data, type: "service", requiresShipping: false };
default:
throw new Error(`Unknown product type: ${type}`);
}
}
}
// Order Subject (Observer)
class OrderSubject {
constructor() {
this.observers = new Set();
}
subscribe(observer) {
this.observers.add(observer);
}
unsubscribe(observer) {
this.observers.delete(observer);
}
notify(event, data) {
this.observers.forEach(obs => obs.update(event, data));
}
}
// Email Observer
class EmailService {
update(event, data) {
if (event === "order_placed") {
Logger.getInstance().info(`Sending email confirmation for order #${data.orderId}`);
}
}
}
// Inventory Observer
class InventoryService {
update(event, data) {
if (event === "order_placed") {
data.items.forEach(item => {
Logger.getInstance().info(`Updating inventory for ${item.name}`);
});
}
}
}
// Order Module
const OrderModule = (function() {
const orders = [];
const subject = new OrderSubject();
const logger = Logger.getInstance();
return {
init() {
subject.subscribe(new EmailService());
subject.subscribe(new InventoryService());
logger.info("Order module initialized");
},
placeOrder(items) {
const order = {
orderId: `ORD-${Date.now()}`,
items,
total: items.reduce((sum, item) => sum + item.price, 0),
status: "confirmed",
createdAt: new Date()
};
orders.push(order);
logger.info(`Order #${order.orderId} placed for $${order.total}`);
subject.notify("order_placed", order);
return order;
},
getOrders() {
return [...orders];
}
};
})();
// Usage
OrderModule.init();
const laptop = ProductFactory.create("physical", { name: "Laptop", price: 999.99, weight: "2kg" });
const ebook = ProductFactory.create("digital", { name: "E-Book", price: 19.99, fileSize: "5MB" });
OrderModule.placeOrder([laptop, ebook]);
4. Exercises
Beginner
- Implement a simple Module pattern for a
MathUtilsmodule with private helpersquare(n)and publicsumSquares(a, b). - Create a basic Observer pattern: a
Buttonclass that notifies listeners when clicked. - Implement a Singleton
Loggerclass that ensures only one instance logs messages.
Intermediate
- Build a
ShapeFactorythat createsCircle,Square, andTriangleobjects based on a type string. Each shape should have anarea()method. - Implement a Decorator pattern for a
TextProcessorbase class with decoratorsBoldandItalicthat wrap therender()method. - Create a Strategy pattern for sorting: implement
bubbleSort,quickSort, andmergeSortas strategies, and aSorterclass that can switch between them.
Advanced
- Combine Module, Observer, and Factory patterns to build a simple Chat Application: users can send messages (Factory creates message types), a ChatRoom (Module) manages state, and observers receive notifications.
- Build a Plugin System using the Decorator pattern: a base
Appclass and plugins that add functionality (logging, analytics, authentication) via theuse()method.
5. Mini Project: Notification Hub
Build a notification hub that uses multiple design patterns to manage and deliver notifications across different channels.
// === Notification Hub ===
// ---- Singleton: Config Manager ----
const Config = (function() {
let instance;
function createInstance() {
return {
channels: { email: true, sms: false, push: true },
rateLimit: 5, // per minute
set(key, value) { this[key] = value; },
get(key) { return this[key]; }
};
}
return { getInstance: () => instance || (instance = createInstance()) };
})();
// ---- Factory: Notification Creator ----
class NotificationFactory {
static create(type, data) {
switch (type) {
case "email":
return { type: "email", to: data.to, subject: data.subject, body: data.body, priority: data.priority || "normal" };
case "sms":
return { type: "sms", to: data.to, message: data.message, priority: data.priority || "normal" };
case "push":
return { type: "push", deviceId: data.deviceId, title: data.title, body: data.body, priority: data.priority || "normal" };
default:
throw new Error(`Unknown notification type: ${type}`);
}
}
}
// ---- Observer: Notification Channels ----
class EmailChannel {
update(notification) {
if (Config.getInstance().get("channels").email) {
console.log(`📧 Email to ${notification.to}: ${notification.subject}`);
console.log(` Body: ${notification.body}`);
}
}
}
class SMSChannel {
update(notification) {
if (Config.getInstance().get("channels").sms) {
console.log(`📱 SMS to ${notification.to}: ${notification.message}`);
}
}
}
class PushChannel {
update(notification) {
if (Config.getInstance().get("channels").push) {
console.log(`🔔 Push to ${notification.deviceId}: ${notification.title}`);
console.log(` Body: ${notification.body}`);
}
}
}
// ---- Strategy: Priority Routing ----
const priorityRouting = {
high(notification) {
// Send via all available channels
return ["email", "sms", "push"];
},
normal(notification) {
// Send via default channels
return ["email", "push"];
},
low(notification) {
// Send via email only
return ["email"];
}
};
// ---- Subject: Notification Hub (Module) ----
const NotificationHub = (function() {
const channels = {
email: new EmailChannel(),
sms: new SMSChannel(),
push: new PushChannel()
};
const history = [];
const MAX_HISTORY = 50;
let rateCount = 0;
let rateResetTimer = null;
function startRateTimer() {
if (!rateResetTimer) {
rateResetTimer = setTimeout(() => {
rateCount = 0;
rateResetTimer = null;
}, 60000);
}
}
return {
send(type, data) {
const config = Config.getInstance();
const notification = NotificationFactory.create(type, data);
// Rate limiting
if (rateCount >= config.get("rateLimit")) {
console.warn("⚠️ Rate limit exceeded. Notification queued.");
return { success: false, error: "Rate limit exceeded" };
}
// Determine channels
const route = priorityRouting[notification.priority] || priorityRouting.normal;
const targetChannels = route(notification);
// Send via each channel
targetChannels.forEach(channelName => {
const channel = channels[channelName];
if (channel) {
channel.update(notification);
}
});
// Record history
history.unshift({ ...notification, sentAt: new Date(), channels: targetChannels });
if (history.length > MAX_HISTORY) history.pop();
rateCount++;
startRateTimer();
console.log(`✅ Notification sent via ${targetChannels.join(", ")}`);
return { success: true, notificationId: Date.now() };
},
getHistory() {
return [...history];
},
updateConfig(key, value) {
Config.getInstance().set(key, value);
console.log(`⚙️ Config updated: ${key} = ${value}`);
}
};
})();
// ---- Test the Notification Hub ----
console.log("=== Notification Hub Demo ===\n");
// Send different types
NotificationHub.send("email", {
to: "alice@example.com",
subject: "Welcome!",
body: "Thanks for joining.",
priority: "high"
});
console.log("---");
NotificationHub.send("sms", {
to: "+1234567890",
message: "Your code is 123456",
priority: "high"
});
console.log("---");
NotificationHub.send("push", {
deviceId: "device-abc-123",
title: "New Message",
body: "You have a new message from Bob.",
priority: "normal"
});
console.log("\n--- History ---");
NotificationHub.getHistory().forEach(n => {
console.log(`[${n.type}] ${n.subject || n.message || n.title} via ${n.channels.join(", ")}`);
});
console.log("\n--- Disable SMS and send again ---");
NotificationHub.updateConfig("channels", { email: true, sms: false, push: true });
NotificationHub.send("email", { to: "bob@example.com", subject: "Hello", body: "Test" });
Try extending it: Add more channels (Slack, Telegram), add a retry mechanism for failed sends, implement a priority queue, or add scheduled/delayed notifications.
6. Common Mistakes
| Mistake | Why it's wrong | Correct approach |
|---|---|---|
| Overusing Singleton | Creates global state, makes testing difficult | Use dependency injection or module patterns sparingly |
| Tight coupling in Observer | Observers know too much about the subject | Pass minimal data; let observers decide what to do |
| Factory becoming too complex | Violates Open/Closed principle when adding new types | Use registry pattern or dynamic dispatch |
| Decorator losing the original interface | Decorated object behaves differently than expected | Keep the same interface; delegate to wrapped object |
| Strategy pattern with too many strategies | Over-engineering for simple conditional logic | Use simple functions or objects for small variations |
| Module pattern with mutable exposed state | External code can modify internal state | Return copies of arrays/objects; freeze if needed |
| Creating a new instance of Singleton | new Singleton() creates second instance | Return the existing instance from constructor |
| Forgetting to unsubscribe observers | Memory leaks from orphaned observers | Return unsubscribe function; clean up in lifecycle hooks |
7. Best Practices
- Use the Module pattern for organizing related functionality with private state.
- Use the Observer pattern for event-driven communication between decoupled components.
- Use the Singleton pattern sparingly — prefer dependency injection for testability.
- Use the Factory pattern when object creation logic is complex or varies by type.
- Use the Decorator pattern for adding behavior dynamically without modifying existing code.
- Use the Strategy pattern when you need to swap algorithms at runtime.
- Combine patterns when appropriate — they are not mutually exclusive.
- Keep patterns simple — don't force a pattern where a simple function would suffice.
- Document your patterns — make it clear which pattern you're using and why.
- Prefer composition over inheritance — many patterns (Decorator, Strategy) use composition.
8. Challenge Assignment (Optional)
"Plugin Architecture with Multiple Patterns" Challenge
Build a plugin-based application framework that combines Module, Observer, Factory, Decorator, and Strategy patterns.
Requirements:
Core Application (Module pattern):
- Manages plugins, events, and configuration
- Provides
use(plugin),start(),stop()methods
Plugin System (Decorator pattern):
- Each plugin can wrap or extend app functionality
- Plugins have lifecycle hooks:
onInit(app),onStart(app),onStop(app)
Event System (Observer pattern):
- Built-in event emitter for app-wide events
- Plugins can subscribe to events
Service Factory (Factory pattern):
- Creates services (Database, Cache, Logger) by type
- Services are injected into plugins
Task Runner (Strategy pattern):
- Supports different execution strategies:
immediate,debounced,throttled,queued
- Supports different execution strategies:
Example usage:
const app = new App();
// Register plugins
app.use(new LoggerPlugin());
app.use(new AnalyticsPlugin({ trackingId: "UA-123" }));
app.use(new CachePlugin());
// Start the app
app.start();
// Use services
const db = app.getService("database");
db.query("SELECT * FROM users");
// Emit events that plugins can react to
app.emit("user:login", { userId: 1 });
// Run tasks with different strategies
app.run("sync-data", () => fetchData(), { strategy: "debounced", delay: 500 });
app.run("send-email", () => sendEmail(), { strategy: "queued" });
9. Knowledge Check (Quiz)
Which pattern ensures a class has only one instance?
- a) Factory
- b) Singleton
- c) Observer
- d) Decorator
Which pattern defines a one-to-many dependency between objects?
- a) Module
- b) Strategy
- c) Observer
- d) Prototype
Which pattern provides an interface for creating objects without specifying their concrete classes?
- a) Factory
- b) Singleton
- c) Decorator
- d) Module
What is the main advantage of the Module pattern?
- a) It creates only one instance
- b) It encapsulates private state and exposes a public API
- c) It allows swapping algorithms at runtime
- d) It clones objects
Which pattern adds behavior to an object dynamically without modifying its class?
- a) Strategy
- b) Factory
- c) Decorator
- d) Observer
Which pattern allows you to swap algorithms at runtime?
- a) Singleton
- b) Strategy
- c) Prototype
- d) Module
What is a common problem with the Observer pattern?
- a) It creates too many objects
- b) Memory leaks from forgotten unsubscriptions
- c) It's hard to implement
- d) It only works with classes
When should you avoid the Singleton pattern?
- a) When you need global state
- b) When you need testable code with dependency injection
- c) When you need only one instance
- d) Never — Singleton is always good
Answers: 1-b, 2-c, 3-a, 4-b, 5-c, 6-b, 7-b, 8-b
10. Additional Resources
- MDN: Design Patterns → developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Details_of_the_Object_Model
- JavaScript.info: Design Patterns → javascript.info/patterns
- Refactoring Guru: Design Patterns → refactoring.guru/design-patterns (excellent visual explanations)
- Addy Osmani: Learning JavaScript Design Patterns → addyosmani.com/resources/essentialjsdesignpatterns/book/ (free online book)
- Video: "JavaScript Design Patterns" by Web Dev Simplified (YouTube playlist)
- Video: "Design Patterns in JavaScript" by Fun Fun Function (YouTube playlist)
- Practice: freeCodeCamp: Design Patterns
- Book: Learning JavaScript Design Patterns by Addy Osmani (O'Reilly)
- Book: Design Patterns: Elements of Reusable Object-Oriented Software by Gang of Four
11. Summary
Today you learned:
- Module pattern — encapsulation with private state and public API using IIFE or ES6 modules
- Observer pattern — one-to-many dependency with subject and observers, event emitters
- Singleton pattern — ensuring a single instance with shared state
- Factory pattern — creating objects without specifying concrete classes
- Prototype pattern — cloning objects using
Object.create()andstructuredClone() - Decorator pattern — adding behavior dynamically with wrapper functions and classes
- Strategy pattern — swapping algorithms at runtime
- Combining patterns — building a complete Notification Hub using Module, Singleton, Factory, Observer, and Strategy
- Common pitfalls — overusing Singleton, tight coupling, memory leaks from unsubscribed observers, over-engineering with patterns
You now have a toolkit of reusable design patterns to solve common problems in JavaScript. These patterns will help you write more maintainable, scalable, and professional code. Tomorrow we'll explore testing with Jest — ensuring your code works correctly.
"Design patterns are not silver bullets — they are proven solutions to common problems. Use them wisely, and they'll make your code more robust and maintainable."

Comments
Post a Comment