Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Core Design Patterns in Modern Frontend Development

Tech 2

Design patterns provide standardized solutions to recurring architectural challenges in client-side applications. By abstracting comon interaction flows into predictable structures, developers can improve code maintainability, testability, and scalability. The following sections detail four foundational patterns frequently applied in JavaScript environments.

Singleton Architecture

Ensuring a single shared instance across an application prevents redundant resource allocation and maintains consistent global state. This is commonly achieved through module scoping or explicit instantiation control.

class EventBus {
  #instance;
  constructor() {
    if (this.#instance) return this.#instance;
    this.listeners = new Map();
  }

  subscribe(event, callback) {
    if (!this.listeners.has(event)) this.listeners.set(event, []);
    const callbacks = this.listeners.get(event);
    callbacks.push(callback);
  }

  publish(event, payload) {
    const callbacks = this.listeners.get(event);
    if (callbacks) callbacks.forEach(fn => fn(payload));
  }

  static getSharedInstance() {
    return new EventBus();
  }
}

const busA = EventBus.getSharedInstance();
const busB = EventBus.getSharedInstance();

console.assert(busA === busB, 'Instances are identical');

Pub/Sub Communication

Decoupling components requires a communication channel where producers emit signals without knowing the consumers, and subscribers react asynchronously. This decouples lifecycle dependencies and simplifies cross-module updates.

class MediatorChannel {
  constructor() {
    this.channels = new Map();
  }

  register(topic, handler) {
    const handlers = this.channels.get(topic) || new Set();
    handlers.add(handler);
    this.channels.set(topic, handlers);
    return () => this.unregister(topic, handler);
  }

  unregister(topic, handler) {
    const handlers = this.channels.get(topic);
    if (handlers) handlers.delete(handler);
  }

  dispatch(topic, data) {
    const handlers = this.channels.get(topic);
    handlers?.forEach(h => h(data));
  }
}

const channel = new MediatorChannel();
const cleanup = channel.register('user:update', (userData) => {
  console.log(`Processed update: ${JSON.stringify(userData)}`);
});

channel.dispatch('user:update', { id: 42, status: 'active' });
cleanup(); // Removes listener

Algorithmic Strategy Switching

Isolating algorithm implementations allows dynamic behavior selection at runtime without modifying core logic. This reduces conditional branching and adheres to the Open/Closed Principle.

const SortingStrategies = {
  ascNumeric: (data) => [...data].sort((a, b) => a - b),
  descAlpha: (data) => [...data].sort((a, b) => b.localeCompare(a)),
  customPriority: (data) => [...data].sort((a, b) => a.priority - b.priority)
};

function executeSort(type, dataset) {
  const algorithm = SortingStrategies[type];
  if (!algorithm) throw new Error(`Unsupported sort type: ${type}`);
  return algorithm(dataset);
}

console.log(executeSort('ascNumeric', [9, 1, 5]));
console.log(executeSort('descAlpha', ['banana', 'apple']));

Higher-Order Function Decorators

Wrapping original functions enables cross-cutting concerns like validation, logging, or caching to be applied without altering base implementations. Functional decorators promote composition over inheritance.

function cacheResult(ttlMs) {
  const memoryCache = new Map();

  return function(target, methodName, descriptor) {
    const original = descriptor.value;
    
    descriptor.value = async function(...args) {
      const key = `${methodName}:${JSON.stringify(args)}`;
      const cached = memoryCache.get(key);

      if (cached && Date.now() < cached.expiry) {
        return cached.result;
      }

      const result = await original.apply(this, args);
      memoryCache.set(key, { result, expiry: Date.now() + ttlMs });
      return result;
    };
  };
}

class DataService {
  @cacheResult(5000)
  async fetchRemoteData(query) {
    // Simulated network request
    return { data: `Response for ${query}`, timestamp: Date.now() };
  }
}

Architectural Considerations

Applying these patterns requires careful evaluation of project scale and team expertise. Introducing abstraction layers premature increases cognitive load and complicates debugging workflows. Prioritize patterns that address specific coupling, state synchronization, or algorithmic complexity issues within the current codebase. Regularly review pattern usage against performance metrics and bundle size constraints to ensure they deliver measurable maintainability improvements rather than unnecessary overhead.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.