OOP Patterns and Best Practices
This lesson covers common object-oriented patterns and when to use them in JavaScript.
Composition Over Inheritance
While inheritance is powerful, composition is often more flexible:
javascript1// Inheritance approach (rigid)2class FlyingAnimal extends Animal {3 fly() { console.log("Flying!"); }4}56class SwimmingAnimal extends Animal {7 swim() { console.log("Swimming!"); }8}910// What about a duck that can do both? Multiple inheritance issues!1112// Composition approach (flexible)13const canFly = {14 fly() { console.log(`${this.name} is flying!`); }15};1617const canSwim = {18 swim() { console.log(`${this.name} is swimming!`); }19};2021const canWalk = {22 walk() { console.log(`${this.name} is walking!`); }23};2425class Duck {26 constructor(name) {27 this.name = name;28 }29}3031// Mix in behaviors32Object.assign(Duck.prototype, canFly, canSwim, canWalk);3334const donald = new Duck("Donald");35donald.fly(); // "Donald is flying!"36donald.swim(); // "Donald is swimming!"37donald.walk(); // "Donald is walking!"
Mixins Pattern
Reusable behavior packages:
javascript1// Mixin factory2const TimestampMixin = (Base) => class extends Base {3 get createdAt() {4 return this._createdAt || (this._createdAt = new Date());5 }67 get updatedAt() {8 return this._updatedAt;9 }1011 touch() {12 this._updatedAt = new Date();13 }14};1516const SerializableMixin = (Base) => class extends Base {17 toJSON() {18 return JSON.stringify(this);19 }2021 static fromJSON(json) {22 return Object.assign(new this(), JSON.parse(json));23 }24};2526// Apply mixins27class User extends TimestampMixin(SerializableMixin(Object)) {28 constructor(name) {29 super();30 this.name = name;31 }32}3334const user = new User("Alice");35user.touch();36console.log(user.createdAt);37console.log(user.toJSON());
Singleton Pattern
Ensure only one instance exists:
javascript1class Database {2 static #instance = null;34 constructor() {5 if (Database.#instance) {6 return Database.#instance;7 }8 Database.#instance = this;9 this.connection = null;10 }1112 connect(url) {13 this.connection = `Connected to ${url}`;14 return this.connection;15 }1617 static getInstance() {18 if (!Database.#instance) {19 Database.#instance = new Database();20 }21 return Database.#instance;22 }23}2425// All references point to same instance26const db1 = new Database();27const db2 = new Database();28const db3 = Database.getInstance();2930console.log(db1 === db2); // true31console.log(db2 === db3); // true
Builder Pattern
Construct complex objects step by step:
javascript1class QueryBuilder {2 #table = "";3 #columns = ["*"];4 #conditions = [];5 #orderBy = null;6 #limit = null;78 from(table) {9 this.#table = table;10 return this;11 }1213 select(...columns) {14 this.#columns = columns;15 return this;16 }1718 where(condition) {19 this.#conditions.push(condition);20 return this;21 }2223 orderBy(column, direction = "ASC") {24 this.#orderBy = `${column} ${direction}`;25 return this;26 }2728 limit(n) {29 this.#limit = n;30 return this;31 }3233 build() {34 let query = `SELECT ${this.#columns.join(", ")} FROM ${this.#table}`;3536 if (this.#conditions.length) {37 query += ` WHERE ${this.#conditions.join(" AND ")}`;38 }39 if (this.#orderBy) {40 query += ` ORDER BY ${this.#orderBy}`;41 }42 if (this.#limit) {43 query += ` LIMIT ${this.#limit}`;44 }4546 return query;47 }48}4950// Usage51const query = new QueryBuilder()52 .from("users")53 .select("name", "email")54 .where("age > 18")55 .where("active = true")56 .orderBy("created_at", "DESC")57 .limit(10)58 .build();5960// "SELECT name, email FROM users WHERE age > 18 AND active = true ORDER BY created_at DESC LIMIT 10"
Observer Pattern
Subscribe to and react to events:
javascript1class EventEmitter {2 #events = new Map();34 on(event, callback) {5 if (!this.#events.has(event)) {6 this.#events.set(event, []);7 }8 this.#events.get(event).push(callback);9 return this;10 }1112 off(event, callback) {13 const callbacks = this.#events.get(event);14 if (callbacks) {15 const index = callbacks.indexOf(callback);16 if (index > -1) callbacks.splice(index, 1);17 }18 return this;19 }2021 emit(event, ...args) {22 const callbacks = this.#events.get(event);23 if (callbacks) {24 callbacks.forEach(cb => cb(...args));25 }26 return this;27 }2829 once(event, callback) {30 const wrapper = (...args) => {31 callback(...args);32 this.off(event, wrapper);33 };34 return this.on(event, wrapper);35 }36}3738// Usage39class User extends EventEmitter {40 constructor(name) {41 super();42 this.name = name;43 }4445 login() {46 this.emit("login", this);47 }48}4950const user = new User("Alice");51user.on("login", (u) => console.log(`${u.name} logged in`));52user.login(); // "Alice logged in"
Proxy for Validation
Use Proxy for transparent validation:
javascript1function createValidatedObject(target, validators) {2 return new Proxy(target, {3 set(obj, prop, value) {4 if (validators[prop]) {5 const isValid = validators[prop](value);6 if (!isValid) {7 throw new Error(`Invalid value for ${prop}: ${value}`);8 }9 }10 obj[prop] = value;11 return true;12 }13 });14}1516const userValidators = {17 age: (v) => typeof v === "number" && v >= 0 && v <= 150,18 email: (v) => typeof v === "string" && v.includes("@")19};2021const user = createValidatedObject({}, userValidators);2223user.name = "Alice"; // OK (no validator)24user.age = 25; // OK25user.email = "a@b.com"; // OK2627user.age = -5; // Error! Invalid value28user.email = "invalid"; // Error! Invalid value
State Pattern
Manage object behavior based on state:
javascript1class TrafficLight {2 #states = {3 red: {4 color: "red",5 next: "green",6 duration: 50007 },8 yellow: {9 color: "yellow",10 next: "red",11 duration: 200012 },13 green: {14 color: "green",15 next: "yellow",16 duration: 500017 }18 };1920 #currentState = "red";2122 get color() {23 return this.#states[this.#currentState].color;24 }2526 get duration() {27 return this.#states[this.#currentState].duration;28 }2930 change() {31 this.#currentState = this.#states[this.#currentState].next;32 return this.color;33 }3435 start() {36 console.log(`Light is ${this.color}`);37 setTimeout(() => {38 this.change();39 this.start();40 }, this.duration);41 }42}
Encapsulation Best Practices
1. Minimize Public API
javascript1class User {2 #data;3 #validators;45 constructor(data) {6 this.#data = data;7 this.#validators = { /* ... */ };8 }910 // Only expose what's needed11 get name() { return this.#data.name; }12 get email() { return this.#data.email; }1314 updateProfile(updates) {15 // Validate and update internally16 this.#validate(updates);17 this.#data = { ...this.#data, ...updates };18 }1920 #validate(data) {21 // Private method for validation22 }23}
2. Defensive Copying
javascript1class Config {2 #settings;34 constructor(settings) {5 // Copy incoming data6 this.#settings = { ...settings };7 }89 getSettings() {10 // Return a copy, not the original11 return { ...this.#settings };12 }1314 getList() {15 // Deep copy for arrays16 return [...this.#list];17 }18}
3. Immutable Updates
javascript1class TodoList {2 #todos = [];34 addTodo(todo) {5 // Create new array instead of mutating6 this.#todos = [...this.#todos, { ...todo, id: Date.now() }];7 return this;8 }910 removeTodo(id) {11 this.#todos = this.#todos.filter(t => t.id !== id);12 return this;13 }1415 updateTodo(id, updates) {16 this.#todos = this.#todos.map(t =>17 t.id === id ? { ...t, ...updates } : t18 );19 return this;20 }21}
SOLID Principles in JavaScript
Single Responsibility
javascript1// BAD: Does too much2class UserManager {3 createUser() { /* ... */ }4 validateEmail() { /* ... */ }5 sendWelcomeEmail() { /* ... */ }6 saveToDatabase() { /* ... */ }7 generateReport() { /* ... */ }8}910// GOOD: Separate concerns11class UserService {12 createUser(data) { /* ... */ }13}1415class EmailValidator {16 validate(email) { /* ... */ }17}1819class EmailService {20 sendWelcome(user) { /* ... */ }21}
Open/Closed Principle
javascript1// Open for extension, closed for modification2class PaymentProcessor {3 #handlers = new Map();45 registerHandler(type, handler) {6 this.#handlers.set(type, handler);7 }89 process(payment) {10 const handler = this.#handlers.get(payment.type);11 if (!handler) throw new Error(`Unknown payment type: ${payment.type}`);12 return handler.process(payment);13 }14}1516// Add new payment types without modifying PaymentProcessor17processor.registerHandler("creditCard", new CreditCardHandler());18processor.registerHandler("paypal", new PayPalHandler());19processor.registerHandler("crypto", new CryptoHandler());
Dependency Injection
javascript1// BAD: Hard dependency2class UserService {3 constructor() {4 this.db = new PostgresDatabase(); // Tightly coupled5 }6}78// GOOD: Inject dependencies9class UserService {10 constructor(database) {11 this.db = database;12 }1314 getUser(id) {15 return this.db.find("users", id);16 }17}1819// Easy to test and swap implementations20const service = new UserService(new PostgresDatabase());21const testService = new UserService(new MockDatabase());
Summary
| Pattern | Use When |
|---|---|
| Composition | Need flexible behavior combinations |
| Singleton | Need exactly one instance |
| Builder | Complex object construction |
| Observer | Event-driven communication |
| Proxy | Transparent validation/logging |
| State | Behavior changes based on state |
Remember:
- Prefer composition over inheritance
- Keep classes focused (single responsibility)
- Use private fields for encapsulation
- Return defensive copies of internal data
- Inject dependencies for testability