javascript

exercises

exercises.js⚔
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ā•‘                    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                                                         ā•‘
ā•‘                                                                              ā•‘
ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•
`);
})();
Exercises - JavaScript Tutorial | DeepML