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