javascript
exercises
exercises.js⚡javascript
/**
* ========================================
* 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('========================================');