javascript
exercises
exercises.jsā”javascript
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// ā MODULE DESIGN PATTERNS - EXERCISES ā
// ā Practice Implementing Common Patterns ā
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā EXERCISE OVERVIEW ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā Exercise 1: Singleton - Create an Application State Manager ā
ā Exercise 2: Factory - Build a UI Component Factory ā
ā Exercise 3: Observer - Implement a State Store (like Redux) ā
ā Exercise 4: Dependency Injection - Create a testable service ā
ā Exercise 5: Strategy - Build a payment processing system ā
ā Exercise 6: Facade - Create an API Client Facade ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
console.log('Module Design Patterns - Exercises');
console.log('====================================\n');
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// EXERCISE 1: Singleton - Application State Manager
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Create a Singleton State Manager for application state ā
ā ā
ā Requirements: ā
ā 1. Only one instance exists across the application ā
ā 2. getState(path) - Get state by dot notation path ā
ā 3. setState(path, value) - Set state at path ā
ā 4. subscribe(callback) - Subscribe to state changes ā
ā 5. reset() - Reset to initial state ā
ā 6. History: undo() and redo() functionality ā
ā ā
ā Bonus: Implement selectors for computed state ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
// Your solution here:
class AppStateManager {
static #instance = null;
constructor() {
if (AppStateManager.#instance) {
return AppStateManager.#instance;
}
this.initialState = {};
this.state = {};
this.subscribers = new Set();
this.history = [];
this.historyIndex = -1;
this.selectors = new Map();
AppStateManager.#instance = this;
}
static getInstance() {
if (!AppStateManager.#instance) {
AppStateManager.#instance = new AppStateManager();
}
return AppStateManager.#instance;
}
// Initialize with default state
init(initialState) {
this.initialState = JSON.parse(JSON.stringify(initialState));
this.state = JSON.parse(JSON.stringify(initialState));
this.history = [JSON.parse(JSON.stringify(this.state))];
this.historyIndex = 0;
return this;
}
// Get state by path (e.g., 'user.profile.name')
getState(path = '') {
if (!path) return this.state;
return path.split('.').reduce((obj, key) => {
return obj && obj[key] !== undefined ? obj[key] : undefined;
}, this.state);
}
// Set state at path
setState(path, value) {
const oldState = JSON.parse(JSON.stringify(this.state));
if (!path) {
this.state = value;
} else {
const keys = path.split('.');
let current = this.state;
for (let i = 0; i < keys.length - 1; i++) {
if (current[keys[i]] === undefined) {
current[keys[i]] = {};
}
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
}
// Add to history
this.historyIndex++;
this.history = this.history.slice(0, this.historyIndex);
this.history.push(JSON.parse(JSON.stringify(this.state)));
// Notify subscribers
this.notify(path, oldState);
return this;
}
// Subscribe to changes
subscribe(callback) {
this.subscribers.add(callback);
return () => this.subscribers.delete(callback);
}
// Notify subscribers
notify(path, oldState) {
this.subscribers.forEach((callback) => {
callback({
path,
oldState,
newState: this.state,
value: this.getState(path),
});
});
}
// Undo last change
undo() {
if (this.historyIndex > 0) {
this.historyIndex--;
this.state = JSON.parse(JSON.stringify(this.history[this.historyIndex]));
this.notify('*', this.state);
return true;
}
return false;
}
// Redo undone change
redo() {
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++;
this.state = JSON.parse(JSON.stringify(this.history[this.historyIndex]));
this.notify('*', this.state);
return true;
}
return false;
}
// Reset to initial state
reset() {
this.state = JSON.parse(JSON.stringify(this.initialState));
this.history = [JSON.parse(JSON.stringify(this.state))];
this.historyIndex = 0;
this.notify('*', {});
return this;
}
// Register a selector for computed state
registerSelector(name, selectorFn) {
this.selectors.set(name, selectorFn);
return this;
}
// Get computed value from selector
select(name) {
const selector = this.selectors.get(name);
if (!selector) throw new Error(`Selector not found: ${name}`);
return selector(this.state);
}
// Can undo/redo
canUndo() {
return this.historyIndex > 0;
}
canRedo() {
return this.historyIndex < this.history.length - 1;
}
}
// Test Singleton State Manager
console.log('Exercise 1 - Singleton State Manager:');
const appState = AppStateManager.getInstance();
appState.init({
user: { name: 'John', logged: false },
cart: { items: [], total: 0 },
theme: 'light',
});
// Subscribe to changes
const unsub = appState.subscribe(({ path, value }) => {
console.log(` State changed at "${path}":`, value);
});
// Test state operations
appState.setState('user.logged', true);
appState.setState('theme', 'dark');
appState.setState('cart.items', [{ id: 1, name: 'Widget' }]);
console.log(' Current theme:', appState.getState('theme'));
console.log(' User logged:', appState.getState('user.logged'));
// Test undo/redo
console.log(' Undo available:', appState.canUndo());
appState.undo();
console.log(' After undo, theme:', appState.getState('theme'));
appState.redo();
console.log(' After redo, theme:', appState.getState('theme'));
// Selector
appState.registerSelector('cartItemCount', (state) => state.cart.items.length);
console.log(' Cart item count (selector):', appState.select('cartItemCount'));
unsub(); // Unsubscribe
console.log('');
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// EXERCISE 2: Factory - UI Component Factory
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Create a Factory for UI Components ā
ā ā
ā Requirements: ā
ā 1. Create button, input, select, checkbox components ā
ā 2. Each component has: render(), getValue(), setValue(), validate() ā
ā 3. Support for different variants (primary, secondary, danger) ā
ā 4. Support for validation rules ā
ā 5. Registry pattern to register custom components ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
// Your solution here:
class UIComponentFactory {
static #registry = new Map();
// Register a component type
static register(type, ComponentClass) {
this.#registry.set(type, ComponentClass);
}
// Create a component
static create(type, config = {}) {
const ComponentClass = this.#registry.get(type);
if (!ComponentClass) {
throw new Error(`Unknown component type: ${type}`);
}
return new ComponentClass(config);
}
// Get all registered types
static getTypes() {
return Array.from(this.#registry.keys());
}
}
// Base component
class UIComponent {
constructor(config = {}) {
this.id = config.id || `comp-${Date.now()}`;
this.label = config.label || '';
this.value = config.value ?? '';
this.variant = config.variant || 'default';
this.disabled = config.disabled || false;
this.validationRules = config.validation || [];
this.errors = [];
}
getValue() {
return this.value;
}
setValue(value) {
this.value = value;
return this;
}
validate() {
this.errors = [];
for (const rule of this.validationRules) {
const error = rule(this.value);
if (error) {
this.errors.push(error);
}
}
return this.errors.length === 0;
}
getErrors() {
return [...this.errors];
}
render() {
throw new Error('render() must be implemented');
}
}
// Button Component
class ButtonComponent extends UIComponent {
constructor(config = {}) {
super(config);
this.text = config.text || 'Button';
this.onClick = config.onClick || (() => {});
}
render() {
const variants = {
default: 'btn-default',
primary: 'btn-primary',
secondary: 'btn-secondary',
danger: 'btn-danger',
};
return `<button id="${this.id}" class="btn ${variants[this.variant]}" ${
this.disabled ? 'disabled' : ''
}>${this.text}</button>`;
}
click() {
if (!this.disabled) {
this.onClick();
}
}
}
// Input Component
class InputComponent extends UIComponent {
constructor(config = {}) {
super(config);
this.type = config.type || 'text';
this.placeholder = config.placeholder || '';
}
render() {
return `<div class="form-group">
${this.label ? `<label for="${this.id}">${this.label}</label>` : ''}
<input type="${this.type}" id="${this.id}" value="${
this.value
}" placeholder="${this.placeholder}" ${this.disabled ? 'disabled' : ''} />
${
this.errors.length
? `<span class="error">${this.errors[0]}</span>`
: ''
}
</div>`;
}
}
// Select Component
class SelectComponent extends UIComponent {
constructor(config = {}) {
super(config);
this.options = config.options || [];
}
render() {
const optionsHtml = this.options
.map(
(opt) =>
`<option value="${opt.value}" ${
opt.value === this.value ? 'selected' : ''
}>${opt.label}</option>`
)
.join('\n');
return `<div class="form-group">
${this.label ? `<label for="${this.id}">${this.label}</label>` : ''}
<select id="${this.id}" ${this.disabled ? 'disabled' : ''}>
${optionsHtml}
</select>
</div>`;
}
}
// Checkbox Component
class CheckboxComponent extends UIComponent {
constructor(config = {}) {
super(config);
this.checked = config.checked || false;
}
getValue() {
return this.checked;
}
setValue(checked) {
this.checked = checked;
return this;
}
toggle() {
this.checked = !this.checked;
return this;
}
render() {
return `<div class="form-check">
<input type="checkbox" id="${this.id}" ${
this.checked ? 'checked' : ''
} ${this.disabled ? 'disabled' : ''} />
${this.label ? `<label for="${this.id}">${this.label}</label>` : ''}
</div>`;
}
}
// Register components
UIComponentFactory.register('button', ButtonComponent);
UIComponentFactory.register('input', InputComponent);
UIComponentFactory.register('select', SelectComponent);
UIComponentFactory.register('checkbox', CheckboxComponent);
// Validation helpers
const Validators = {
required:
(msg = 'This field is required') =>
(value) =>
!value || (typeof value === 'string' && !value.trim()) ? msg : null,
minLength: (min, msg) => (value) =>
value && value.length < min
? msg || `Minimum ${min} characters required`
: null,
maxLength: (max, msg) => (value) =>
value && value.length > max
? msg || `Maximum ${max} characters allowed`
: null,
email:
(msg = 'Invalid email format') =>
(value) =>
value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? msg : null,
pattern:
(regex, msg = 'Invalid format') =>
(value) =>
value && !regex.test(value) ? msg : null,
};
// Test Factory
console.log('Exercise 2 - UI Component Factory:');
// Create components using factory
const submitBtn = UIComponentFactory.create('button', {
text: 'Submit',
variant: 'primary',
onClick: () => console.log(' Clicked!'),
});
const emailInput = UIComponentFactory.create('input', {
label: 'Email',
type: 'email',
placeholder: 'Enter email',
validation: [Validators.required(), Validators.email()],
});
const countrySelect = UIComponentFactory.create('select', {
label: 'Country',
value: 'us',
options: [
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'ca', label: 'Canada' },
],
});
const termsCheckbox = UIComponentFactory.create('checkbox', {
label: 'I agree to terms',
checked: false,
});
console.log(' Available types:', UIComponentFactory.getTypes().join(', '));
console.log(' Button HTML:', submitBtn.render().replace(/\s+/g, ' ').trim());
console.log(' Email value:', emailInput.getValue());
// Validate
emailInput.setValue('invalid');
const isValid = emailInput.validate();
console.log(' Email valid:', isValid, '| Errors:', emailInput.getErrors());
emailInput.setValue('test@example.com');
console.log(' Email valid (fixed):', emailInput.validate());
console.log('');
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// EXERCISE 3: Observer - State Store (Redux-like)
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Create a Redux-like State Store ā
ā ā
ā Requirements: ā
ā 1. createStore(reducer, initialState) - Create a store ā
ā 2. getState() - Get current state ā
ā 3. dispatch(action) - Dispatch an action ā
ā 4. subscribe(listener) - Subscribe to changes ā
ā 5. Middleware support ā
ā 6. combineReducers() helper ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
// Your solution here:
function createStore(reducer, initialState = {}, middlewares = []) {
let state = initialState;
let listeners = new Set();
function getState() {
return state;
}
function dispatch(action) {
// Apply middlewares
let dispatchFn = (action) => {
state = reducer(state, action);
listeners.forEach((listener) => listener());
};
// Build middleware chain
const chain = middlewares.map((middleware) =>
middleware({ getState, dispatch })
);
dispatchFn = chain.reduceRight(
(next, middleware) => middleware(next),
dispatchFn
);
return dispatchFn(action);
}
function subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
// Initialize state with INIT action
dispatch({ type: '@@INIT' });
return { getState, dispatch, subscribe };
}
// Combine multiple reducers
function combineReducers(reducers) {
return function (state = {}, action) {
const newState = {};
let hasChanged = false;
for (const [key, reducer] of Object.entries(reducers)) {
const previousStateForKey = state[key];
const nextStateForKey = reducer(previousStateForKey, action);
newState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
return hasChanged ? newState : state;
};
}
// Logger middleware
const loggerMiddleware = (store) => (next) => (action) => {
console.log(' [Logger] Dispatching:', action.type);
const result = next(action);
console.log(' [Logger] New state:', store.getState());
return result;
};
// Thunk middleware (for async actions)
const thunkMiddleware = (store) => (next) => (action) => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return next(action);
};
// Example reducers
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'SET_COUNT':
return { ...state, count: action.payload };
default:
return state;
}
};
const todosReducer = (state = { items: [] }, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
items: [
...state.items,
{ id: Date.now(), text: action.payload, done: false },
],
};
case 'TOGGLE_TODO':
return {
...state,
items: state.items.map((item) =>
item.id === action.payload ? { ...item, done: !item.done } : item
),
};
case 'REMOVE_TODO':
return {
...state,
items: state.items.filter((item) => item.id !== action.payload),
};
default:
return state;
}
};
// Action creators
const actions = {
increment: () => ({ type: 'INCREMENT' }),
decrement: () => ({ type: 'DECREMENT' }),
setCount: (count) => ({ type: 'SET_COUNT', payload: count }),
addTodo: (text) => ({ type: 'ADD_TODO', payload: text }),
toggleTodo: (id) => ({ type: 'TOGGLE_TODO', payload: id }),
// Async action (thunk)
incrementAsync: () => (dispatch, getState) => {
setTimeout(() => {
dispatch(actions.increment());
}, 100);
},
};
// Test Observer Store
console.log('Exercise 3 - Redux-like Store:');
const rootReducer = combineReducers({
counter: counterReducer,
todos: todosReducer,
});
const store = createStore(rootReducer, {}, [loggerMiddleware]);
// Subscribe
const unsubscribe = store.subscribe(() => {
// Called on every state change
});
// Dispatch actions
store.dispatch(actions.increment());
store.dispatch(actions.increment());
store.dispatch(actions.addTodo('Learn patterns'));
store.dispatch(actions.addTodo('Build project'));
const state = store.getState();
console.log(' Final count:', state.counter.count);
console.log(' Todos:', state.todos.items.map((t) => t.text).join(', '));
unsubscribe();
console.log('');
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// EXERCISE 4: Dependency Injection - Testable Service
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Create a Notification Service with Dependency Injection ā
ā ā
ā Requirements: ā
ā 1. NotificationService that uses: Logger, EmailSender, SmsSender ā
ā 2. All dependencies should be injectable ā
ā 3. Create mock implementations for testing ā
ā 4. Implement a DI container to manage dependencies ā
ā 5. Support for different notification templates ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
// Your solution here:
// DI Container
class Container {
constructor() {
this.dependencies = new Map();
this.singletons = new Map();
}
register(name, factory, isSingleton = false) {
this.dependencies.set(name, { factory, isSingleton });
return this;
}
resolve(name) {
const dep = this.dependencies.get(name);
if (!dep) throw new Error(`Dependency not found: ${name}`);
if (dep.isSingleton) {
if (!this.singletons.has(name)) {
this.singletons.set(name, dep.factory(this));
}
return this.singletons.get(name);
}
return dep.factory(this);
}
// For testing: override a dependency
override(name, factory) {
if (this.singletons.has(name)) {
this.singletons.delete(name);
}
this.dependencies.set(name, { factory, isSingleton: false });
return this;
}
}
// Interfaces / Base classes
class Logger {
log(message) {
throw new Error('Not implemented');
}
error(message) {
throw new Error('Not implemented');
}
}
class EmailSender {
send(to, subject, body) {
throw new Error('Not implemented');
}
}
class SmsSender {
send(to, message) {
throw new Error('Not implemented');
}
}
// Real implementations
class ConsoleLogger extends Logger {
log(message) {
console.log(` [LOG] ${message}`);
}
error(message) {
console.error(` [ERROR] ${message}`);
}
}
class RealEmailSender extends EmailSender {
send(to, subject, body) {
console.log(` [EMAIL] To: ${to}, Subject: ${subject}`);
return { sent: true, to, subject };
}
}
class RealSmsSender extends SmsSender {
send(to, message) {
console.log(` [SMS] To: ${to}, Message: ${message}`);
return { sent: true, to };
}
}
// Mock implementations for testing
class MockLogger extends Logger {
constructor() {
super();
this.logs = [];
}
log(message) {
this.logs.push({ type: 'log', message });
}
error(message) {
this.logs.push({ type: 'error', message });
}
}
class MockEmailSender extends EmailSender {
constructor() {
super();
this.sent = [];
}
send(to, subject, body) {
this.sent.push({ to, subject, body });
return { sent: true, to, subject };
}
}
class MockSmsSender extends SmsSender {
constructor() {
super();
this.sent = [];
}
send(to, message) {
this.sent.push({ to, message });
return { sent: true, to };
}
}
// Notification templates
const templates = {
welcome: {
email: {
subject: 'Welcome to our platform!',
body: (data) => `Hi ${data.name}, welcome aboard!`,
},
sms: (data) => `Welcome ${data.name}! Thanks for joining.`,
},
orderConfirmation: {
email: {
subject: 'Order Confirmed',
body: (data) => `Order #${data.orderId} confirmed. Total: $${data.total}`,
},
sms: (data) => `Order #${data.orderId} confirmed!`,
},
};
// Notification Service (uses DI)
class NotificationService {
constructor(logger, emailSender, smsSender) {
this.logger = logger;
this.emailSender = emailSender;
this.smsSender = smsSender;
}
notify(templateName, channel, recipient, data) {
const template = templates[templateName];
if (!template) {
this.logger.error(`Template not found: ${templateName}`);
throw new Error(`Template not found: ${templateName}`);
}
this.logger.log(`Sending ${templateName} via ${channel} to ${recipient}`);
try {
if (channel === 'email') {
const emailTemplate = template.email;
return this.emailSender.send(
recipient,
emailTemplate.subject,
emailTemplate.body(data)
);
} else if (channel === 'sms') {
return this.smsSender.send(recipient, template.sms(data));
} else {
throw new Error(`Unknown channel: ${channel}`);
}
} catch (error) {
this.logger.error(`Failed to send notification: ${error.message}`);
throw error;
}
}
notifyAll(templateName, recipient, data, channels = ['email', 'sms']) {
return channels.map((channel) =>
this.notify(templateName, channel, recipient, data)
);
}
}
// Test DI
console.log('Exercise 4 - Dependency Injection:');
// Setup production container
const prodContainer = new Container();
prodContainer
.register('logger', () => new ConsoleLogger(), true)
.register('emailSender', () => new RealEmailSender(), true)
.register('smsSender', () => new RealSmsSender(), true)
.register(
'notificationService',
(c) =>
new NotificationService(
c.resolve('logger'),
c.resolve('emailSender'),
c.resolve('smsSender')
)
);
console.log(' Production notifications:');
const notifier = prodContainer.resolve('notificationService');
notifier.notify('welcome', 'email', 'john@example.com', { name: 'John' });
notifier.notify('orderConfirmation', 'sms', '+1234567890', {
orderId: '123',
total: 99.99,
});
// Setup test container with mocks
console.log(' \n Testing with mocks:');
const testContainer = new Container();
const mockLogger = new MockLogger();
const mockEmail = new MockEmailSender();
const mockSms = new MockSmsSender();
testContainer
.register('logger', () => mockLogger)
.register('emailSender', () => mockEmail)
.register('smsSender', () => mockSms)
.register(
'notificationService',
(c) =>
new NotificationService(
c.resolve('logger'),
c.resolve('emailSender'),
c.resolve('smsSender')
)
);
const testNotifier = testContainer.resolve('notificationService');
testNotifier.notify('welcome', 'email', 'test@test.com', { name: 'Test' });
console.log(' Mock emails sent:', mockEmail.sent.length);
console.log(' Mock logs recorded:', mockLogger.logs.length);
console.log('');
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// EXERCISE 5: Strategy - Payment Processing
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Create a Payment Processing System with Strategy Pattern ā
ā ā
ā Requirements: ā
ā 1. Support multiple payment methods: Card, PayPal, Crypto, BankTransfer ā
ā 2. Each strategy has: validate(), process(), refund() ā
ā 3. PaymentProcessor that uses strategies ā
ā 4. Fees calculation based on method ā
ā 5. Transaction logging ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
// Your solution here:
// Payment Strategy interface
class PaymentStrategy {
validate(paymentData) {
throw new Error('Not implemented');
}
process(amount, paymentData) {
throw new Error('Not implemented');
}
refund(transactionId, amount) {
throw new Error('Not implemented');
}
calculateFees(amount) {
throw new Error('Not implemented');
}
getName() {
throw new Error('Not implemented');
}
}
// Credit Card Strategy
class CardPaymentStrategy extends PaymentStrategy {
getName() {
return 'Credit Card';
}
calculateFees(amount) {
return {
percentage: 2.9,
fixed: 0.3,
total: (amount * 0.029 + 0.3).toFixed(2),
};
}
validate(paymentData) {
const { cardNumber, cvv, expiry } = paymentData;
const errors = [];
if (!cardNumber || !/^\d{16}$/.test(cardNumber.replace(/\s/g, ''))) {
errors.push('Invalid card number');
}
if (!cvv || !/^\d{3,4}$/.test(cvv)) {
errors.push('Invalid CVV');
}
if (!expiry || !/^\d{2}\/\d{2}$/.test(expiry)) {
errors.push('Invalid expiry date');
}
return { valid: errors.length === 0, errors };
}
process(amount, paymentData) {
const validation = this.validate(paymentData);
if (!validation.valid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
const fees = this.calculateFees(amount);
return {
success: true,
transactionId: `CARD-${Date.now()}`,
method: this.getName(),
amount,
fees: fees.total,
total: (parseFloat(amount) + parseFloat(fees.total)).toFixed(2),
};
}
refund(transactionId, amount) {
return {
success: true,
refundId: `REFUND-${transactionId}`,
amount,
};
}
}
// PayPal Strategy
class PayPalPaymentStrategy extends PaymentStrategy {
getName() {
return 'PayPal';
}
calculateFees(amount) {
return {
percentage: 3.49,
fixed: 0.49,
total: (amount * 0.0349 + 0.49).toFixed(2),
};
}
validate(paymentData) {
const { email } = paymentData;
const valid = email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
return {
valid,
errors: valid ? [] : ['Invalid PayPal email'],
};
}
process(amount, paymentData) {
const validation = this.validate(paymentData);
if (!validation.valid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
const fees = this.calculateFees(amount);
return {
success: true,
transactionId: `PAYPAL-${Date.now()}`,
method: this.getName(),
amount,
fees: fees.total,
total: (parseFloat(amount) + parseFloat(fees.total)).toFixed(2),
};
}
refund(transactionId, amount) {
return {
success: true,
refundId: `REFUND-${transactionId}`,
amount,
};
}
}
// Crypto Strategy
class CryptoPaymentStrategy extends PaymentStrategy {
getName() {
return 'Cryptocurrency';
}
calculateFees(amount) {
return {
percentage: 1.0,
fixed: 0,
total: (amount * 0.01).toFixed(2),
};
}
validate(paymentData) {
const { walletAddress, currency } = paymentData;
const supportedCurrencies = ['BTC', 'ETH', 'USDT'];
const errors = [];
if (!walletAddress || walletAddress.length < 20) {
errors.push('Invalid wallet address');
}
if (!supportedCurrencies.includes(currency)) {
errors.push(
`Unsupported currency. Supported: ${supportedCurrencies.join(', ')}`
);
}
return { valid: errors.length === 0, errors };
}
process(amount, paymentData) {
const validation = this.validate(paymentData);
if (!validation.valid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
const fees = this.calculateFees(amount);
return {
success: true,
transactionId: `CRYPTO-${Date.now()}`,
method: this.getName(),
currency: paymentData.currency,
amount,
fees: fees.total,
total: (parseFloat(amount) + parseFloat(fees.total)).toFixed(2),
};
}
refund(transactionId, amount) {
// Crypto refunds are complex - simplified here
return {
success: true,
refundId: `REFUND-${transactionId}`,
amount,
note: 'Refund will be processed within 24 hours',
};
}
}
// Payment Processor (Context)
class PaymentProcessor {
constructor() {
this.strategies = new Map();
this.transactions = [];
}
registerStrategy(name, strategy) {
this.strategies.set(name, strategy);
return this;
}
getStrategy(name) {
const strategy = this.strategies.get(name);
if (!strategy) {
throw new Error(`Unknown payment method: ${name}`);
}
return strategy;
}
getAvailableMethods() {
return Array.from(this.strategies.keys());
}
calculateFees(method, amount) {
return this.getStrategy(method).calculateFees(amount);
}
validate(method, paymentData) {
return this.getStrategy(method).validate(paymentData);
}
process(method, amount, paymentData) {
const strategy = this.getStrategy(method);
const result = strategy.process(amount, paymentData);
// Log transaction
this.transactions.push({
...result,
timestamp: new Date(),
});
return result;
}
refund(transactionId) {
const transaction = this.transactions.find(
(t) => t.transactionId === transactionId
);
if (!transaction) {
throw new Error(`Transaction not found: ${transactionId}`);
}
const methodName = transaction.method.toLowerCase().replace(' ', '');
const strategy = this.strategies.get(methodName);
return strategy.refund(transactionId, transaction.amount);
}
getTransactionHistory() {
return [...this.transactions];
}
}
// Test Strategy Pattern
console.log('Exercise 5 - Payment Strategy Pattern:');
const paymentProcessor = new PaymentProcessor();
paymentProcessor
.registerStrategy('card', new CardPaymentStrategy())
.registerStrategy('paypal', new PayPalPaymentStrategy())
.registerStrategy('crypto', new CryptoPaymentStrategy());
console.log(
' Available methods:',
paymentProcessor.getAvailableMethods().join(', ')
);
// Compare fees
console.log(' Fee comparison for $100:');
['card', 'paypal', 'crypto'].forEach((method) => {
const fees = paymentProcessor.calculateFees(method, 100);
console.log(
` ${method}: $${fees.total} (${fees.percentage}% + $${fees.fixed || 0})`
);
});
// Process payments
console.log(' Processing payments:');
const cardResult = paymentProcessor.process('card', 50, {
cardNumber: '4111111111111111',
cvv: '123',
expiry: '12/25',
});
console.log(
` Card: $${cardResult.amount} + $${cardResult.fees} fees = $${cardResult.total}`
);
const paypalResult = paymentProcessor.process('paypal', 75, {
email: 'customer@example.com',
});
console.log(
` PayPal: $${paypalResult.amount} + $${paypalResult.fees} fees = $${paypalResult.total}`
);
console.log(' Transactions:', paymentProcessor.getTransactionHistory().length);
console.log('');
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// EXERCISE 6: Facade - API Client
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
/*
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Create an API Client Facade ā
ā ā
ā Requirements: ā
ā 1. Wrap complex HTTP operations behind simple interface ā
ā 2. Handle: authentication, retries, caching, error handling ā
ā 3. Provide simple methods: users.get(), products.list(), etc. ā
ā 4. Interceptors for request/response ā
ā 5. Rate limiting ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*/
// Your solution here:
// Low-level HTTP client (complex subsystem)
class HttpClient {
async request(method, url, options = {}) {
// Simulate HTTP request
console.log(` HTTP ${method} ${url}`);
return { status: 200, data: options.mockData || {} };
}
}
// Cache subsystem
class CacheManager {
constructor(ttl = 60000) {
this.cache = new Map();
this.ttl = ttl;
}
get(key) {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expiry) {
this.cache.delete(key);
return null;
}
return item.data;
}
set(key, data, ttl = this.ttl) {
this.cache.set(key, {
data,
expiry: Date.now() + ttl,
});
}
clear() {
this.cache.clear();
}
}
// Auth subsystem
class AuthManager {
constructor() {
this.token = null;
this.refreshToken = null;
}
setTokens(token, refreshToken) {
this.token = token;
this.refreshToken = refreshToken;
}
getAuthHeader() {
return this.token ? { Authorization: `Bearer ${this.token}` } : {};
}
isAuthenticated() {
return !!this.token;
}
logout() {
this.token = null;
this.refreshToken = null;
}
}
// Retry subsystem
class RetryHandler {
constructor(maxRetries = 3, baseDelay = 1000) {
this.maxRetries = maxRetries;
this.baseDelay = baseDelay;
}
async execute(fn) {
let lastError;
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt < this.maxRetries - 1) {
const delay = this.baseDelay * Math.pow(2, attempt);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
}
// Rate limiter subsystem
class RateLimiter {
constructor(maxRequests = 10, windowMs = 1000) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = [];
}
async acquire() {
const now = Date.now();
this.requests = this.requests.filter((time) => now - time < this.windowMs);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = this.requests[0];
const waitTime = this.windowMs - (now - oldestRequest);
await new Promise((resolve) => setTimeout(resolve, waitTime));
return this.acquire();
}
this.requests.push(now);
return true;
}
}
// API Client Facade
class ApiClient {
constructor(baseUrl, options = {}) {
this.baseUrl = baseUrl;
// Initialize subsystems
this.http = new HttpClient();
this.cache = new CacheManager(options.cacheTtl);
this.auth = new AuthManager();
this.retry = new RetryHandler(options.maxRetries);
this.rateLimiter = new RateLimiter(options.rateLimit);
// Interceptors
this.requestInterceptors = [];
this.responseInterceptors = [];
}
// Add interceptors
onRequest(fn) {
this.requestInterceptors.push(fn);
return this;
}
onResponse(fn) {
this.responseInterceptors.push(fn);
return this;
}
// Core request method (simplified for demo)
async request(method, endpoint, options = {}) {
await this.rateLimiter.acquire();
// Check cache for GET requests
const cacheKey = `${method}:${endpoint}`;
if (method === 'GET' && options.cache !== false) {
const cached = this.cache.get(cacheKey);
if (cached) {
console.log(` [CACHE HIT] ${endpoint}`);
return cached;
}
}
// Build request config
let config = {
method,
url: `${this.baseUrl}${endpoint}`,
headers: {
...this.auth.getAuthHeader(),
...options.headers,
},
...options,
};
// Run request interceptors
for (const interceptor of this.requestInterceptors) {
config = interceptor(config);
}
// Make request with retry
let response = await this.retry.execute(() =>
this.http.request(method, config.url, config)
);
// Run response interceptors
for (const interceptor of this.responseInterceptors) {
response = interceptor(response);
}
// Cache GET responses
if (method === 'GET' && options.cache !== false) {
this.cache.set(cacheKey, response.data);
}
return response.data;
}
// Simple interface methods
get users() {
return {
get: (id) =>
this.request('GET', `/users/${id}`, {
mockData: { id, name: `User ${id}`, email: `user${id}@example.com` },
}),
list: () =>
this.request('GET', '/users', {
mockData: [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
],
}),
create: (data) => this.request('POST', '/users', { body: data }),
update: (id, data) => this.request('PUT', `/users/${id}`, { body: data }),
delete: (id) => this.request('DELETE', `/users/${id}`),
};
}
get products() {
return {
get: (id) =>
this.request('GET', `/products/${id}`, {
mockData: { id, name: `Product ${id}`, price: 99.99 },
}),
list: (params) =>
this.request('GET', '/products', {
mockData: [
{ id: 1, name: 'Widget' },
{ id: 2, name: 'Gadget' },
],
}),
search: (query) => this.request('GET', `/products/search?q=${query}`),
};
}
// Auth methods
login(email, password) {
return this.request('POST', '/auth/login', {
body: { email, password },
mockData: { token: 'abc123', refreshToken: 'xyz789' },
}).then((data) => {
this.auth.setTokens(data.token, data.refreshToken);
return data;
});
}
logout() {
this.auth.logout();
this.cache.clear();
}
}
// Test Facade
console.log('Exercise 6 - API Client Facade:');
const api = new ApiClient('https://api.example.com', {
cacheTtl: 30000,
maxRetries: 3,
rateLimit: 100,
});
// Add interceptors
api.onRequest((config) => {
console.log(` [Interceptor] Request to ${config.url}`);
return config;
});
// Use simple interface
(async () => {
// Get user
const user = await api.users.get(1);
console.log(' User:', user);
// Get products
const products = await api.products.list();
console.log(' Products:', products.length);
// Login
const authResult = await api.login('test@example.com', 'password');
console.log(' Logged in:', !!authResult.token);
console.log(`
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā MODULE DESIGN PATTERNS - EXERCISES COMPLETE ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā£
ā ā
ā Completed Exercises: ā
ā ā Exercise 1: Singleton State Manager ā
ā ā Exercise 2: Factory UI Components ā
ā ā Exercise 3: Observer Redux-like Store ā
ā ā Exercise 4: Dependency Injection Services ā
ā ā Exercise 5: Strategy Payment Processing ā
ā ā Exercise 6: Facade API Client ā
ā ā
ā Key Takeaways: ā
ā ⢠Singleton: Use for shared state/resources ā
ā ⢠Factory: Decouple object creation from usage ā
ā ⢠Observer: Enable loose coupling with events ā
ā ⢠DI: Make code testable by injecting dependencies ā
ā ⢠Strategy: Make algorithms interchangeable ā
ā ⢠Facade: Simplify complex subsystems ā
ā ā
ā These patterns make code: ā
ā ⢠More maintainable ā
ā ⢠Easier to test ā
ā ⢠More flexible ā
ā ⢠Better organized ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
`);
})();