javascript

exercises

exercises.js
/**
 * ========================================
 * 8.5 CLASS PATTERNS - EXERCISES
 * ========================================
 *
 * Practice implementing common design patterns with ES6+ classes.
 *
 * Instructions:
 * 1. Read each exercise description carefully
 * 2. Implement the solution below the exercise
 * 3. Check the solution in the comments if stuck
 */

/**
 * EXERCISE 1: Factory Pattern - Shape Factory
 *
 * Create a ShapeFactory that creates different shapes based on type.
 * Each shape should have:
 * - A type property
 * - An area() method
 * - A perimeter() method
 *
 * Shapes to support: circle, rectangle, triangle
 */
console.log('--- Exercise 1: Shape Factory ---');

// Your code here:

// Test cases:
// const circle = ShapeFactory.create('circle', { radius: 5 });
// console.log(circle.area());      // ~78.54
// console.log(circle.perimeter()); // ~31.42

// const rect = ShapeFactory.create('rectangle', { width: 4, height: 6 });
// console.log(rect.area());      // 24
// console.log(rect.perimeter()); // 20

// const triangle = ShapeFactory.create('triangle', { a: 3, b: 4, c: 5 });
// console.log(triangle.area());      // 6 (for right triangle)
// console.log(triangle.perimeter()); // 12

/*
 * SOLUTION 1:
 *
 * class Circle {
 *   constructor({ radius }) {
 *     this.type = 'circle';
 *     this.radius = radius;
 *   }
 *   area() { return Math.PI * this.radius ** 2; }
 *   perimeter() { return 2 * Math.PI * this.radius; }
 * }
 *
 * class Rectangle {
 *   constructor({ width, height }) {
 *     this.type = 'rectangle';
 *     this.width = width;
 *     this.height = height;
 *   }
 *   area() { return this.width * this.height; }
 *   perimeter() { return 2 * (this.width + this.height); }
 * }
 *
 * class Triangle {
 *   constructor({ a, b, c }) {
 *     this.type = 'triangle';
 *     this.a = a; this.b = b; this.c = c;
 *   }
 *   area() {
 *     const s = (this.a + this.b + this.c) / 2;
 *     return Math.sqrt(s * (s - this.a) * (s - this.b) * (s - this.c));
 *   }
 *   perimeter() { return this.a + this.b + this.c; }
 * }
 *
 * class ShapeFactory {
 *   static create(type, options) {
 *     switch (type) {
 *       case 'circle': return new Circle(options);
 *       case 'rectangle': return new Rectangle(options);
 *       case 'triangle': return new Triangle(options);
 *       default: throw new Error(`Unknown shape: ${type}`);
 *     }
 *   }
 * }
 */

/**
 * EXERCISE 2: Builder Pattern - Email Builder
 *
 * Create an EmailBuilder class that builds email objects step by step.
 * Support:
 * - from(address): sender email
 * - to(address): recipient (can be called multiple times)
 * - cc(address): CC recipient
 * - subject(text): email subject
 * - body(text): email body
 * - html(content): HTML content
 * - attachment(file): add attachment
 * - build(): returns the email object
 */
console.log('\n--- Exercise 2: Email Builder ---');

// Your code here:

// Test cases:
// const email = new EmailBuilder()
//   .from('sender@example.com')
//   .to('user1@example.com')
//   .to('user2@example.com')
//   .cc('manager@example.com')
//   .subject('Meeting Reminder')
//   .body('Don\'t forget our meeting at 3pm')
//   .attachment('agenda.pdf')
//   .build();
//
// console.log(email);
// Should output: { from, to: [...], cc: [...], subject, body, html: null, attachments: [...] }

/*
 * SOLUTION 2:
 *
 * class EmailBuilder {
 *   #email = {
 *     from: null,
 *     to: [],
 *     cc: [],
 *     subject: '',
 *     body: '',
 *     html: null,
 *     attachments: []
 *   };
 *
 *   from(address) {
 *     this.#email.from = address;
 *     return this;
 *   }
 *
 *   to(address) {
 *     this.#email.to.push(address);
 *     return this;
 *   }
 *
 *   cc(address) {
 *     this.#email.cc.push(address);
 *     return this;
 *   }
 *
 *   subject(text) {
 *     this.#email.subject = text;
 *     return this;
 *   }
 *
 *   body(text) {
 *     this.#email.body = text;
 *     return this;
 *   }
 *
 *   html(content) {
 *     this.#email.html = content;
 *     return this;
 *   }
 *
 *   attachment(file) {
 *     this.#email.attachments.push(file);
 *     return this;
 *   }
 *
 *   build() {
 *     if (!this.#email.from) throw new Error('From address required');
 *     if (this.#email.to.length === 0) throw new Error('At least one recipient required');
 *     return { ...this.#email };
 *   }
 * }
 */

/**
 * EXERCISE 3: Singleton Pattern - Configuration Manager
 *
 * Create a Configuration singleton class that:
 * - Ensures only one instance exists
 * - Allows getting/setting configuration values
 * - Supports loading from an object
 * - Has a reset() method to clear all values
 */
console.log('\n--- Exercise 3: Configuration Manager ---');

// Your code here:

// Test cases:
// const config1 = Configuration.getInstance();
// const config2 = Configuration.getInstance();
// console.log(config1 === config2); // true
//
// config1.set('apiUrl', 'https://api.example.com');
// config1.set('timeout', 5000);
// console.log(config2.get('apiUrl')); // 'https://api.example.com'
//
// config1.load({ theme: 'dark', language: 'en' });
// console.log(config1.getAll()); // { apiUrl: '...', timeout: 5000, theme: 'dark', language: 'en' }

/*
 * SOLUTION 3:
 *
 * class Configuration {
 *   static #instance = null;
 *   #config = {};
 *
 *   constructor() {
 *     if (Configuration.#instance) {
 *       return Configuration.#instance;
 *     }
 *     Configuration.#instance = this;
 *   }
 *
 *   static getInstance() {
 *     if (!Configuration.#instance) {
 *       new Configuration();
 *     }
 *     return Configuration.#instance;
 *   }
 *
 *   get(key) {
 *     return this.#config[key];
 *   }
 *
 *   set(key, value) {
 *     this.#config[key] = value;
 *     return this;
 *   }
 *
 *   load(obj) {
 *     this.#config = { ...this.#config, ...obj };
 *     return this;
 *   }
 *
 *   getAll() {
 *     return { ...this.#config };
 *   }
 *
 *   reset() {
 *     this.#config = {};
 *     return this;
 *   }
 * }
 */

/**
 * EXERCISE 4: Observer Pattern - Stock Ticker
 *
 * Create a StockTicker class that:
 * - Tracks stock prices
 * - Allows observers to subscribe/unsubscribe
 * - Notifies observers when prices change
 * - Observers receive: symbol, oldPrice, newPrice
 */
console.log('\n--- Exercise 4: Stock Ticker ---');

// Your code here:

// Test cases:
// const ticker = new StockTicker();
//
// const investor1 = (symbol, oldPrice, newPrice) => {
//   console.log(`Investor 1: ${symbol} changed from $${oldPrice} to $${newPrice}`);
// };
//
// const investor2 = (symbol, oldPrice, newPrice) => {
//   if (newPrice > oldPrice) {
//     console.log(`Investor 2: ${symbol} is going UP!`);
//   }
// };
//
// ticker.subscribe(investor1);
// ticker.subscribe(investor2);
//
// ticker.updatePrice('AAPL', 150);
// ticker.updatePrice('AAPL', 155);
// ticker.updatePrice('GOOGL', 2800);
//
// ticker.unsubscribe(investor1);
// ticker.updatePrice('AAPL', 160); // Only investor2 notified

/*
 * SOLUTION 4:
 *
 * class StockTicker {
 *   #observers = new Set();
 *   #prices = new Map();
 *
 *   subscribe(observer) {
 *     this.#observers.add(observer);
 *     return () => this.unsubscribe(observer);
 *   }
 *
 *   unsubscribe(observer) {
 *     this.#observers.delete(observer);
 *   }
 *
 *   updatePrice(symbol, newPrice) {
 *     const oldPrice = this.#prices.get(symbol) || 0;
 *     this.#prices.set(symbol, newPrice);
 *
 *     for (const observer of this.#observers) {
 *       observer(symbol, oldPrice, newPrice);
 *     }
 *   }
 *
 *   getPrice(symbol) {
 *     return this.#prices.get(symbol);
 *   }
 * }
 */

/**
 * EXERCISE 5: Strategy Pattern - Compression Strategy
 *
 * Create a compression system with different strategies:
 * - ZipStrategy: simulates zip compression
 * - GzipStrategy: simulates gzip compression
 * - Bz2Strategy: simulates bz2 compression
 *
 * Each strategy has compress(data) and decompress(data) methods.
 */
console.log('\n--- Exercise 5: Compression Strategy ---');

// Your code here:

// Test cases:
// const compressor = new Compressor();
//
// compressor.setStrategy(new ZipStrategy());
// const zipped = compressor.compress('Hello World');
// console.log(zipped);   // '[ZIP] Hello World'
// console.log(compressor.decompress(zipped)); // 'Hello World'
//
// compressor.setStrategy(new GzipStrategy());
// const gzipped = compressor.compress('Hello World');
// console.log(gzipped);  // '[GZIP] Hello World'

/*
 * SOLUTION 5:
 *
 * class CompressionStrategy {
 *   compress(data) { throw new Error('Not implemented'); }
 *   decompress(data) { throw new Error('Not implemented'); }
 * }
 *
 * class ZipStrategy extends CompressionStrategy {
 *   compress(data) { return `[ZIP] ${data}`; }
 *   decompress(data) { return data.replace('[ZIP] ', ''); }
 * }
 *
 * class GzipStrategy extends CompressionStrategy {
 *   compress(data) { return `[GZIP] ${data}`; }
 *   decompress(data) { return data.replace('[GZIP] ', ''); }
 * }
 *
 * class Bz2Strategy extends CompressionStrategy {
 *   compress(data) { return `[BZ2] ${data}`; }
 *   decompress(data) { return data.replace('[BZ2] ', ''); }
 * }
 *
 * class Compressor {
 *   #strategy = null;
 *
 *   setStrategy(strategy) {
 *     this.#strategy = strategy;
 *     return this;
 *   }
 *
 *   compress(data) {
 *     if (!this.#strategy) throw new Error('No strategy set');
 *     return this.#strategy.compress(data);
 *   }
 *
 *   decompress(data) {
 *     if (!this.#strategy) throw new Error('No strategy set');
 *     return this.#strategy.decompress(data);
 *   }
 * }
 */

/**
 * EXERCISE 6: Decorator Pattern - HTTP Request
 *
 * Create a decorator system for HTTP requests:
 * - Base HttpRequest with send() method
 * - AuthDecorator: adds authentication header
 * - LoggingDecorator: logs request/response
 * - CacheDecorator: caches responses
 * - RetryDecorator: retries failed requests
 */
console.log('\n--- Exercise 6: HTTP Request Decorators ---');

// Your code here:

// Test cases:
// let request = new HttpRequest('https://api.example.com/data');
// request = new AuthDecorator(request, 'token123');
// request = new LoggingDecorator(request);
// request = new RetryDecorator(request, 3);
//
// const response = request.send();
// console.log(response);

/*
 * SOLUTION 6:
 *
 * class HttpRequest {
 *   constructor(url) {
 *     this.url = url;
 *     this.headers = {};
 *   }
 *
 *   send() {
 *     // Simulate HTTP request
 *     console.log(`Sending request to ${this.url}`);
 *     return { status: 200, data: 'Response data', headers: this.headers };
 *   }
 * }
 *
 * class RequestDecorator {
 *   constructor(request) {
 *     this.request = request;
 *   }
 *
 *   send() {
 *     return this.request.send();
 *   }
 *
 *   get url() { return this.request.url; }
 *   get headers() { return this.request.headers; }
 * }
 *
 * class AuthDecorator extends RequestDecorator {
 *   constructor(request, token) {
 *     super(request);
 *     this.token = token;
 *   }
 *
 *   send() {
 *     this.request.headers['Authorization'] = `Bearer ${this.token}`;
 *     return super.send();
 *   }
 * }
 *
 * class LoggingDecorator extends RequestDecorator {
 *   send() {
 *     console.log(`[LOG] Request: ${this.url}`);
 *     const response = super.send();
 *     console.log(`[LOG] Response: ${response.status}`);
 *     return response;
 *   }
 * }
 *
 * class RetryDecorator extends RequestDecorator {
 *   constructor(request, maxRetries = 3) {
 *     super(request);
 *     this.maxRetries = maxRetries;
 *   }
 *
 *   send() {
 *     let lastError;
 *     for (let i = 0; i < this.maxRetries; i++) {
 *       try {
 *         return super.send();
 *       } catch (error) {
 *         console.log(`Retry ${i + 1}/${this.maxRetries}`);
 *         lastError = error;
 *       }
 *     }
 *     throw lastError;
 *   }
 * }
 */

/**
 * EXERCISE 7: Command Pattern - Calculator with History
 *
 * Create a calculator with undo/redo functionality:
 * - AddCommand, SubtractCommand, MultiplyCommand, DivideCommand
 * - Each command stores operation details for undo
 * - Calculator tracks command history
 */
console.log('\n--- Exercise 7: Calculator with Commands ---');

// Your code here:

// Test cases:
// const calc = new Calculator();
//
// calc.execute(new AddCommand(10));        // value = 10
// calc.execute(new MultiplyCommand(2));    // value = 20
// calc.execute(new SubtractCommand(5));    // value = 15
// calc.execute(new DivideCommand(3));      // value = 5
//
// console.log(calc.getValue()); // 5
//
// calc.undo(); // value = 15
// calc.undo(); // value = 20
// console.log(calc.getValue()); // 20
//
// calc.redo(); // value = 15
// console.log(calc.getValue()); // 15

/*
 * SOLUTION 7:
 *
 * class CalculatorCommand {
 *   constructor(value) {
 *     this.value = value;
 *     this.previousValue = null;
 *   }
 *   execute(calculator) { throw new Error('Not implemented'); }
 *   undo(calculator) {
 *     calculator.setValue(this.previousValue);
 *   }
 * }
 *
 * class AddCommand extends CalculatorCommand {
 *   execute(calculator) {
 *     this.previousValue = calculator.getValue();
 *     calculator.setValue(this.previousValue + this.value);
 *   }
 * }
 *
 * class SubtractCommand extends CalculatorCommand {
 *   execute(calculator) {
 *     this.previousValue = calculator.getValue();
 *     calculator.setValue(this.previousValue - this.value);
 *   }
 * }
 *
 * class MultiplyCommand extends CalculatorCommand {
 *   execute(calculator) {
 *     this.previousValue = calculator.getValue();
 *     calculator.setValue(this.previousValue * this.value);
 *   }
 * }
 *
 * class DivideCommand extends CalculatorCommand {
 *   execute(calculator) {
 *     this.previousValue = calculator.getValue();
 *     calculator.setValue(this.previousValue / this.value);
 *   }
 * }
 *
 * class Calculator {
 *   #value = 0;
 *   #history = [];
 *   #redoStack = [];
 *
 *   getValue() { return this.#value; }
 *   setValue(value) { this.#value = value; }
 *
 *   execute(command) {
 *     command.execute(this);
 *     this.#history.push(command);
 *     this.#redoStack = [];
 *   }
 *
 *   undo() {
 *     const command = this.#history.pop();
 *     if (command) {
 *       command.undo(this);
 *       this.#redoStack.push(command);
 *     }
 *   }
 *
 *   redo() {
 *     const command = this.#redoStack.pop();
 *     if (command) {
 *       command.execute(this);
 *       this.#history.push(command);
 *     }
 *   }
 * }
 */

/**
 * EXERCISE 8: State Pattern - Traffic Light
 *
 * Create a traffic light that cycles through states:
 * - RedState: next() → GreenState
 * - GreenState: next() → YellowState
 * - YellowState: next() → RedState
 *
 * Each state has a duration and color property.
 */
console.log('\n--- Exercise 8: Traffic Light States ---');

// Your code here:

// Test cases:
// const trafficLight = new TrafficLight();
//
// console.log(trafficLight.getColor());    // 'red'
// console.log(trafficLight.getDuration()); // 60000
//
// trafficLight.next();
// console.log(trafficLight.getColor());    // 'green'
// console.log(trafficLight.getDuration()); // 55000
//
// trafficLight.next();
// console.log(trafficLight.getColor());    // 'yellow'
//
// trafficLight.next();
// console.log(trafficLight.getColor());    // 'red' (cycles back)

/*
 * SOLUTION 8:
 *
 * class TrafficLightState {
 *   constructor(color, duration) {
 *     this.color = color;
 *     this.duration = duration;
 *   }
 *   next(trafficLight) { throw new Error('Not implemented'); }
 * }
 *
 * class RedState extends TrafficLightState {
 *   constructor() { super('red', 60000); }
 *   next(trafficLight) { trafficLight.setState(new GreenState()); }
 * }
 *
 * class GreenState extends TrafficLightState {
 *   constructor() { super('green', 55000); }
 *   next(trafficLight) { trafficLight.setState(new YellowState()); }
 * }
 *
 * class YellowState extends TrafficLightState {
 *   constructor() { super('yellow', 5000); }
 *   next(trafficLight) { trafficLight.setState(new RedState()); }
 * }
 *
 * class TrafficLight {
 *   #state;
 *
 *   constructor() {
 *     this.#state = new RedState();
 *   }
 *
 *   setState(state) {
 *     this.#state = state;
 *   }
 *
 *   next() {
 *     this.#state.next(this);
 *   }
 *
 *   getColor() {
 *     return this.#state.color;
 *   }
 *
 *   getDuration() {
 *     return this.#state.duration;
 *   }
 * }
 */

/**
 * EXERCISE 9: Composite Pattern - Organization Chart
 *
 * Create an organization structure where:
 * - Employee: leaf node (name, position, salary)
 * - Department: composite (name, has employees or sub-departments)
 *
 * Both should have:
 * - getSalaryTotal(): total salary
 * - getHeadCount(): number of employees
 * - print(): display hierarchy
 */
console.log('\n--- Exercise 9: Organization Chart ---');

// Your code here:

// Test cases:
// const dev1 = new Employee('Alice', 'Developer', 80000);
// const dev2 = new Employee('Bob', 'Developer', 75000);
// const lead = new Employee('Charlie', 'Lead', 100000);
//
// const engineering = new Department('Engineering');
// engineering.add(dev1).add(dev2).add(lead);
//
// const hr = new Department('HR');
// hr.add(new Employee('Diana', 'HR Manager', 70000));
//
// const company = new Department('Company');
// company.add(engineering).add(hr);
//
// console.log('Total salary:', company.getSalaryTotal()); // 325000
// console.log('Head count:', company.getHeadCount());      // 4
// company.print();

/*
 * SOLUTION 9:
 *
 * class OrgUnit {
 *   getSalaryTotal() { throw new Error('Not implemented'); }
 *   getHeadCount() { throw new Error('Not implemented'); }
 *   print(indent = 0) { throw new Error('Not implemented'); }
 * }
 *
 * class Employee extends OrgUnit {
 *   constructor(name, position, salary) {
 *     super();
 *     this.name = name;
 *     this.position = position;
 *     this.salary = salary;
 *   }
 *
 *   getSalaryTotal() { return this.salary; }
 *   getHeadCount() { return 1; }
 *   print(indent = 0) {
 *     console.log(`${'  '.repeat(indent)}👤 ${this.name} - ${this.position} ($${this.salary})`);
 *   }
 * }
 *
 * class Department extends OrgUnit {
 *   #members = [];
 *
 *   constructor(name) {
 *     super();
 *     this.name = name;
 *   }
 *
 *   add(member) {
 *     this.#members.push(member);
 *     return this;
 *   }
 *
 *   getSalaryTotal() {
 *     return this.#members.reduce((sum, m) => sum + m.getSalaryTotal(), 0);
 *   }
 *
 *   getHeadCount() {
 *     return this.#members.reduce((sum, m) => sum + m.getHeadCount(), 0);
 *   }
 *
 *   print(indent = 0) {
 *     console.log(`${'  '.repeat(indent)}📁 ${this.name}`);
 *     this.#members.forEach(m => m.print(indent + 1));
 *   }
 * }
 */

/**
 * EXERCISE 10: Chain of Responsibility - Discount Calculator
 *
 * Create a chain of discount handlers:
 * - MemberDiscountHandler: 5% for members
 * - VolumeDiscountHandler: 10% for orders > 10 items
 * - CouponHandler: applies coupon code discount
 * - SeasonalHandler: 15% during holiday season
 *
 * Discounts stack (multiply together).
 */
console.log('\n--- Exercise 10: Discount Chain ---');

// Your code here:

// Test cases:
// const order = {
//   items: 15,
//   total: 200,
//   isMember: true,
//   couponCode: 'SAVE10',
//   isHolidaySeason: true
// };
//
// const memberHandler = new MemberDiscountHandler();
// const volumeHandler = new VolumeDiscountHandler();
// const couponHandler = new CouponHandler({ 'SAVE10': 0.10 });
// const seasonalHandler = new SeasonalHandler();
//
// memberHandler
//   .setNext(volumeHandler)
//   .setNext(couponHandler)
//   .setNext(seasonalHandler);
//
// const finalTotal = memberHandler.handle(order);
// console.log(`Final total: $${finalTotal.toFixed(2)}`);

/*
 * SOLUTION 10:
 *
 * class DiscountHandler {
 *   #next = null;
 *
 *   setNext(handler) {
 *     this.#next = handler;
 *     return handler;
 *   }
 *
 *   handle(order) {
 *     if (this.#next) {
 *       return this.#next.handle(order);
 *     }
 *     return order.total;
 *   }
 * }
 *
 * class MemberDiscountHandler extends DiscountHandler {
 *   handle(order) {
 *     if (order.isMember) {
 *       order.total *= 0.95;
 *       console.log('Member discount: 5% off');
 *     }
 *     return super.handle(order);
 *   }
 * }
 *
 * class VolumeDiscountHandler extends DiscountHandler {
 *   handle(order) {
 *     if (order.items > 10) {
 *       order.total *= 0.90;
 *       console.log('Volume discount: 10% off');
 *     }
 *     return super.handle(order);
 *   }
 * }
 *
 * class CouponHandler extends DiscountHandler {
 *   #coupons;
 *
 *   constructor(coupons = {}) {
 *     super();
 *     this.#coupons = coupons;
 *   }
 *
 *   handle(order) {
 *     const discount = this.#coupons[order.couponCode];
 *     if (discount) {
 *       order.total *= (1 - discount);
 *       console.log(`Coupon ${order.couponCode}: ${discount * 100}% off`);
 *     }
 *     return super.handle(order);
 *   }
 * }
 *
 * class SeasonalHandler extends DiscountHandler {
 *   handle(order) {
 *     if (order.isHolidaySeason) {
 *       order.total *= 0.85;
 *       console.log('Seasonal discount: 15% off');
 *     }
 *     return super.handle(order);
 *   }
 * }
 */

/**
 * EXERCISE 11: Adapter Pattern - Legacy Data Adapter
 *
 * Create adapters to convert legacy API responses to modern format.
 *
 * Legacy format: { user_name, user_email, created_timestamp }
 * Modern format: { name, email, createdAt: Date }
 *
 * Create UserAdapter that works with both formats.
 */
console.log('\n--- Exercise 11: Legacy Data Adapter ---');

// Your code here:

// Test cases:
// const legacyData = {
//   user_name: 'john_doe',
//   user_email: 'john@example.com',
//   created_timestamp: 1609459200000
// };
//
// const modernData = {
//   name: 'Jane Doe',
//   email: 'jane@example.com',
//   createdAt: new Date('2021-01-01')
// };
//
// const adapter = new UserAdapter();
//
// const user1 = adapter.normalize(legacyData);
// const user2 = adapter.normalize(modernData);
//
// console.log(user1); // { name: 'john_doe', email: 'john@example.com', createdAt: Date }
// console.log(user2); // { name: 'Jane Doe', email: 'jane@example.com', createdAt: Date }

/*
 * SOLUTION 11:
 *
 * class UserAdapter {
 *   isLegacyFormat(data) {
 *     return 'user_name' in data || 'user_email' in data || 'created_timestamp' in data;
 *   }
 *
 *   normalize(data) {
 *     if (this.isLegacyFormat(data)) {
 *       return this.fromLegacy(data);
 *     }
 *     return this.fromModern(data);
 *   }
 *
 *   fromLegacy(data) {
 *     return {
 *       name: data.user_name,
 *       email: data.user_email,
 *       createdAt: new Date(data.created_timestamp)
 *     };
 *   }
 *
 *   fromModern(data) {
 *     return {
 *       name: data.name,
 *       email: data.email,
 *       createdAt: data.createdAt instanceof Date ? data.createdAt : new Date(data.createdAt)
 *     };
 *   }
 *
 *   toLegacy(data) {
 *     return {
 *       user_name: data.name,
 *       user_email: data.email,
 *       created_timestamp: data.createdAt.getTime()
 *     };
 *   }
 * }
 */

/**
 * EXERCISE 12: Mixin Pattern - Game Character
 *
 * Create mixins for game character abilities:
 * - Moveable: x, y, move(dx, dy)
 * - Attackable: health, takeDamage(amount), isAlive()
 * - Attacker: attack(target), damage property
 * - Flying: altitude, fly(height), land()
 *
 * Create different character types combining mixins.
 */
console.log('\n--- Exercise 12: Game Character Mixins ---');

// Your code here:

// Test cases:
// const warrior = new Warrior('Conan');
// const dragon = new Dragon('Smaug');
//
// console.log(warrior.isAlive()); // true
// warrior.move(5, 3);
// console.log(`${warrior.name} at (${warrior.x}, ${warrior.y})`);
//
// dragon.fly(100);
// dragon.attack(warrior);
// console.log(`${warrior.name} health: ${warrior.health}`);
//
// warrior.attack(dragon);
// console.log(`${dragon.name} health: ${dragon.health}`);

/*
 * SOLUTION 12:
 *
 * const Moveable = (Base) => class extends Base {
 *   x = 0;
 *   y = 0;
 *
 *   move(dx, dy) {
 *     this.x += dx;
 *     this.y += dy;
 *     return this;
 *   }
 * };
 *
 * const Attackable = (Base) => class extends Base {
 *   health = 100;
 *
 *   takeDamage(amount) {
 *     this.health = Math.max(0, this.health - amount);
 *     return this;
 *   }
 *
 *   isAlive() {
 *     return this.health > 0;
 *   }
 * };
 *
 * const Attacker = (Base) => class extends Base {
 *   damage = 10;
 *
 *   attack(target) {
 *     if (target.takeDamage) {
 *       target.takeDamage(this.damage);
 *       console.log(`${this.name} attacks ${target.name} for ${this.damage} damage!`);
 *     }
 *     return this;
 *   }
 * };
 *
 * const Flying = (Base) => class extends Base {
 *   altitude = 0;
 *
 *   fly(height) {
 *     this.altitude = height;
 *     console.log(`${this.name} flies to ${height}m`);
 *     return this;
 *   }
 *
 *   land() {
 *     this.altitude = 0;
 *     return this;
 *   }
 * };
 *
 * class Character {
 *   constructor(name) {
 *     this.name = name;
 *   }
 * }
 *
 * class Warrior extends Attacker(Attackable(Moveable(Character))) {
 *   damage = 15;
 * }
 *
 * class Dragon extends Flying(Attacker(Attackable(Moveable(Character)))) {
 *   damage = 30;
 *   health = 200;
 * }
 */

/**
 * EXERCISE 13: Facade Pattern - Multimedia Player
 *
 * Create a MediaPlayerFacade that simplifies:
 * - AudioDecoder: decode(file) → audio data
 * - VideoDecoder: decode(file) → video data
 * - Screen: render(data)
 * - Speakers: play(audio)
 * - Subtitles: load(file), render(timestamp)
 *
 * Facade provides: playVideo(file), playAudio(file), stop()
 */
console.log('\n--- Exercise 13: Multimedia Facade ---');

// Your code here:

// Test cases:
// const player = new MediaPlayerFacade();
//
// player.playVideo('movie.mp4');
// // Output: AudioDecoder: decoding movie.mp4
// //         VideoDecoder: decoding movie.mp4
// //         Screen: rendering video
// //         Speakers: playing audio
// //         Subtitles: loaded movie.srt
//
// player.stop();
// // Output: Screen: stopped
// //         Speakers: stopped

/*
 * SOLUTION 13:
 *
 * class AudioDecoder {
 *   decode(file) {
 *     console.log(`AudioDecoder: decoding ${file}`);
 *     return { type: 'audio', file };
 *   }
 * }
 *
 * class VideoDecoder {
 *   decode(file) {
 *     console.log(`VideoDecoder: decoding ${file}`);
 *     return { type: 'video', file };
 *   }
 * }
 *
 * class Screen {
 *   render(data) { console.log('Screen: rendering video'); }
 *   stop() { console.log('Screen: stopped'); }
 * }
 *
 * class Speakers {
 *   play(audio) { console.log('Speakers: playing audio'); }
 *   stop() { console.log('Speakers: stopped'); }
 * }
 *
 * class Subtitles {
 *   load(file) {
 *     console.log(`Subtitles: loaded ${file}`);
 *   }
 *   render(timestamp) { console.log(`Subtitles at ${timestamp}`); }
 * }
 *
 * class MediaPlayerFacade {
 *   #audioDecoder = new AudioDecoder();
 *   #videoDecoder = new VideoDecoder();
 *   #screen = new Screen();
 *   #speakers = new Speakers();
 *   #subtitles = new Subtitles();
 *
 *   playVideo(file) {
 *     const audio = this.#audioDecoder.decode(file);
 *     const video = this.#videoDecoder.decode(file);
 *     this.#screen.render(video);
 *     this.#speakers.play(audio);
 *
 *     const subtitleFile = file.replace(/\.\w+$/, '.srt');
 *     this.#subtitles.load(subtitleFile);
 *   }
 *
 *   playAudio(file) {
 *     const audio = this.#audioDecoder.decode(file);
 *     this.#speakers.play(audio);
 *   }
 *
 *   stop() {
 *     this.#screen.stop();
 *     this.#speakers.stop();
 *   }
 * }
 */

/**
 * EXERCISE 14: Repository Pattern - Product Repository
 *
 * Create a ProductRepository with:
 * - CRUD operations (create, read, update, delete)
 * - findByCategory(category)
 * - findByPriceRange(min, max)
 * - search(query): search in name and description
 */
console.log('\n--- Exercise 14: Product Repository ---');

// Your code here:

// Test cases:
// const repo = new ProductRepository();
//
// repo.create({ name: 'Laptop', category: 'Electronics', price: 999, description: 'Powerful laptop' });
// repo.create({ name: 'Mouse', category: 'Electronics', price: 29, description: 'Wireless mouse' });
// repo.create({ name: 'Desk', category: 'Furniture', price: 299, description: 'Standing desk' });
//
// console.log('All:', repo.findAll().map(p => p.name));
// console.log('Electronics:', repo.findByCategory('Electronics').map(p => p.name));
// console.log('Under $100:', repo.findByPriceRange(0, 100).map(p => p.name));
// console.log('Search "desk":', repo.search('desk').map(p => p.name));

/*
 * SOLUTION 14:
 *
 * class ProductRepository {
 *   #products = new Map();
 *   #nextId = 1;
 *
 *   create(data) {
 *     const product = { ...data, id: this.#nextId++ };
 *     this.#products.set(product.id, product);
 *     return product;
 *   }
 *
 *   findById(id) {
 *     return this.#products.get(id) || null;
 *   }
 *
 *   findAll() {
 *     return [...this.#products.values()];
 *   }
 *
 *   update(id, data) {
 *     const product = this.findById(id);
 *     if (!product) return null;
 *     const updated = { ...product, ...data, id };
 *     this.#products.set(id, updated);
 *     return updated;
 *   }
 *
 *   delete(id) {
 *     return this.#products.delete(id);
 *   }
 *
 *   findByCategory(category) {
 *     return this.findAll().filter(p => p.category === category);
 *   }
 *
 *   findByPriceRange(min, max) {
 *     return this.findAll().filter(p => p.price >= min && p.price <= max);
 *   }
 *
 *   search(query) {
 *     const lowerQuery = query.toLowerCase();
 *     return this.findAll().filter(p =>
 *       p.name.toLowerCase().includes(lowerQuery) ||
 *       p.description.toLowerCase().includes(lowerQuery)
 *     );
 *   }
 * }
 */

/**
 * EXERCISE 15: Complete System - Event Management System
 *
 * Combine multiple patterns to create an event management system:
 *
 * Patterns to use:
 * - Factory: Create different event types (Conference, Workshop, Webinar)
 * - Observer: Notify attendees of updates
 * - Strategy: Different pricing strategies
 * - Repository: Store and retrieve events
 * - Builder: Build complex event configurations
 *
 * Features:
 * - Create events with builder
 * - Register attendees (observers)
 * - Apply pricing strategies
 * - Store/retrieve from repository
 */
console.log('\n--- Exercise 15: Event Management System ---');

// Your code here:

// Test cases:
// const eventRepo = new EventRepository();
//
// // Create event with builder
// const conference = new EventBuilder()
//   .setType('conference')
//   .setTitle('JS Conf 2024')
//   .setDate(new Date('2024-06-15'))
//   .setCapacity(500)
//   .setPricing(new EarlyBirdPricing(299))
//   .build();
//
// eventRepo.save(conference);
//
// // Register attendees
// conference.registerAttendee({
//   name: 'Alice',
//   onUpdate: (msg) => console.log(`Alice received: ${msg}`)
// });
//
// // Update event (notifies attendees)
// conference.update({ venue: 'Convention Center' });
//
// // Get ticket price
// console.log('Ticket price:', conference.getTicketPrice());
//
// // Find events
// console.log('All events:', eventRepo.findAll().map(e => e.title));

/*
 * SOLUTION 15:
 *
 * // Observer mixin
 * const Observable = (Base) => class extends Base {
 *   #observers = new Set();
 *
 *   registerAttendee(attendee) {
 *     this.#observers.add(attendee);
 *     return () => this.#observers.delete(attendee);
 *   }
 *
 *   notifyAll(message) {
 *     for (const observer of this.#observers) {
 *       observer.onUpdate(message);
 *     }
 *   }
 * };
 *
 * // Base Event class
 * class Event {
 *   constructor(type, title, date, capacity) {
 *     this.id = Date.now();
 *     this.type = type;
 *     this.title = title;
 *     this.date = date;
 *     this.capacity = capacity;
 *     this.pricing = null;
 *   }
 *
 *   update(changes) {
 *     Object.assign(this, changes);
 *     this.notifyAll(`${this.title} has been updated`);
 *   }
 *
 *   getTicketPrice() {
 *     return this.pricing ? this.pricing.calculate() : 0;
 *   }
 * }
 *
 * // Event types
 * class Conference extends Observable(Event) {
 *   constructor(...args) {
 *     super('conference', ...args);
 *   }
 * }
 *
 * class Workshop extends Observable(Event) {
 *   constructor(...args) {
 *     super('workshop', ...args);
 *   }
 * }
 *
 * class Webinar extends Observable(Event) {
 *   constructor(...args) {
 *     super('webinar', ...args);
 *   }
 * }
 *
 * // Factory
 * class EventFactory {
 *   static create(type, ...args) {
 *     switch (type) {
 *       case 'conference': return new Conference(...args);
 *       case 'workshop': return new Workshop(...args);
 *       case 'webinar': return new Webinar(...args);
 *       default: throw new Error(`Unknown event type: ${type}`);
 *     }
 *   }
 * }
 *
 * // Pricing strategies
 * class PricingStrategy {
 *   calculate() { throw new Error('Not implemented'); }
 * }
 *
 * class EarlyBirdPricing extends PricingStrategy {
 *   constructor(basePrice) {
 *     super();
 *     this.basePrice = basePrice;
 *   }
 *   calculate() { return this.basePrice * 0.8; }
 * }
 *
 * class RegularPricing extends PricingStrategy {
 *   constructor(basePrice) {
 *     super();
 *     this.basePrice = basePrice;
 *   }
 *   calculate() { return this.basePrice; }
 * }
 *
 * class VIPPricing extends PricingStrategy {
 *   constructor(basePrice) {
 *     super();
 *     this.basePrice = basePrice;
 *   }
 *   calculate() { return this.basePrice * 1.5; }
 * }
 *
 * // Builder
 * class EventBuilder {
 *   #type = 'conference';
 *   #title = '';
 *   #date = new Date();
 *   #capacity = 100;
 *   #pricing = null;
 *
 *   setType(type) { this.#type = type; return this; }
 *   setTitle(title) { this.#title = title; return this; }
 *   setDate(date) { this.#date = date; return this; }
 *   setCapacity(capacity) { this.#capacity = capacity; return this; }
 *   setPricing(pricing) { this.#pricing = pricing; return this; }
 *
 *   build() {
 *     const event = EventFactory.create(
 *       this.#type,
 *       this.#title,
 *       this.#date,
 *       this.#capacity
 *     );
 *     event.pricing = this.#pricing;
 *     return event;
 *   }
 * }
 *
 * // Repository
 * class EventRepository {
 *   #events = new Map();
 *
 *   save(event) {
 *     this.#events.set(event.id, event);
 *     return event;
 *   }
 *
 *   findById(id) {
 *     return this.#events.get(id) || null;
 *   }
 *
 *   findAll() {
 *     return [...this.#events.values()];
 *   }
 *
 *   findByType(type) {
 *     return this.findAll().filter(e => e.type === type);
 *   }
 *
 *   findUpcoming() {
 *     const now = new Date();
 *     return this.findAll().filter(e => e.date > now);
 *   }
 * }
 */

console.log('\n========================================');
console.log('End of Class Patterns Exercises');
console.log('========================================');
Exercises - JavaScript Tutorial | DeepML