Module 28: Design Patterns in JavaScript
Design patterns are reusable solutions to commonly occurring problems in software design. They represent best practices refined over time.
1. Creational Patterns
1.1 Singleton Pattern
Ensure a class has only one instance.
// Basic Singleton
class Database {
constructor() {
if (Database.instance) {
return Database.instance;
}
this.connection = null;
Database.instance = this;
}
connect() {
if (!this.connection) {
this.connection = 'Connected to DB';
console.log(this.connection);
}
return this.connection;
}
}
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true (same instance)
// Module Singleton (using closures)
const ConfigManager = (function() {
let instance;
function init() {
// Private variables
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
// Public interface
return {
get(key) {
return config[key];
},
set(key, value) {
config[key] = value;
},
getAll() {
return { ...config };
}
};
}
return {
getInstance() {
if (!instance) {
instance = init();
}
return instance;
}
};
})();
// Usage
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();
console.log(config1 === config2); // true
// ES6 Module Singleton (simplest)
// config.js
class Config {
constructor() {
this.settings = {};
}
set(key, value) {
this.settings[key] = value;
}
get(key) {
return this.settings[key];
}
}
export default new Config(); // Export single instance
// Usage
// import config from './config.js';
// config.set('theme', 'dark');
Use Singleton for shared resources like database connections, configuration managers, logging systems, or caches.
1.2 Factory Pattern
Create objects without specifying their exact class.
// Simple Factory
class Car {
constructor(options) {
this.doors = options.doors || 4;
this.state = options.state || 'brand new';
this.color = options.color || 'silver';
}
}
class Truck {
constructor(options) {
this.wheelSize = options.wheelSize || 'large';
this.state = options.state || 'used';
this.color = options.color || 'blue';
}
}
class VehicleFactory {
createVehicle(type, options) {
switch(type) {
case 'car':
return new Car(options);
case 'truck':
return new Truck(options);
default:
throw new Error('Unknown vehicle type');
}
}
}
// Usage
const factory = new VehicleFactory();
const car = factory.createVehicle('car', { doors: 2, color: 'red' });
const truck = factory.createVehicle('truck', { wheelSize: 'medium' });
// Abstract Factory
class Button {
render() {
throw new Error('render() must be implemented');
}
}
class WindowsButton extends Button {
render() {
return '<button class="windows-btn">Click</button>';
}
}
class MacButton extends Button {
render() {
return '<button class="mac-btn">Click</button>';
}
}
class Checkbox {
render() {
throw new Error('render() must be implemented');
}
}
class WindowsCheckbox extends Checkbox {
render() {
return '<input type="checkbox" class="windows-cb">';
}
}
class MacCheckbox extends Checkbox {
render() {
return '<input type="checkbox" class="mac-cb">';
}
}
class GUIFactory {
createButton() {
throw new Error('createButton() must be implemented');
}
createCheckbox() {
throw new Error('createCheckbox() must be implemented');
}
}
class WindowsFactory extends GUIFactory {
createButton() {
return new WindowsButton();
}
createCheckbox() {
return new WindowsCheckbox();
}
}
class MacFactory extends GUIFactory {
createButton() {
return new MacButton();
}
createCheckbox() {
return new MacCheckbox();
}
}
// Usage
function createUI(factory) {
const button = factory.createButton();
const checkbox = factory.createCheckbox();
return {
button: button.render(),
checkbox: checkbox.render()
};
}
const os = 'Windows'; // or 'Mac'
const factory = os === 'Windows' ? new WindowsFactory() : new MacFactory();
const ui = createUI(factory);
1.3 Builder Pattern
Construct complex objects step by step.
// HTTP Request Builder
class HttpRequest {
constructor() {
this.url = '';
this.method = 'GET';
this.headers = {};
this.body = null;
this.timeout = 5000;
}
}
class HttpRequestBuilder {
constructor() {
this.request = new HttpRequest();
}
setUrl(url) {
this.request.url = url;
return this; // Return this for method chaining
}
setMethod(method) {
this.request.method = method;
return this;
}
addHeader(key, value) {
this.request.headers[key] = value;
return this;
}
setBody(body) {
this.request.body = body;
return this;
}
setTimeout(timeout) {
this.request.timeout = timeout;
return this;
}
build() {
return this.request;
}
}
// Usage
const request = new HttpRequestBuilder()
.setUrl('https://api.example.com/users')
.setMethod('POST')
.addHeader('Content-Type', 'application/json')
.addHeader('Authorization', 'Bearer token123')
.setBody({ name: 'John' })
.setTimeout(10000)
.build();
// SQL Query Builder
class QueryBuilder {
constructor() {
this.query = {
select: [],
from: '',
where: [],
orderBy: [],
limit: null
};
}
select(...fields) {
this.query.select.push(...fields);
return this;
}
from(table) {
this.query.from = table;
return this;
}
where(condition) {
this.query.where.push(condition);
return this;
}
orderBy(field, direction = 'ASC') {
this.query.orderBy.push(`${field} ${direction}`);
return this;
}
limit(count) {
this.query.limit = count;
return this;
}
build() {
const { select, from, where, orderBy, limit } = this.query;
let sql = `SELECT ${select.join(', ')} FROM ${from}`;
if (where.length > 0) {
sql += ` WHERE ${where.join(' AND ')}`;
}
if (orderBy.length > 0) {
sql += ` ORDER BY ${orderBy.join(', ')}`;
}
if (limit) {
sql += ` LIMIT ${limit}`;
}
return sql;
}
}
// Usage
const query = new QueryBuilder()
.select('id', 'name', 'email')
.from('users')
.where('age > 18')
.where('active = true')
.orderBy('name', 'ASC')
.limit(10)
.build();
console.log(query);
// SELECT id, name, email FROM users WHERE age > 18 AND active = true ORDER BY name ASC LIMIT 10
Builder pattern is excellent for objects with many optional parameters, making code more readable than constructor with many parameters.
2. Structural Patterns
2.1 Module Pattern
Encapsulate private and public members.
// Revealing Module Pattern
const Calculator = (function() {
// Private variables
let result = 0;
// Private methods
function validateNumber(num) {
if (typeof num !== 'number') {
throw new Error('Invalid number');
}
}
function logOperation(operation, value) {
console.log(`${operation}: ${value}, result: ${result}`);
}
// Public interface
return {
add(num) {
validateNumber(num);
result += num;
logOperation('add', num);
return this;
},
subtract(num) {
validateNumber(num);
result -= num;
logOperation('subtract', num);
return this;
},
multiply(num) {
validateNumber(num);
result *= num;
logOperation('multiply', num);
return this;
},
getResult() {
return result;
},
reset() {
result = 0;
return this;
}
};
})();
// Usage
Calculator.add(10).multiply(2).subtract(5);
console.log(Calculator.getResult()); // 15
// ES6 Module Pattern
class CalculatorES6 {
#result = 0; // Private field
#validateNumber(num) {
if (typeof num !== 'number') {
throw new Error('Invalid number');
}
}
add(num) {
this.#validateNumber(num);
this.#result += num;
return this;
}
getResult() {
return this.#result;
}
}
2.2 Decorator Pattern
Add new functionality to objects dynamically.
// Function decorator
function logger(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn.apply(this, args);
console.log(`Result:`, result);
return result;
};
}
function add(a, b) {
return a + b;
}
const loggedAdd = logger(add);
loggedAdd(2, 3);
// Calling add with [2, 3]
// Result: 5
// Class decorator pattern
class Coffee {
cost() {
return 5;
}
description() {
return 'Coffee';
}
}
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost();
}
description() {
return this.coffee.description();
}
}
class MilkDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1;
}
description() {
return this.coffee.description() + ', Milk';
}
}
class SugarDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 0.5;
}
description() {
return this.coffee.description() + ', Sugar';
}
}
// Usage
let coffee = new Coffee();
console.log(coffee.description(), '-', coffee.cost()); // Coffee - 5
coffee = new MilkDecorator(coffee);
console.log(coffee.description(), '-', coffee.cost()); // Coffee, Milk - 6
coffee = new SugarDecorator(coffee);
console.log(coffee.description(), '-', coffee.cost()); // Coffee, Milk, Sugar - 6.5
// Modern decorator syntax (experimental)
function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
function validate(target, key, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
if (args.some(arg => typeof arg !== 'number')) {
throw new Error('All arguments must be numbers');
}
return originalMethod.apply(this, args);
};
return descriptor;
}
class MathOperations {
@readonly
PI = 3.14159;
@validate
add(a, b) {
return a + b;
}
}
2.3 Proxy Pattern
Control access to an object.
// Virtual proxy (lazy loading)
class Image {
constructor(url) {
this.url = url;
console.log(`Loading image from ${url}`);
}
display() {
console.log(`Displaying image ${this.url}`);
}
}
class ImageProxy {
constructor(url) {
this.url = url;
this.image = null;
}
display() {
if (!this.image) {
this.image = new Image(this.url);
}
this.image.display();
}
}
// Usage
const proxy = new ImageProxy('photo.jpg');
// Image not loaded yet
proxy.display(); // Now image is loaded and displayed
// ES6 Proxy for validation
const validator = {
set(target, property, value) {
if (property === 'age') {
if (typeof value !== 'number' || value < 0 || value > 150) {
throw new Error('Invalid age');
}
}
target[property] = value;
return true;
}
};
const person = new Proxy({}, validator);
person.age = 30; // OK
// person.age = -1; // Error: Invalid age
// Access control proxy
function createProtectedObject(target, allowedProps) {
return new Proxy(target, {
get(target, prop) {
if (!allowedProps.includes(prop)) {
throw new Error(`Access to property ${prop} is denied`);
}
return target[prop];
},
set(target, prop, value) {
if (!allowedProps.includes(prop)) {
throw new Error(`Cannot modify property ${prop}`);
}
target[prop] = value;
return true;
}
});
}
const user = { name: 'John', password: 'secret123', email: 'john@example.com' };
const protectedUser = createProtectedObject(user, ['name', 'email']);
console.log(protectedUser.name); // 'John'
// console.log(protectedUser.password); // Error: Access denied
// Logging proxy
function createLoggingProxy(target, name) {
return new Proxy(target, {
get(target, prop) {
console.log(`[${name}] Getting ${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`[${name}] Setting ${prop} = ${value}`);
target[prop] = value;
return true;
}
});
}
const obj = { x: 1, y: 2 };
const logged = createLoggingProxy(obj, 'MyObject');
logged.x; // [MyObject] Getting x
logged.z = 3; // [MyObject] Setting z = 3
Proxies add overhead. Use them judiciously, especially in performance-critical code.
3. Behavioral Patterns
3.1 Observer Pattern (Pub/Sub)
Define one-to-many dependency between objects.
// Event Emitter (Observer Pattern)
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
return () => this.off(event, listener); // Return unsubscribe function
}
off(event, listener) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(l => l !== listener);
}
emit(event, ...args) {
if (!this.events[event]) return;
this.events[event].forEach(listener => {
listener(...args);
});
}
once(event, listener) {
const onceWrapper = (...args) => {
listener(...args);
this.off(event, onceWrapper);
};
this.on(event, onceWrapper);
}
}
// Usage
const emitter = new EventEmitter();
const unsubscribe = emitter.on('data', (data) => {
console.log('Received:', data);
});
emitter.emit('data', { message: 'Hello' }); // Received: { message: 'Hello' }
unsubscribe(); // Unsubscribe
emitter.emit('data', { message: 'World' }); // No output
// Real-world: Store pattern
class Store extends EventEmitter {
constructor(initialState = {}) {
super();
this.state = initialState;
}
getState() {
return { ...this.state };
}
setState(updates) {
const oldState = this.state;
this.state = { ...this.state, ...updates };
this.emit('change', this.state, oldState);
}
subscribe(listener) {
return this.on('change', listener);
}
}
// Usage
const store = new Store({ count: 0 });
store.subscribe((newState, oldState) => {
console.log('State changed:', oldState, '->', newState);
});
store.setState({ count: 1 });
// State changed: { count: 0 } -> { count: 1 }
3.2 Strategy Pattern
Define a family of algorithms and make them interchangeable.
// Validation strategies
class ValidationStrategy {
validate(value) {
throw new Error('validate() must be implemented');
}
}
class EmailValidation extends ValidationStrategy {
validate(value) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(value);
}
}
class PhoneValidation extends ValidationStrategy {
validate(value) {
const regex = /^\d{10}$/;
return regex.test(value);
}
}
class RequiredValidation extends ValidationStrategy {
validate(value) {
return value !== null && value !== undefined && value !== '';
}
}
class Validator {
constructor() {
this.strategies = [];
}
addStrategy(strategy) {
this.strategies.push(strategy);
return this;
}
validate(value) {
return this.strategies.every(strategy => strategy.validate(value));
}
}
// Usage
const emailValidator = new Validator()
.addStrategy(new RequiredValidation())
.addStrategy(new EmailValidation());
console.log(emailValidator.validate('test@example.com')); // true
console.log(emailValidator.validate('invalid')); // false
// Sorting strategies
class SortStrategy {
sort(array) {
throw new Error('sort() must be implemented');
}
}
class BubbleSort extends SortStrategy {
sort(array) {
const arr = [...array];
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
}
class QuickSort extends SortStrategy {
sort(array) {
if (array.length <= 1) return array;
const pivot = array[0];
const left = array.slice(1).filter(x => x <= pivot);
const right = array.slice(1).filter(x => x > pivot);
return [...this.sort(left), pivot, ...this.sort(right)];
}
}
class Sorter {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
sort(array) {
return this.strategy.sort(array);
}
}
// Usage
const data = [5, 2, 8, 1, 9];
const sorter = new Sorter(new BubbleSort());
console.log(sorter.sort(data)); // [1, 2, 5, 8, 9]
sorter.setStrategy(new QuickSort());
console.log(sorter.sort(data)); // [1, 2, 5, 8, 9]
3.3 Command Pattern
Encapsulate requests as objects.
// Command interface
class Command {
execute() {
throw new Error('execute() must be implemented');
}
undo() {
throw new Error('undo() must be implemented');
}
}
// Concrete commands
class AddCommand extends Command {
constructor(receiver, value) {
super();
this.receiver = receiver;
this.value = value;
}
execute() {
this.receiver.add(this.value);
}
undo() {
this.receiver.subtract(this.value);
}
}
class MultiplyCommand extends Command {
constructor(receiver, value) {
super();
this.receiver = receiver;
this.value = value;
this.previousValue = null;
}
execute() {
this.previousValue = this.receiver.getValue();
this.receiver.multiply(this.value);
}
undo() {
this.receiver.setValue(this.previousValue);
}
}
// Receiver
class Calculator {
constructor() {
this.value = 0;
}
add(n) {
this.value += n;
}
subtract(n) {
this.value -= n;
}
multiply(n) {
this.value *= n;
}
getValue() {
return this.value;
}
setValue(n) {
this.value = n;
}
}
// Invoker
class CommandManager {
constructor() {
this.history = [];
this.currentIndex = -1;
}
execute(command) {
// Remove any commands after current index
this.history = this.history.slice(0, this.currentIndex + 1);
command.execute();
this.history.push(command);
this.currentIndex++;
}
undo() {
if (this.currentIndex < 0) return;
const command = this.history[this.currentIndex];
command.undo();
this.currentIndex--;
}
redo() {
if (this.currentIndex >= this.history.length - 1) return;
this.currentIndex++;
const command = this.history[this.currentIndex];
command.execute();
}
}
// Usage
const calculator = new Calculator();
const manager = new CommandManager();
manager.execute(new AddCommand(calculator, 10));
console.log(calculator.getValue()); // 10
manager.execute(new MultiplyCommand(calculator, 2));
console.log(calculator.getValue()); // 20
manager.undo();
console.log(calculator.getValue()); // 10
manager.redo();
console.log(calculator.getValue()); // 20
// Text editor example
class InsertTextCommand extends Command {
constructor(editor, text, position) {
super();
this.editor = editor;
this.text = text;
this.position = position;
}
execute() {
this.editor.insertText(this.text, this.position);
}
undo() {
this.editor.deleteText(this.position, this.text.length);
}
}
class TextEditor {
constructor() {
this.content = '';
}
insertText(text, position) {
this.content =
this.content.slice(0, position) +
text +
this.content.slice(position);
}
deleteText(position, length) {
this.content =
this.content.slice(0, position) +
this.content.slice(position + length);
}
getContent() {
return this.content;
}
}
Command pattern is perfect for implementing undo/redo, queuing operations, logging, and transaction management.
3.4 State Pattern
Alter object's behavior when internal state changes.
// State interface
class State {
handle(context) {
throw new Error('handle() must be implemented');
}
}
// Concrete states
class DraftState extends State {
handle(context) {
console.log('Document is in draft. Publishing...');
context.setState(new PublishedState());
}
}
class PublishedState extends State {
handle(context) {
console.log('Document is published. Archiving...');
context.setState(new ArchivedState());
}
}
class ArchivedState extends State {
handle(context) {
console.log('Document is archived. Cannot perform action.');
}
}
// Context
class Document {
constructor() {
this.state = new DraftState();
}
setState(state) {
this.state = state;
}
publish() {
this.state.handle(this);
}
}
// Usage
const doc = new Document();
doc.publish(); // Document is in draft. Publishing...
doc.publish(); // Document is published. Archiving...
doc.publish(); // Document is archived. Cannot perform action.
// Traffic light example
class TrafficLight {
constructor() {
this.states = {
red: {
next: 'green',
duration: 5000,
color: 'red'
},
yellow: {
next: 'red',
duration: 2000,
color: 'yellow'
},
green: {
next: 'yellow',
duration: 5000,
color: 'green'
}
};
this.currentState = 'red';
}
change() {
const state = this.states[this.currentState];
console.log(`Light is ${state.color}`);
setTimeout(() => {
this.currentState = state.next;
this.change();
}, state.duration);
}
}
// const light = new TrafficLight();
// light.change();
// Connection state machine
class Connection {
constructor() {
this.state = 'disconnected';
this.transitions = {
disconnected: {
connect: 'connecting'
},
connecting: {
success: 'connected',
failure: 'disconnected'
},
connected: {
disconnect: 'disconnected',
error: 'error'
},
error: {
retry: 'connecting',
disconnect: 'disconnected'
}
};
}
transition(action) {
const nextState = this.transitions[this.state]?.[action];
if (!nextState) {
console.log(`Cannot ${action} from ${this.state} state`);
return false;
}
console.log(`${this.state} -> ${action} -> ${nextState}`);
this.state = nextState;
return true;
}
getState() {
return this.state;
}
}
// Usage
const conn = new Connection();
conn.transition('connect'); // disconnected -> connect -> connecting
conn.transition('success'); // connecting -> success -> connected
conn.transition('error'); // connected -> error -> error
conn.transition('retry'); // error -> retry -> connecting
4. Architectural Patterns
4.1 MVC (Model-View-Controller)
// Model
class TodoModel {
constructor() {
this.todos = [];
this.listeners = [];
}
addTodo(text) {
const todo = {
id: Date.now(),
text,
completed: false
};
this.todos.push(todo);
this.notifyListeners();
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.notifyListeners();
}
}
deleteTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
this.notifyListeners();
}
getTodos() {
return [...this.todos];
}
subscribe(listener) {
this.listeners.push(listener);
}
notifyListeners() {
this.listeners.forEach(listener => listener(this.todos));
}
}
// View
class TodoView {
constructor(container) {
this.container = container;
this.input = null;
this.list = null;
}
render() {
this.container.innerHTML = `
<div class="todo-app">
<input type="text" id="todoInput" placeholder="Add todo...">
<button id="addBtn">Add</button>
<ul id="todoList"></ul>
</div>
`;
this.input = this.container.querySelector('#todoInput');
this.list = this.container.querySelector('#todoList');
}
renderTodos(todos) {
this.list.innerHTML = todos.map(todo => `
<li data-id="${todo.id}" class="${todo.completed ? 'completed' : ''}">
<span>${todo.text}</span>
<button class="toggle">Toggle</button>
<button class="delete">Delete</button>
</li>
`).join('');
}
bindAddTodo(handler) {
this.container.querySelector('#addBtn').addEventListener('click', () => {
const text = this.input.value.trim();
if (text) {
handler(text);
this.input.value = '';
}
});
}
bindToggleTodo(handler) {
this.list.addEventListener('click', (e) => {
if (e.target.classList.contains('toggle')) {
const id = Number(e.target.closest('li').dataset.id);
handler(id);
}
});
}
bindDeleteTodo(handler) {
this.list.addEventListener('click', (e) => {
if (e.target.classList.contains('delete')) {
const id = Number(e.target.closest('li').dataset.id);
handler(id);
}
});
}
}
// Controller
class TodoController {
constructor(model, view) {
this.model = model;
this.view = view;
// Subscribe to model changes
this.model.subscribe(todos => {
this.view.renderTodos(todos);
});
// Bind view handlers
this.view.bindAddTodo(text => this.addTodo(text));
this.view.bindToggleTodo(id => this.toggleTodo(id));
this.view.bindDeleteTodo(id => this.deleteTodo(id));
// Initial render
this.view.render();
this.view.renderTodos(this.model.getTodos());
}
addTodo(text) {
this.model.addTodo(text);
}
toggleTodo(id) {
this.model.toggleTodo(id);
}
deleteTodo(id) {
this.model.deleteTodo(id);
}
}
// Usage
// const container = document.getElementById('app');
// const model = new TodoModel();
// const view = new TodoView(container);
// const controller = new TodoController(model, view);
4.2 MVVM (Model-View-ViewModel)
// Simplified MVVM with reactive data binding
class Observable {
constructor(value) {
this._value = value;
this._listeners = [];
}
get value() {
return this._value;
}
set value(newValue) {
if (this._value !== newValue) {
this._value = newValue;
this._listeners.forEach(listener => listener(newValue));
}
}
subscribe(listener) {
this._listeners.push(listener);
return () => {
this._listeners = this._listeners.filter(l => l !== listener);
};
}
}
class ViewModel {
constructor() {
this.count = new Observable(0);
this.message = new Observable('');
}
increment() {
this.count.value++;
}
decrement() {
this.count.value--;
}
updateMessage(text) {
this.message.value = text;
}
}
// View bindings
function bindView(viewModel, elements) {
// Bind count
viewModel.count.subscribe(value => {
elements.countDisplay.textContent = value;
});
// Bind message
viewModel.message.subscribe(value => {
elements.messageDisplay.textContent = value;
});
// Bind events
elements.incrementBtn.addEventListener('click', () => {
viewModel.increment();
});
elements.decrementBtn.addEventListener('click', () => {
viewModel.decrement();
});
elements.messageInput.addEventListener('input', (e) => {
viewModel.updateMessage(e.target.value);
});
// Initial render
elements.countDisplay.textContent = viewModel.count.value;
elements.messageDisplay.textContent = viewModel.message.value;
}
Summary
In this module, you learned:
- ✅ Creational patterns: Singleton, Factory, Builder
- ✅ Structural patterns: Module, Decorator, Proxy
- ✅ Behavioral patterns: Observer, Strategy, Command, State
- ✅ Architectural patterns: MVC, MVVM
- ✅ When and how to apply each pattern
- ✅ Real-world implementations in JavaScript
- ✅ Modern JavaScript features in patterns
- ✅ Pattern trade-offs and best practices
In Module 29, you'll learn about Testing and Test-Driven Development, writing reliable tests and following TDD principles.
Practice Exercises
- Implement a plugin system using Singleton and Factory patterns
- Create a middleware pipeline using Chain of Responsibility pattern
- Build a state machine for a game character
- Implement undo/redo for a drawing application using Command pattern
- Create a reactive form validation system using Observer pattern
- Build a caching proxy for API calls
- Implement Strategy pattern for different payment methods
- Create a simple MVC todo application