javascript

exercises

exercises.js
/**
 * ========================================
 * 7.6 OBJECT PATTERNS - EXERCISES
 * ========================================
 * Difficulty: ⭐ = Easy, ⭐⭐ = Medium, ⭐⭐⭐ = Hard
 */

/**
 * EXERCISE 1: Factory Function ⭐
 *
 * Create a factory function `createBook` that:
 * - Takes title, author, and pages
 * - Returns an object with those properties plus:
 *   - isLong: boolean (true if pages > 300)
 *   - summary(): returns "Title by Author (X pages)"
 */
function createBook(title, author, pages) {
  // Your code here
}

// const book = createBook("The Hobbit", "Tolkien", 310);
// console.log(book.isLong); // true
// console.log(book.summary()); // "The Hobbit by Tolkien (310 pages)"

/**
 * EXERCISE 2: Factory with Type ⭐
 *
 * Create a `createNotification` factory that accepts a type
 * ("success", "error", "warning", "info") and a message.
 * Return an object with:
 * - type, message, timestamp
 * - icon: emoji based on type (✓ for success, ✗ for error, ⚠ for warning, ℹ for info)
 * - display(): returns "[icon] [TYPE]: message"
 */
function createNotification(type, message) {
  // Your code here
}

// const notif = createNotification("error", "Something went wrong");
// console.log(notif.display()); // "✗ [ERROR]: Something went wrong"

/**
 * EXERCISE 3: Module Pattern - Counter ⭐⭐
 *
 * Create a module `CounterModule` using IIFE that:
 * - Has private `count` starting at 0
 * - Exposes: increment(), decrement(), getCount(), reset()
 * - increment/decrement return the module for chaining
 */
const CounterModule = (function () {
  // Your code here
})();

// CounterModule.increment().increment().increment();
// console.log(CounterModule.getCount()); // 3
// CounterModule.decrement();
// console.log(CounterModule.getCount()); // 2

/**
 * EXERCISE 4: Revealing Module - Stack ⭐⭐
 *
 * Create a module `Stack` using the revealing module pattern:
 * - Private array for storage
 * - Methods: push(item), pop(), peek(), size(), isEmpty(), clear()
 * - push returns the stack for chaining
 */
const Stack = (function () {
  // Your code here
})();

// Stack.push(1).push(2).push(3);
// console.log(Stack.peek()); // 3
// console.log(Stack.pop()); // 3
// console.log(Stack.size()); // 2

/**
 * EXERCISE 5: Singleton Pattern ⭐⭐
 *
 * Create a `GameSettings` singleton that:
 * - Can only have one instance
 * - Has properties: volume (0-100), difficulty ("easy"|"medium"|"hard")
 * - Has methods: setVolume(n), setDifficulty(d), getSettings()
 * - setVolume should clamp value between 0-100
 */
const GameSettings = (function () {
  // Your code here
})();

// const settings1 = GameSettings.getInstance();
// const settings2 = GameSettings.getInstance();
// console.log(settings1 === settings2); // true
// settings1.setVolume(150);
// console.log(settings1.getSettings().volume); // 100 (clamped)

/**
 * EXERCISE 6: Mixin Pattern ⭐⭐
 *
 * Create three mixins:
 * - Timestamped: adds createdAt, updatedAt, touch() method
 * - Identifiable: adds id (auto-generated), getId() method
 * - Validatable: adds isValid property, validate() method that returns true/false
 *
 * Create a function `createEntity` that creates an object
 * with name property and applies all three mixins.
 */
const Timestamped = {
  // Your code here
};

const Identifiable = {
  // Your code here
};

const Validatable = {
  // Your code here
};

function createEntity(name) {
  // Your code here
}

// const entity = createEntity("Test Entity");
// console.log(entity.getId()); // Some unique ID
// console.log(entity.createdAt); // Date object
// entity.touch();
// console.log(entity.updatedAt); // New date

/**
 * EXERCISE 7: Builder Pattern - Query ⭐⭐
 *
 * Create a `QueryBuilder` class that builds SQL-like query strings:
 * - select(...fields): sets fields to select
 * - from(table): sets the table
 * - where(condition): adds a WHERE condition
 * - orderBy(field, direction): sets ORDER BY
 * - limit(n): sets LIMIT
 * - build(): returns the query string
 */
class QueryBuilder {
  // Your code here
}

// const query = new QueryBuilder()
//     .select("name", "email", "age")
//     .from("users")
//     .where("age > 18")
//     .where("active = true")
//     .orderBy("name", "ASC")
//     .limit(10)
//     .build();
// console.log(query);
// "SELECT name, email, age FROM users WHERE age > 18 AND active = true ORDER BY name ASC LIMIT 10"

/**
 * EXERCISE 8: Event Emitter ⭐⭐
 *
 * Implement a simple EventEmitter class with:
 * - on(event, callback): register a listener
 * - off(event, callback): remove a listener
 * - once(event, callback): register one-time listener
 * - emit(event, ...args): trigger event with data
 * - listenerCount(event): return number of listeners
 */
class EventEmitter {
  // Your code here
}

// const emitter = new EventEmitter();
// const handler = (data) => console.log("Received:", data);
// emitter.on("message", handler);
// emitter.emit("message", "Hello"); // "Received: Hello"
// emitter.off("message", handler);
// emitter.emit("message", "World"); // (nothing)

/**
 * EXERCISE 9: Observer Pattern - Store ⭐⭐⭐
 *
 * Create a `createStore` function that returns an observable store:
 * - getState(): returns current state
 * - setState(updates): updates state and notifies subscribers
 * - subscribe(listener): adds listener, returns unsubscribe function
 * - select(selector): returns a function that subscribes but only
 *   calls listener when selected value changes
 */
function createStore(initialState) {
  // Your code here
}

// const store = createStore({ user: { name: "Alice" }, count: 0 });
// const unsubscribe = store.select(state => state.count)((count) => {
//     console.log("Count changed:", count);
// });
// store.setState({ count: 1 }); // "Count changed: 1"
// store.setState({ user: { name: "Bob" } }); // (nothing - count didn't change)

/**
 * EXERCISE 10: Command Pattern ⭐⭐⭐
 *
 * Create a TextEditor class that uses the command pattern:
 * - type(text): adds text
 * - delete(count): removes last N characters
 * - undo(): undoes last operation
 * - redo(): redoes undone operation
 * - getText(): returns current text
 */
class TextEditor {
  // Your code here
}

// const editor = new TextEditor();
// editor.type("Hello ");
// editor.type("World");
// console.log(editor.getText()); // "Hello World"
// editor.delete(5);
// console.log(editor.getText()); // "Hello "
// editor.undo();
// console.log(editor.getText()); // "Hello World"
// editor.undo();
// console.log(editor.getText()); // "Hello "

/**
 * EXERCISE 11: Strategy Pattern ⭐⭐⭐
 *
 * Create a `Validator` that uses strategy pattern:
 * - addRule(name, validator): adds validation strategy
 * - validate(value, rules): validates value against named rules
 * - Returns { valid: boolean, errors: string[] }
 *
 * Built-in rules: required, email, minLength(n), maxLength(n), pattern(regex)
 */
class Validator {
  // Your code here
}

// const validator = new Validator();
// validator.addRule("username", (value) => {
//     if (value.length < 3) return "Username too short";
//     return null;
// });
// const result = validator.validate("ab", ["required", "username"]);
// console.log(result); // { valid: false, errors: ["Username too short"] }

/**
 * EXERCISE 12: Namespace with Lazy Loading ⭐⭐⭐
 *
 * Create a namespace system that supports:
 * - namespace.define("path.to.module", factory)
 * - namespace.require("path.to.module") - lazy loads module
 * - factory is only called once when first required
 */
function createNamespace() {
  // Your code here
}

// const ns = createNamespace();
// ns.define("utils.math", () => ({
//     add: (a, b) => a + b,
//     subtract: (a, b) => a - b
// }));
// const math = ns.require("utils.math");
// console.log(math.add(2, 3)); // 5

/**
 * EXERCISE 13: Pub/Sub with Channels ⭐⭐⭐
 *
 * Create a PubSub system with channel support:
 * - subscribe(channel, callback): subscribe to channel
 * - publish(channel, data): publish to channel
 * - unsubscribe(channel, callback): unsubscribe
 * - subscribeOnce(channel, callback): one-time subscription
 * - getChannels(): list all channels with subscriber counts
 */
class PubSub {
  // Your code here
}

// const pubsub = new PubSub();
// pubsub.subscribe("news", (data) => console.log("News:", data));
// pubsub.subscribeOnce("alert", (data) => console.log("Alert:", data));
// pubsub.publish("news", "Breaking news!");
// pubsub.publish("alert", "First alert");
// pubsub.publish("alert", "Second alert"); // (nothing - was once)

/**
 * EXERCISE 14: State Machine Pattern ⭐⭐⭐
 *
 * Create a createStateMachine function that:
 * - Takes initial state and transitions config
 * - Returns object with:
 *   - getState(): current state
 *   - transition(event): changes state based on config
 *   - canTransition(event): checks if transition is valid
 *   - onEnter(state, callback): register enter callback
 *   - onLeave(state, callback): register leave callback
 */
function createStateMachine(initialState, transitions) {
  // Your code here
}

// const trafficLight = createStateMachine("red", {
//     red: { timer: "green" },
//     green: { timer: "yellow" },
//     yellow: { timer: "red" }
// });
// trafficLight.onEnter("green", () => console.log("Go!"));
// trafficLight.onLeave("red", () => console.log("Leaving red"));
// trafficLight.transition("timer"); // "Leaving red" -> "Go!" -> state is now "green"

/**
 * EXERCISE 15: Plugin System ⭐⭐⭐
 *
 * Create an application with a plugin system:
 * - Core app has: name, version, data object
 * - registerPlugin(plugin): adds plugin functionality
 * - Plugin is object with: name, install(app) function
 * - install receives app and can extend it
 * - listPlugins(): returns array of plugin names
 */
function createApp(name, version) {
  // Your code here
}

// const app = createApp("MyApp", "1.0.0");
// app.registerPlugin({
//     name: "logger",
//     install(app) {
//         app.log = (msg) => console.log(`[${app.name}]: ${msg}`);
//     }
// });
// app.log("Hello"); // "[MyApp]: Hello"
// console.log(app.listPlugins()); // ["logger"]

// ============================================
// SOLUTIONS (Hidden below - try first!)
// ============================================

/*
// SOLUTION 1:
function createBook(title, author, pages) {
    return {
        title,
        author,
        pages,
        isLong: pages > 300,
        summary() {
            return `${this.title} by ${this.author} (${this.pages} pages)`;
        }
    };
}

// SOLUTION 2:
function createNotification(type, message) {
    const icons = {
        success: "✓",
        error: "✗",
        warning: "⚠",
        info: "ℹ"
    };
    
    return {
        type,
        message,
        timestamp: new Date(),
        icon: icons[type] || "?",
        display() {
            return `${this.icon} [${this.type.toUpperCase()}]: ${this.message}`;
        }
    };
}

// SOLUTION 3:
const CounterModule = (function() {
    let count = 0;
    
    return {
        increment() {
            count++;
            return this;
        },
        decrement() {
            count--;
            return this;
        },
        getCount() {
            return count;
        },
        reset() {
            count = 0;
            return this;
        }
    };
})();

// SOLUTION 4:
const Stack = (function() {
    const items = [];
    
    function push(item) {
        items.push(item);
        return this;
    }
    
    function pop() {
        return items.pop();
    }
    
    function peek() {
        return items[items.length - 1];
    }
    
    function size() {
        return items.length;
    }
    
    function isEmpty() {
        return items.length === 0;
    }
    
    function clear() {
        items.length = 0;
        return this;
    }
    
    return { push, pop, peek, size, isEmpty, clear };
})();

// SOLUTION 5:
const GameSettings = (function() {
    let instance = null;
    
    function createInstance() {
        let volume = 50;
        let difficulty = "medium";
        
        return {
            setVolume(n) {
                volume = Math.max(0, Math.min(100, n));
                return this;
            },
            setDifficulty(d) {
                if (["easy", "medium", "hard"].includes(d)) {
                    difficulty = d;
                }
                return this;
            },
            getSettings() {
                return { volume, difficulty };
            }
        };
    }
    
    return {
        getInstance() {
            if (!instance) {
                instance = createInstance();
            }
            return instance;
        }
    };
})();

// SOLUTION 6:
let entityIdCounter = 0;

const Timestamped = {
    initTimestamps() {
        this.createdAt = new Date();
        this.updatedAt = new Date();
    },
    touch() {
        this.updatedAt = new Date();
        return this;
    }
};

const Identifiable = {
    initId() {
        this.id = ++entityIdCounter;
    },
    getId() {
        return this.id;
    }
};

const Validatable = {
    isValid: true,
    validate() {
        this.isValid = this.name && this.name.length > 0;
        return this.isValid;
    }
};

function createEntity(name) {
    const entity = { name };
    Object.assign(entity, Timestamped, Identifiable, Validatable);
    entity.initTimestamps();
    entity.initId();
    return entity;
}

// SOLUTION 7:
class QueryBuilder {
    constructor() {
        this._select = [];
        this._from = "";
        this._where = [];
        this._orderBy = null;
        this._limit = null;
    }
    
    select(...fields) {
        this._select = fields;
        return this;
    }
    
    from(table) {
        this._from = table;
        return this;
    }
    
    where(condition) {
        this._where.push(condition);
        return this;
    }
    
    orderBy(field, direction = "ASC") {
        this._orderBy = { field, direction };
        return this;
    }
    
    limit(n) {
        this._limit = n;
        return this;
    }
    
    build() {
        let query = `SELECT ${this._select.join(", ")} FROM ${this._from}`;
        
        if (this._where.length) {
            query += ` WHERE ${this._where.join(" AND ")}`;
        }
        
        if (this._orderBy) {
            query += ` ORDER BY ${this._orderBy.field} ${this._orderBy.direction}`;
        }
        
        if (this._limit) {
            query += ` LIMIT ${this._limit}`;
        }
        
        return query;
    }
}

// SOLUTION 8:
class EventEmitter {
    constructor() {
        this.events = new Map();
    }
    
    on(event, callback) {
        if (!this.events.has(event)) {
            this.events.set(event, []);
        }
        this.events.get(event).push(callback);
        return this;
    }
    
    off(event, callback) {
        if (this.events.has(event)) {
            const callbacks = this.events.get(event);
            const index = callbacks.indexOf(callback);
            if (index > -1) callbacks.splice(index, 1);
        }
        return this;
    }
    
    once(event, callback) {
        const wrapper = (...args) => {
            callback(...args);
            this.off(event, wrapper);
        };
        return this.on(event, wrapper);
    }
    
    emit(event, ...args) {
        if (this.events.has(event)) {
            for (const cb of this.events.get(event)) {
                cb(...args);
            }
        }
        return this;
    }
    
    listenerCount(event) {
        return this.events.has(event) ? this.events.get(event).length : 0;
    }
}

// SOLUTION 9:
function createStore(initialState) {
    let state = { ...initialState };
    const listeners = new Set();
    
    return {
        getState() {
            return { ...state };
        },
        
        setState(updates) {
            const prevState = state;
            state = { ...state, ...updates };
            
            for (const listener of listeners) {
                listener(state, prevState);
            }
        },
        
        subscribe(listener) {
            listeners.add(listener);
            return () => listeners.delete(listener);
        },
        
        select(selector) {
            return (listener) => {
                let prevSelected = selector(state);
                
                return this.subscribe((newState) => {
                    const newSelected = selector(newState);
                    if (newSelected !== prevSelected) {
                        prevSelected = newSelected;
                        listener(newSelected);
                    }
                });
            };
        }
    };
}

// SOLUTION 10:
class TextEditor {
    constructor() {
        this.text = "";
        this.history = [];
        this.undoStack = [];
    }
    
    type(str) {
        const prevText = this.text;
        this.text += str;
        this.history.push({
            execute: () => { this.text = prevText + str; },
            undo: () => { this.text = prevText; }
        });
        this.undoStack = [];
        return this;
    }
    
    delete(count) {
        const prevText = this.text;
        this.text = this.text.slice(0, -count);
        this.history.push({
            execute: () => { this.text = prevText.slice(0, -count); },
            undo: () => { this.text = prevText; }
        });
        this.undoStack = [];
        return this;
    }
    
    undo() {
        const command = this.history.pop();
        if (command) {
            command.undo();
            this.undoStack.push(command);
        }
        return this;
    }
    
    redo() {
        const command = this.undoStack.pop();
        if (command) {
            command.execute();
            this.history.push(command);
        }
        return this;
    }
    
    getText() {
        return this.text;
    }
}

// SOLUTION 11:
class Validator {
    constructor() {
        this.rules = new Map();
        
        // Built-in rules
        this.addRule("required", (value) => {
            if (!value || value.length === 0) return "This field is required";
            return null;
        });
        
        this.addRule("email", (value) => {
            if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
                return "Invalid email format";
            }
            return null;
        });
    }
    
    addRule(name, validator) {
        this.rules.set(name, validator);
        return this;
    }
    
    validate(value, rules) {
        const errors = [];
        
        for (const rule of rules) {
            if (this.rules.has(rule)) {
                const error = this.rules.get(rule)(value);
                if (error) errors.push(error);
            }
        }
        
        return { valid: errors.length === 0, errors };
    }
}

// SOLUTION 12:
function createNamespace() {
    const modules = {};
    const loaded = {};
    
    return {
        define(path, factory) {
            modules[path] = factory;
        },
        
        require(path) {
            if (!loaded[path]) {
                if (!modules[path]) {
                    throw new Error(`Module not found: ${path}`);
                }
                loaded[path] = modules[path]();
            }
            return loaded[path];
        }
    };
}

// SOLUTION 13:
class PubSub {
    constructor() {
        this.channels = new Map();
    }
    
    subscribe(channel, callback) {
        if (!this.channels.has(channel)) {
            this.channels.set(channel, []);
        }
        this.channels.get(channel).push({ callback, once: false });
        return this;
    }
    
    subscribeOnce(channel, callback) {
        if (!this.channels.has(channel)) {
            this.channels.set(channel, []);
        }
        this.channels.get(channel).push({ callback, once: true });
        return this;
    }
    
    publish(channel, data) {
        if (this.channels.has(channel)) {
            const subs = this.channels.get(channel);
            const remaining = [];
            
            for (const sub of subs) {
                sub.callback(data);
                if (!sub.once) remaining.push(sub);
            }
            
            this.channels.set(channel, remaining);
        }
        return this;
    }
    
    unsubscribe(channel, callback) {
        if (this.channels.has(channel)) {
            const subs = this.channels.get(channel);
            const index = subs.findIndex(s => s.callback === callback);
            if (index > -1) subs.splice(index, 1);
        }
        return this;
    }
    
    getChannels() {
        const result = {};
        for (const [channel, subs] of this.channels) {
            result[channel] = subs.length;
        }
        return result;
    }
}

// SOLUTION 14:
function createStateMachine(initialState, transitions) {
    let state = initialState;
    const enterCallbacks = {};
    const leaveCallbacks = {};
    
    return {
        getState() {
            return state;
        },
        
        canTransition(event) {
            return transitions[state] && transitions[state][event];
        },
        
        transition(event) {
            if (this.canTransition(event)) {
                const newState = transitions[state][event];
                
                if (leaveCallbacks[state]) {
                    for (const cb of leaveCallbacks[state]) {
                        cb();
                    }
                }
                
                state = newState;
                
                if (enterCallbacks[state]) {
                    for (const cb of enterCallbacks[state]) {
                        cb();
                    }
                }
            }
            return this;
        },
        
        onEnter(targetState, callback) {
            if (!enterCallbacks[targetState]) {
                enterCallbacks[targetState] = [];
            }
            enterCallbacks[targetState].push(callback);
            return this;
        },
        
        onLeave(targetState, callback) {
            if (!leaveCallbacks[targetState]) {
                leaveCallbacks[targetState] = [];
            }
            leaveCallbacks[targetState].push(callback);
            return this;
        }
    };
}

// SOLUTION 15:
function createApp(name, version) {
    const plugins = [];
    const data = {};
    
    const app = {
        name,
        version,
        data,
        
        registerPlugin(plugin) {
            if (plugin.name && plugin.install) {
                plugin.install(this);
                plugins.push(plugin.name);
            }
            return this;
        },
        
        listPlugins() {
            return [...plugins];
        }
    };
    
    return app;
}
*/

console.log('========================================');
console.log('Object Patterns Exercises Loaded!');
console.log('Uncomment the test code to check your solutions.');
console.log('========================================');
Exercises - JavaScript Tutorial | DeepML