javascript
exercises
exercises.js⚡javascript
/**
* 21.4 Design Patterns - Exercises
*
* Practice implementing common design patterns
*/
/**
* Exercise 1: Plugin System (Factory + Strategy)
*
* Create a plugin system that:
* - Dynamically loads plugins
* - Manages plugin lifecycle
* - Supports plugin dependencies
* - Handles plugin conflicts
*
* Requirements:
* 1. Factory to create plugin instances
* 2. Strategy for different plugin types
* 3. Lifecycle hooks (init, start, stop, destroy)
* 4. Dependency resolution
*/
class PluginSystem {
constructor() {
// TODO: Initialize plugin system
this.plugins = new Map();
this.factories = new Map();
this.started = new Set();
}
// Register plugin factory
registerFactory(type, factory) {
// TODO: Store factory for plugin type
}
// Create and register plugin
register(name, type, config = {}) {
// TODO: Create plugin using factory
// Check dependencies
}
// Start plugin with dependencies
async start(name) {
// TODO: Start plugin and its dependencies
}
// Stop plugin
async stop(name) {
// TODO: Stop plugin and dependents
}
// Get plugin
get(name) {
// TODO: Return plugin instance
}
// List all plugins
list() {
// TODO: Return plugin info
}
}
// Plugin interface
class Plugin {
constructor(config) {
this.config = config;
this.dependencies = config.dependencies || [];
}
async init() {}
async start() {}
async stop() {}
async destroy() {}
}
// Test your implementation
function testPluginSystem() {
const system = new PluginSystem();
console.log('Testing Plugin System...');
// Add your tests here
}
// Uncomment to test
// testPluginSystem();
/**
* Exercise 2: State Machine (State Pattern)
*
* Implement a finite state machine that:
* - Manages state transitions
* - Validates transitions
* - Supports guards and actions
* - Emits state change events
*
* Requirements:
* 1. Define states and transitions
* 2. Guard conditions for transitions
* 3. Entry/exit actions for states
* 4. Event-driven state changes
*/
class StateMachine {
constructor(config) {
// TODO: Initialize state machine
this.states = config.states || {};
this.currentState = config.initial;
this.context = config.context || {};
this.listeners = [];
}
// Get current state
get state() {
// TODO: Return current state
}
// Check if can transition
can(event) {
// TODO: Check if transition is valid
}
// Perform transition
transition(event, payload) {
// TODO: Execute transition
// Run guards, exit actions, enter actions
}
// Subscribe to state changes
onTransition(callback) {
// TODO: Add listener
}
// Get available transitions
getAvailableTransitions() {
// TODO: Return valid transitions from current state
}
}
// Example configuration
const trafficLightConfig = {
initial: 'red',
states: {
red: {
on: { TIMER: 'green' },
entry: () => console.log('Red light ON'),
},
green: {
on: { TIMER: 'yellow' },
entry: () => console.log('Green light ON'),
},
yellow: {
on: { TIMER: 'red' },
entry: () => console.log('Yellow light ON'),
},
},
};
// Test your implementation
function testStateMachine() {
const machine = new StateMachine(trafficLightConfig);
console.log('Testing State Machine...');
// Add your tests here
}
// Uncomment to test
// testStateMachine();
/**
* Exercise 3: Dependency Injection Container
*
* Build a DI container that:
* - Registers services
* - Resolves dependencies
* - Supports different lifetimes (singleton, transient, scoped)
* - Handles circular dependencies
*
* Requirements:
* 1. Register factories and classes
* 2. Automatic dependency resolution
* 3. Lifetime management
* 4. Circular dependency detection
*/
class DIContainer {
constructor() {
// TODO: Initialize container
this.services = new Map();
this.singletons = new Map();
this.resolving = new Set();
}
// Register service with lifetime
register(name, factory, options = {}) {
// TODO: Store service registration
// options: { lifetime: 'singleton' | 'transient' | 'scoped' }
}
// Register class with auto-injection
registerClass(name, Class, options = {}) {
// TODO: Register class as service
// Resolve constructor dependencies
}
// Resolve service
resolve(name) {
// TODO: Get or create service instance
// Handle lifetime and dependencies
}
// Create scoped container
createScope() {
// TODO: Create child container with scope
}
// Check for circular dependencies
checkCircular(name) {
// TODO: Detect circular dependency
}
}
// Test your implementation
function testDIContainer() {
const container = new DIContainer();
console.log('Testing DI Container...');
container.register('config', () => ({ apiUrl: 'https://api.example.com' }), {
lifetime: 'singleton',
});
container.register(
'http',
(c) => ({
baseUrl: c.resolve('config').apiUrl,
get(url) {
return `GET ${this.baseUrl}${url}`;
},
}),
{ lifetime: 'transient' }
);
// Add your tests here
}
// Uncomment to test
// testDIContainer();
/**
* Exercise 4: Chain of Responsibility
*
* Implement a request handling chain that:
* - Processes requests through handlers
* - Allows handlers to stop chain
* - Supports async handlers
* - Provides error handling
*
* Requirements:
* 1. Linear handler chain
* 2. Handlers can handle, pass, or reject
* 3. Async support
* 4. Error propagation
*/
class Handler {
constructor() {
this.next = null;
}
setNext(handler) {
// TODO: Set next handler in chain
return handler;
}
async handle(request) {
// TODO: Process or pass to next
throw new Error('Must implement handle');
}
}
class HandlerChain {
constructor() {
// TODO: Initialize chain
this.handlers = [];
}
add(handler) {
// TODO: Add handler to chain
}
async process(request) {
// TODO: Process request through chain
}
}
// Example handlers
class AuthHandler extends Handler {
async handle(request) {
// TODO: Check authentication
}
}
class ValidationHandler extends Handler {
async handle(request) {
// TODO: Validate request
}
}
class RateLimitHandler extends Handler {
constructor(limit, windowMs) {
super();
// TODO: Initialize rate limiter
}
async handle(request) {
// TODO: Check rate limit
}
}
// Test your implementation
function testChainOfResponsibility() {
const chain = new HandlerChain();
console.log('Testing Chain of Responsibility...');
// Add your tests here
}
// Uncomment to test
// testChainOfResponsibility();
/**
* Exercise 5: Composite Pattern
*
* Build a file system abstraction using composite pattern:
* - Files and folders
* - Recursive operations (size, find, list)
* - Common interface
*
* Requirements:
* 1. File and Folder classes with same interface
* 2. Folders contain children (files or folders)
* 3. Operations traverse tree
* 4. Support for visitors
*/
class FileSystemComponent {
constructor(name) {
this.name = name;
}
getSize() {
throw new Error('Must implement');
}
list(indent = 0) {
throw new Error('Must implement');
}
find(predicate) {
throw new Error('Must implement');
}
accept(visitor) {
throw new Error('Must implement');
}
}
class File extends FileSystemComponent {
constructor(name, size) {
super(name);
// TODO: Initialize file
}
getSize() {
// TODO: Return file size
}
list(indent = 0) {
// TODO: Print file info
}
find(predicate) {
// TODO: Return this if matches
}
accept(visitor) {
// TODO: Accept visitor
}
}
class Folder extends FileSystemComponent {
constructor(name) {
super(name);
// TODO: Initialize folder
}
add(component) {
// TODO: Add child
}
remove(component) {
// TODO: Remove child
}
getSize() {
// TODO: Sum children sizes
}
list(indent = 0) {
// TODO: Print folder and children
}
find(predicate) {
// TODO: Find in this and children
}
accept(visitor) {
// TODO: Accept visitor and visit children
}
}
// Visitor pattern for operations
class FileSystemVisitor {
visitFile(file) {}
visitFolder(folder) {}
}
// Test your implementation
function testCompositePattern() {
const root = new Folder('root');
console.log('Testing Composite Pattern...');
// Add your tests here
}
// Uncomment to test
// testCompositePattern();
// ============================================================
// SOLUTIONS (Uncomment to reveal)
// ============================================================
/*
// SOLUTION 1: Plugin System
class PluginSystemSolution {
constructor() {
this.plugins = new Map();
this.factories = new Map();
this.started = new Set();
}
registerFactory(type, factory) {
this.factories.set(type, factory);
}
register(name, type, config = {}) {
if (!this.factories.has(type)) {
throw new Error(`Unknown plugin type: ${type}`);
}
const factory = this.factories.get(type);
const plugin = factory({ ...config, name });
this.plugins.set(name, {
instance: plugin,
type,
config,
dependencies: config.dependencies || []
});
return this;
}
async start(name) {
if (this.started.has(name)) return;
const plugin = this.plugins.get(name);
if (!plugin) throw new Error(`Plugin not found: ${name}`);
// Start dependencies first
for (const dep of plugin.dependencies) {
if (!this.started.has(dep)) {
await this.start(dep);
}
}
// Initialize and start
await plugin.instance.init();
await plugin.instance.start();
this.started.add(name);
console.log(`Plugin started: ${name}`);
}
async stop(name) {
if (!this.started.has(name)) return;
// Find dependents
for (const [depName, dep] of this.plugins) {
if (dep.dependencies.includes(name) && this.started.has(depName)) {
await this.stop(depName);
}
}
const plugin = this.plugins.get(name);
await plugin.instance.stop();
this.started.delete(name);
console.log(`Plugin stopped: ${name}`);
}
get(name) {
return this.plugins.get(name)?.instance;
}
list() {
return Array.from(this.plugins.entries()).map(([name, p]) => ({
name,
type: p.type,
started: this.started.has(name)
}));
}
}
// SOLUTION 2: State Machine
class StateMachineSolution {
constructor(config) {
this.states = config.states || {};
this._currentState = config.initial;
this.context = config.context || {};
this.listeners = [];
// Run entry action for initial state
const initial = this.states[this._currentState];
if (initial?.entry) {
initial.entry(this.context);
}
}
get state() {
return this._currentState;
}
can(event) {
const stateConfig = this.states[this._currentState];
if (!stateConfig?.on) return false;
const transition = stateConfig.on[event];
if (!transition) return false;
// Check guard if present
if (typeof transition === 'object' && transition.guard) {
return transition.guard(this.context);
}
return true;
}
transition(event, payload) {
if (!this.can(event)) {
throw new Error(`Invalid transition: ${event} from ${this._currentState}`);
}
const stateConfig = this.states[this._currentState];
const transition = stateConfig.on[event];
const target = typeof transition === 'string' ? transition : transition.target;
const previousState = this._currentState;
// Exit action
if (stateConfig.exit) {
stateConfig.exit(this.context, payload);
}
// Update state
this._currentState = target;
// Entry action
const newStateConfig = this.states[target];
if (newStateConfig?.entry) {
newStateConfig.entry(this.context, payload);
}
// Notify listeners
this.listeners.forEach(cb => cb({
from: previousState,
to: target,
event,
payload
}));
return this._currentState;
}
onTransition(callback) {
this.listeners.push(callback);
return () => {
const idx = this.listeners.indexOf(callback);
if (idx > -1) this.listeners.splice(idx, 1);
};
}
getAvailableTransitions() {
const stateConfig = this.states[this._currentState];
return stateConfig?.on ? Object.keys(stateConfig.on) : [];
}
}
// SOLUTION 3: DI Container
class DIContainerSolution {
constructor(parent = null) {
this.services = new Map();
this.singletons = new Map();
this.scopedInstances = new Map();
this.resolving = new Set();
this.parent = parent;
}
register(name, factory, options = {}) {
const lifetime = options.lifetime || 'transient';
this.services.set(name, { factory, lifetime, dependencies: options.dependencies || [] });
return this;
}
registerClass(name, Class, options = {}) {
this.register(name, (container) => {
const deps = (options.dependencies || []).map(d => container.resolve(d));
return new Class(...deps);
}, options);
return this;
}
resolve(name) {
// Check for circular dependency
if (this.resolving.has(name)) {
throw new Error(`Circular dependency detected: ${name}`);
}
// Try parent first for singletons
if (this.parent && this.parent.singletons.has(name)) {
return this.parent.singletons.get(name);
}
// Check if registered
let registration = this.services.get(name);
if (!registration && this.parent) {
registration = this.parent.services.get(name);
}
if (!registration) {
throw new Error(`Service not registered: ${name}`);
}
const { factory, lifetime } = registration;
// Return singleton if exists
if (lifetime === 'singleton' && this.singletons.has(name)) {
return this.singletons.get(name);
}
// Return scoped if exists
if (lifetime === 'scoped' && this.scopedInstances.has(name)) {
return this.scopedInstances.get(name);
}
// Create instance
this.resolving.add(name);
try {
const instance = factory(this);
// Store based on lifetime
if (lifetime === 'singleton') {
this.singletons.set(name, instance);
} else if (lifetime === 'scoped') {
this.scopedInstances.set(name, instance);
}
return instance;
} finally {
this.resolving.delete(name);
}
}
createScope() {
return new DIContainerSolution(this);
}
}
// SOLUTION 4: Chain of Responsibility
class HandlerSolution {
constructor() {
this.next = null;
}
setNext(handler) {
this.next = handler;
return handler;
}
async handle(request) {
if (this.next) {
return this.next.handle(request);
}
return request;
}
}
class HandlerChainSolution {
constructor() {
this.handlers = [];
}
add(handler) {
if (this.handlers.length > 0) {
this.handlers[this.handlers.length - 1].setNext(handler);
}
this.handlers.push(handler);
return this;
}
async process(request) {
if (this.handlers.length === 0) {
return request;
}
return this.handlers[0].handle(request);
}
}
class AuthHandlerSolution extends HandlerSolution {
async handle(request) {
if (!request.headers?.authorization) {
return { error: 'Unauthorized', status: 401 };
}
request.authenticated = true;
return super.handle(request);
}
}
class ValidationHandlerSolution extends HandlerSolution {
constructor(rules) {
super();
this.rules = rules || {};
}
async handle(request) {
for (const [field, rule] of Object.entries(this.rules)) {
if (rule.required && !request.body?.[field]) {
return { error: `${field} is required`, status: 400 };
}
}
request.validated = true;
return super.handle(request);
}
}
// SOLUTION 5: Composite Pattern
class FileSolution extends FileSystemComponent {
constructor(name, size) {
super(name);
this.size = size;
}
getSize() {
return this.size;
}
list(indent = 0) {
console.log(`${' '.repeat(indent)}📄 ${this.name} (${this.size} bytes)`);
}
find(predicate) {
return predicate(this) ? [this] : [];
}
accept(visitor) {
visitor.visitFile(this);
}
}
class FolderSolution extends FileSystemComponent {
constructor(name) {
super(name);
this.children = [];
}
add(component) {
this.children.push(component);
return this;
}
remove(component) {
const idx = this.children.indexOf(component);
if (idx > -1) {
this.children.splice(idx, 1);
}
return this;
}
getSize() {
return this.children.reduce((sum, child) => sum + child.getSize(), 0);
}
list(indent = 0) {
console.log(`${' '.repeat(indent)}📁 ${this.name}/`);
this.children.forEach(child => child.list(indent + 1));
}
find(predicate) {
const results = predicate(this) ? [this] : [];
this.children.forEach(child => {
results.push(...child.find(predicate));
});
return results;
}
accept(visitor) {
visitor.visitFolder(this);
this.children.forEach(child => child.accept(visitor));
}
}
console.log('Solutions loaded. Uncomment test functions to verify implementations.');
*/
module.exports = {
PluginSystem,
Plugin,
StateMachine,
trafficLightConfig,
DIContainer,
Handler,
HandlerChain,
AuthHandler,
ValidationHandler,
RateLimitHandler,
FileSystemComponent,
File,
Folder,
FileSystemVisitor,
};