javascript

exercises

exercises.js
/**
 * 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,
};
Exercises - JavaScript Tutorial | DeepML