README
7.6 Object Patterns
Overview
This section covers common design patterns and best practices for working with objects in JavaScript. These patterns solve recurring problems and provide reusable templates for structuring your code.
Table of Contents
- β’Factory Pattern
- β’Constructor Pattern
- β’Module Pattern
- β’Revealing Module Pattern
- β’Singleton Pattern
- β’Mixin Pattern
- β’Namespace Pattern
- β’Builder Pattern
- β’Prototype Pattern
- β’Observer Pattern
Factory Pattern
Creates objects without specifying the exact class. Encapsulates object creation logic.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Factory Pattern β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β createUser("admin") createUser("guest") β
β β β β
β βΌ βΌ β
β βββββββββββββββ βββββββββββββββ β
β β AdminUser β β GuestUser β β
β β { β β { β β
β β role, β β role, β β
β β perms, β β perms, β β
β β ... β β ... β β
β β } β β } β β
β βββββββββββββββ βββββββββββββββ β
β β
β β’ Encapsulates creation logic β
β β’ Returns different types based on input β
β β’ Client doesn't need to know concrete class β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Basic Factory
function createUser(name, role) {
return {
name,
role,
createdAt: new Date(),
greet() {
return `Hi, I'm ${this.name}`;
},
};
}
const user1 = createUser('Alice', 'admin');
const user2 = createUser('Bob', 'user');
Factory with Type Selection
function createVehicle(type, options) {
const vehicles = {
car: () => ({
type: 'car',
wheels: 4,
doors: options.doors || 4,
drive() {
console.log('Driving on road');
},
}),
motorcycle: () => ({
type: 'motorcycle',
wheels: 2,
drive() {
console.log('Riding on road');
},
}),
boat: () => ({
type: 'boat',
wheels: 0,
drive() {
console.log('Sailing on water');
},
}),
};
if (!vehicles[type]) {
throw new Error(`Unknown vehicle type: ${type}`);
}
return vehicles[type]();
}
Factory vs Constructor
| Aspect | Factory | Constructor |
|---|---|---|
| Syntax | createThing() | new Thing() |
this binding | Explicit return | Implicit |
instanceof | No | Yes |
| Inheritance | Manual | Prototype chain |
| Flexibility | High | Medium |
| Memory | New object each time | Shared prototype |
Constructor Pattern
Uses new keyword with constructor functions to create instances.
function Person(name, age) {
// Instance properties
this.name = name;
this.age = age;
}
// Shared methods on prototype
Person.prototype.greet = function () {
return `Hello, I'm ${this.name}`;
};
Person.prototype.birthday = function () {
this.age++;
return this;
};
const person = new Person('Alice', 30);
Constructor with Private State (Closure)
function BankAccount(initialBalance) {
// Private variable (closure)
let balance = initialBalance;
// Public methods (privileged)
this.deposit = function (amount) {
if (amount > 0) balance += amount;
return this;
};
this.withdraw = function (amount) {
if (amount <= balance) balance -= amount;
return this;
};
this.getBalance = function () {
return balance;
};
}
const account = new BankAccount(100);
account.deposit(50).withdraw(25);
console.log(account.getBalance()); // 125
console.log(account.balance); // undefined (private!)
Module Pattern
Encapsulates private state and exposes a public API using closures.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Module Pattern β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β const module = (function() { β
β β
β ββββ Private Scope ββββ β
β β let privateVar β β
β β function helper() β β
β βββββββββββββββββββββββ β
β β β
β βΌ β
β ββββ Public API ββββ β
β β return { β β
β β method1, β βββ Only these are accessible β
β β method2 β β
β β }; β β
β ββββββββββββββββββββ β
β })(); β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Classic IIFE Module
const Calculator = (function () {
// Private state
let result = 0;
// Private function
function validate(n) {
if (typeof n !== 'number') {
throw new Error('Expected a number');
}
}
// Public API
return {
add(n) {
validate(n);
result += n;
return this;
},
subtract(n) {
validate(n);
result -= n;
return this;
},
multiply(n) {
validate(n);
result *= n;
return this;
},
getResult() {
return result;
},
reset() {
result = 0;
return this;
},
};
})();
Calculator.add(5).multiply(2).subtract(3);
console.log(Calculator.getResult()); // 7
Revealing Module Pattern
A variation where all functions are defined privately, then "revealed" in the return statement.
const UserManager = (function () {
// Private state
const users = [];
// Private functions
function findById(id) {
return users.find((u) => u.id === id);
}
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
function addUser(name, email) {
const user = {
id: generateId(),
name,
email,
createdAt: new Date(),
};
users.push(user);
return user;
}
function removeUser(id) {
const index = users.findIndex((u) => u.id === id);
if (index > -1) {
return users.splice(index, 1)[0];
}
return null;
}
function getUser(id) {
return findById(id);
}
function getAllUsers() {
return [...users]; // Return copy
}
// Reveal public API
return {
add: addUser,
remove: removeUser,
get: getUser,
getAll: getAllUsers,
};
})();
Module Pattern Comparison
| Pattern | Description | Use Case |
|---|---|---|
| Classic Module | Mixed public/private in return | Quick encapsulation |
| Revealing Module | All private, reveal in return | Cleaner, maintainable |
| ES6 Modules | import/export | Modern projects |
Singleton Pattern
Ensures only one instance of a class/object exists.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Singleton Pattern β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β getInstance() ββββββ β
β β β
β getInstance() ββββββΌββββΊ Same Instance β
β β ββββββββββββββ β
β getInstance() ββββββ β Singleton β β
β β { β β
β β data, β β
β β methods β β
β β } β β
β ββββββββββββββ β
β β
β Multiple calls β Always returns the SAME object β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Object Literal Singleton
const AppConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000,
debug: false,
set(key, value) {
if (this.hasOwnProperty(key)) {
this[key] = value;
}
},
get(key) {
return this[key];
},
};
Object.freeze(AppConfig); // Prevent modifications
Lazy Singleton
const Database = (function () {
let instance = null;
function createInstance() {
// Expensive initialization
const connection = {
host: 'localhost',
connected: false,
connect() {
this.connected = true;
console.log('Connected to database');
},
query(sql) {
if (!this.connected) throw new Error('Not connected');
console.log(`Executing: ${sql}`);
},
};
return connection;
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
},
};
})();
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true (same instance)
ES6 Class Singleton
class Logger {
static instance = null;
constructor() {
if (Logger.instance) {
return Logger.instance;
}
this.logs = [];
Logger.instance = this;
}
log(message) {
const entry = { timestamp: new Date(), message };
this.logs.push(entry);
console.log(`[${entry.timestamp.toISOString()}] ${message}`);
}
getLogs() {
return [...this.logs];
}
}
const logger1 = new Logger();
const logger2 = new Logger();
console.log(logger1 === logger2); // true
Mixin Pattern
Adds functionality to objects/classes from multiple sources.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Mixin Pattern β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β Walkable β β Swimmableβ β Flyable β β
β β {walk()} β β {swim()} β β {fly()} β β
β ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ β
β β β β β
β βββββββββββββββΌββββββββββββββ β
β βΌ β
β ββββββββββββββββ β
β β Duck β β
β β { β β
β β walk(), β β
β β swim(), β β
β β fly() β β
β β } β β
β ββββββββββββββββ β
β β
β Compose behavior from multiple sources β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Object Mixins
const Walkable = {
walk() {
console.log(`${this.name} is walking`);
},
};
const Swimmable = {
swim() {
console.log(`${this.name} is swimming`);
},
};
const Flyable = {
fly() {
console.log(`${this.name} is flying`);
},
};
// Duck can walk, swim, and fly
function Duck(name) {
this.name = name;
}
Object.assign(Duck.prototype, Walkable, Swimmable, Flyable);
const duck = new Duck('Donald');
duck.walk(); // "Donald is walking"
duck.swim(); // "Donald is swimming"
duck.fly(); // "Donald is flying"
Functional Mixins
const withLogging = (Base) => {
return {
...Base,
log(message) {
console.log(`[${this.name}]: ${message}`);
},
};
};
const withTimestamp = (Base) => {
return {
...Base,
timestamp() {
return new Date().toISOString();
},
};
};
const withValidation = (Base) => {
return {
...Base,
validate(data) {
return Object.keys(data).every((key) => data[key] !== undefined);
},
};
};
// Compose mixins
let Service = { name: 'UserService' };
Service = withLogging(Service);
Service = withTimestamp(Service);
Service = withValidation(Service);
// Or with compose function
const compose =
(...mixins) =>
(base) =>
mixins.reduce((acc, mixin) => mixin(acc), base);
const enhancedService = compose(
withLogging,
withTimestamp,
withValidation
)({ name: 'OrderService' });
Namespace Pattern
Organizes code under a single global object to avoid pollution.
// Create namespace
const MyApp = MyApp || {};
// Add sub-namespaces
MyApp.Models = {};
MyApp.Views = {};
MyApp.Utils = {};
// Define components
MyApp.Models.User = function (name) {
this.name = name;
};
MyApp.Utils.format = {
date(d) {
return d.toLocaleDateString();
},
currency(n) {
return `$${n.toFixed(2)}`;
},
};
// Usage
const user = new MyApp.Models.User('Alice');
console.log(MyApp.Utils.format.currency(99.5)); // "$99.50"
Deep Namespace Creation
const namespace = (name, root = window) => {
const parts = name.split('.');
let current = root;
for (const part of parts) {
if (!current[part]) {
current[part] = {};
}
current = current[part];
}
return current;
};
// Usage
const globalRoot = {};
namespace('MyApp.Services.API', globalRoot);
namespace('MyApp.Services.Auth', globalRoot);
globalRoot.MyApp.Services.API.fetch = function () {
/* ... */
};
Builder Pattern
Separates construction of complex objects from their representation.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Builder Pattern β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β QueryBuilder β
β .select("name", "email") β
β .from("users") β
β .where("active", true) β
β .orderBy("name") β
β .limit(10) β
β .build() β
β β β
β βΌ β
β "SELECT name, email FROM users WHERE active = true β
β ORDER BY name LIMIT 10" β
β β
β β’ Chain method calls β
β β’ Configure step by step β
β β’ Build final result β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Fluent Builder
class QueryBuilder {
constructor() {
this.query = {
select: [],
from: '',
where: [],
orderBy: null,
limit: null,
};
}
select(...fields) {
this.query.select = fields;
return this;
}
from(table) {
this.query.from = table;
return this;
}
where(field, value) {
this.query.where.push({ field, value });
return this;
}
orderBy(field, direction = 'ASC') {
this.query.orderBy = { field, direction };
return this;
}
limit(n) {
this.query.limit = n;
return this;
}
build() {
let sql = `SELECT ${this.query.select.join(', ')}`;
sql += ` FROM ${this.query.from}`;
if (this.query.where.length) {
const conditions = this.query.where
.map((w) => `${w.field} = '${w.value}'`)
.join(' AND ');
sql += ` WHERE ${conditions}`;
}
if (this.query.orderBy) {
sql += ` ORDER BY ${this.query.orderBy.field} ${this.query.orderBy.direction}`;
}
if (this.query.limit) {
sql += ` LIMIT ${this.query.limit}`;
}
return sql;
}
}
const query = new QueryBuilder()
.select('name', 'email')
.from('users')
.where('status', 'active')
.orderBy('name')
.limit(10)
.build();
Object Configuration Builder
class RequestBuilder {
constructor(url) {
this.config = {
url,
method: 'GET',
headers: {},
body: null,
timeout: 5000,
};
}
method(m) {
this.config.method = m;
return this;
}
header(key, value) {
this.config.headers[key] = value;
return this;
}
body(data) {
this.config.body = JSON.stringify(data);
this.config.headers['Content-Type'] = 'application/json';
return this;
}
timeout(ms) {
this.config.timeout = ms;
return this;
}
async execute() {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), this.config.timeout);
try {
const response = await fetch(this.config.url, {
method: this.config.method,
headers: this.config.headers,
body: this.config.body,
signal: controller.signal,
});
return response.json();
} finally {
clearTimeout(id);
}
}
}
// Usage
const response = await new RequestBuilder('/api/users')
.method('POST')
.header('Authorization', 'Bearer token')
.body({ name: 'Alice' })
.timeout(10000)
.execute();
Observer Pattern
Defines a one-to-many dependency where observers are notified of state changes.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Observer Pattern β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββ β
β β Subject β β
β β (EventBus) β β
β βββββββ¬βββββββ β
β β β
β emit("event", data) β
β β β
β βββββββββββββββββββΌββββββββββββββββββ β
β βΌ βΌ βΌ β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β βObserver 1β βObserver 2β βObserver 3β β
β βcallback()β βcallback()β βcallback()β β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β
β Subject notifies all subscribed observers β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Event Emitter Implementation
class EventEmitter {
constructor() {
this.events = new Map();
}
on(event, callback) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(callback);
return this;
}
off(event, callback) {
if (this.events.has(event)) {
const callbacks = this.events.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
return this;
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
emit(event, ...args) {
if (this.events.has(event)) {
for (const callback of this.events.get(event)) {
callback(...args);
}
}
return this;
}
}
// Usage
const emitter = new EventEmitter();
emitter.on('message', (data) => {
console.log('Received:', data);
});
emitter.once('connect', () => {
console.log('Connected!');
});
emitter.emit('connect');
emitter.emit('message', { text: 'Hello' });
Observable Object
function createObservable(target) {
const listeners = new Map();
return new Proxy(target, {
get(obj, prop) {
if (prop === 'subscribe') {
return (property, callback) => {
if (!listeners.has(property)) {
listeners.set(property, []);
}
listeners.get(property).push(callback);
return () => {
const callbacks = listeners.get(property);
const index = callbacks.indexOf(callback);
if (index > -1) callbacks.splice(index, 1);
};
};
}
return obj[prop];
},
set(obj, prop, value) {
const oldValue = obj[prop];
obj[prop] = value;
if (listeners.has(prop)) {
for (const callback of listeners.get(prop)) {
callback(value, oldValue, prop);
}
}
return true;
},
});
}
// Usage
const state = createObservable({ count: 0, name: 'App' });
const unsubscribe = state.subscribe('count', (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`);
});
state.count = 1; // "Count changed from 0 to 1"
state.count = 2; // "Count changed from 1 to 2"
unsubscribe(); // Stop listening
state.count = 3; // No output
Pattern Selection Guide
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β When to Use Each Pattern β
ββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Pattern β Use When β
ββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Factory β Creating objects based on conditions/config β
β Constructor β Multiple instances with shared methods β
β Module β Encapsulating private state, creating APIs β
β Singleton β Need exactly one instance globally β
β Mixin β Sharing behavior across unrelated objects β
β Namespace β Organizing code, avoiding global pollution β
β Builder β Complex object construction with many options β
β Observer β Decoupled communication between components β
ββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Best Practices
1. Prefer Composition Over Inheritance
// Instead of deep inheritance chains
// Use mixins and composition
const canFly = (base) => ({
...base,
fly() {
console.log(`${this.name} flies`);
},
});
const canSwim = (base) => ({
...base,
swim() {
console.log(`${this.name} swims`);
},
});
const duck = canSwim(canFly({ name: 'Duck' }));
2. Keep Modules Focused
// Good: Single responsibility
const UserAuthentication = {
login() {
/* ... */
},
logout() {
/* ... */
},
validateToken() {
/* ... */
},
};
// Bad: Mixed concerns
const UserEverything = {
login() {
/* ... */
},
formatDate() {
/* ... */
},
sendEmail() {
/* ... */
},
};
3. Use Immutable Patterns When Possible
// Return new objects instead of mutating
const updateUser = (user, updates) => ({
...user,
...updates,
updatedAt: new Date(),
});
const user1 = { name: 'Alice', age: 30 };
const user2 = updateUser(user1, { age: 31 });
// user1 unchanged, user2 is new object
Summary
| Pattern | Key Concept | Memory Aid |
|---|---|---|
| Factory | Creates objects | "Factory produces products" |
| Constructor | new keyword + prototype | "Blueprint for objects" |
| Module | IIFE + closure | "Private room with public door" |
| Singleton | One instance only | "There can be only one" |
| Mixin | Combine behaviors | "Mix ingredients together" |
| Namespace | Organize under one object | "File folders for code" |
| Builder | Step-by-step construction | "Build LEGO piece by piece" |
| Observer | Publish/subscribe | "Newsletter subscription" |
Next Steps
- β’Study ES6 classes (syntactic sugar over these patterns)
- β’Learn about async patterns (Promise, async/await)
- β’Explore state management patterns (Flux, Redux concepts)
- β’Practice identifying when to use each pattern