阿里云主机折上折
  • 微信号
Current Site:Index > The combination of caching strategies and design patterns

The combination of caching strategies and design patterns

Author:Chuan Chen 阅读数:39966人阅读 分类: JavaScript

The Combination of Caching Strategies and Design Patterns

The integration of caching strategies with design patterns can significantly enhance code maintainability and performance. By judiciously applying design patterns, caching logic can be elegantly implemented to reduce redundant computations or requests while maintaining a clear code structure.

Proxy Pattern for Caching Implementation

The proxy pattern is a classic approach to caching. By creating a proxy object to control access to the target object, caching logic can be added before and after access.

class RealImage {
  constructor(filename) {
    this.filename = filename;
    this.loadFromDisk();
  }

  loadFromDisk() {
    console.log(`Loading ${this.filename} from disk...`);
    // Simulate time-consuming operation
  }

  display() {
    console.log(`Displaying ${this.filename}`);
  }
}

class ProxyImage {
  constructor(filename) {
    this.filename = filename;
    this.realImage = null;
  }

  display() {
    if (!this.realImage) {
      this.realImage = new RealImage(this.filename);
    }
    this.realImage.display();
  }
}

// Using the proxy
const image = new ProxyImage("photo.jpg");
// First call creates the real object
image.display();
// Second call uses the cache directly
image.display();

This pattern is particularly suitable for resource-intensive operations, such as image loading or API requests. The proxy object acts as an intermediary layer, completely hiding the caching implementation details.

Decorator Pattern to Enhance Caching Functionality

The decorator pattern allows dynamic addition of functionality to objects, including caching capabilities. Unlike the proxy pattern, decorators emphasize dynamic composition of functionality.

function withCache(target, cacheKey) {
  const cache = new Map();
  
  return function(...args) {
    const key = cacheKey ? cacheKey(...args) : JSON.stringify(args);
    if (cache.has(key)) {
      console.log('Returning cached result');
      return cache.get(key);
    }
    
    const result = target.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

class Calculator {
  @withCache
  static fibonacci(n) {
    if (n <= 1) return n;
    return Calculator.fibonacci(n - 1) + Calculator.fibonacci(n - 2);
  }
}

// Using the decorator for caching
console.log(Calculator.fibonacci(10)); // Computes and caches
console.log(Calculator.fibonacci(10)); // Returns cached result directly

The decorator syntax allows caching functionality to be flexibly attached to any method without modifying the original class. This approach is particularly useful when caching needs to be added to multiple methods.

Strategy Pattern for Replaceable Cache Algorithms

The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern is ideal for implementing different cache eviction strategies.

class Cache {
  constructor(strategy, maxSize = 10) {
    this.cache = new Map();
    this.strategy = strategy;
    this.maxSize = maxSize;
  }

  get(key) {
    if (this.cache.has(key)) {
      this.strategy.hit(this.cache, key);
      return this.cache.get(key);
    }
    return null;
  }

  set(key, value) {
    if (this.cache.size >= this.maxSize) {
      const keyToRemove = this.strategy.evict(this.cache);
      this.cache.delete(keyToRemove);
    }
    this.cache.set(key, value);
  }
}

// LRU strategy
const lruStrategy = {
  hit(cache, key) {
    const value = cache.get(key);
    cache.delete(key);
    cache.set(key, value);
  },
  evict(cache) {
    return cache.keys().next().value;
  }
};

// FIFO strategy
const fifoStrategy = {
  hit() {},
  evict(cache) {
    return cache.keys().next().value;
  }
};

// Using different strategies
const lruCache = new Cache(lruStrategy, 3);
const fifoCache = new Cache(fifoStrategy, 3);

The strategy pattern allows cache algorithms to vary independently, with client code unaware of the specific implementations. This flexibility is especially important in systems that need to support multiple caching strategies.

Factory Pattern for Managing Cache Instances

The factory pattern helps centralize the management of cache instances, ensuring consistency in global caching strategies while hiding instantiation details.

class CacheFactory {
  static caches = new Map();

  static getCache(namespace, options = {}) {
    if (this.caches.has(namespace)) {
      return this.caches.get(namespace);
    }

    const cache = new Cache({
      maxAge: options.maxAge || 3600,
      maxSize: options.maxSize || 100
    });

    this.caches.set(namespace, cache);
    return cache;
  }

  static clear(namespace) {
    if (namespace) {
      this.caches.delete(namespace);
    } else {
      this.caches.clear();
    }
  }
}

// Using the factory to get cache instances
const userCache = CacheFactory.getCache('users', { maxSize: 50 });
const productCache = CacheFactory.getCache('products');

The factory pattern is particularly suitable for scenarios requiring unified management of multiple cache instances, such as micro-frontend architectures or module isolation in large applications.

Observer Pattern for Cache Invalidation

The observer pattern can be used to implement cache invalidation mechanisms, automatically updating related caches when data changes.

class CacheObserver {
  constructor() {
    this.observers = new Map();
  }

  subscribe(key, callback) {
    if (!this.observers.has(key)) {
      this.observers.set(key, new Set());
    }
    this.observers.get(key).add(callback);
  }

  unsubscribe(key, callback) {
    if (this.observers.has(key)) {
      this.observers.get(key).delete(callback);
    }
  }

  notify(key) {
    if (this.observers.has(key)) {
      this.observers.get(key).forEach(cb => cb());
    }
  }
}

class ObservableCache {
  constructor() {
    this.cache = new Map();
    this.observer = new CacheObserver();
  }

  get(key) {
    return this.cache.get(key);
  }

  set(key, value) {
    this.cache.set(key, value);
    this.observer.notify(key);
  }

  onInvalidate(key, callback) {
    this.observer.subscribe(key, callback);
  }
}

// Using the observer pattern
const cache = new ObservableCache();
cache.onInvalidate('userData', () => {
  console.log('userData cache invalidated');
});

cache.set('userData', { name: 'Alice' }); // Triggers notification

This pattern is highly useful in scenarios requiring synchronization between caches and data sources, such as real-time data updates or collaborative editing systems.

Singleton Pattern for Global Cache

The singleton pattern ensures a class has only one instance, making it ideal for implementing global application caches.

class GlobalCache {
  constructor() {
    if (GlobalCache.instance) {
      return GlobalCache.instance;
    }

    this.cache = new Map();
    GlobalCache.instance = this;
  }

  set(key, value) {
    this.cache.set(key, value);
  }

  get(key) {
    return this.cache.get(key);
  }

  clear() {
    this.cache.clear();
  }
}

// Using the singleton cache
const cache1 = new GlobalCache();
cache1.set('theme', 'dark');

const cache2 = new GlobalCache();
console.log(cache2.get('theme')); // 'dark'

The singleton pattern ensures the entire application shares a single cache instance, avoiding the complexity of creating and managing multiple cache instances.

Memento Pattern for Cache Snapshots

The memento pattern captures an object's internal state and restores it when needed, enabling complex cache snapshot functionality.

class Editor {
  constructor() {
    this.content = '';
  }

  type(text) {
    this.content += text;
  }

  save() {
    return new EditorMemento(this.content);
  }

  restore(memento) {
    this.content = memento.getContent();
  }

  getContent() {
    return this.content;
  }
}

class EditorMemento {
  constructor(content) {
    this.content = content;
    this.timestamp = Date.now();
  }

  getContent() {
    return this.content;
  }
}

class History {
  constructor() {
    this.states = [];
  }

  push(state) {
    this.states.push(state);
  }

  pop() {
    return this.states.pop();
  }
}

// Using the memento pattern
const editor = new Editor();
const history = new History();

editor.type('Hello');
history.push(editor.save());

editor.type(' World');
console.log(editor.getContent()); // 'Hello World'

editor.restore(history.pop());
console.log(editor.getContent()); // 'Hello'

The memento pattern is particularly useful in scenarios requiring the saving and restoring of object states, such as rich text editors or form draft saving.

Composite Pattern for Hierarchical Caching

The composite pattern builds tree-like object hierarchies, enabling the implementation of layered caching systems.

class CacheNode {
  constructor(name) {
    this.name = name;
    this.children = [];
    this.cache = new Map();
  }

  add(child) {
    this.children.push(child);
  }

  get(key) {
    if (this.cache.has(key)) {
      return this.cache.get(key);
    }

    for (const child of this.children) {
      const value = child.get(key);
      if (value !== undefined) {
        this.cache.set(key, value);
        return value;
      }
    }

    return undefined;
  }

  set(key, value) {
    this.cache.set(key, value);
  }
}

// Building a hierarchical cache
const localCache = new CacheNode('local');
const sessionCache = new CacheNode('session');
const memoryCache = new CacheNode('memory');

// Setting up the cache hierarchy
memoryCache.add(sessionCache);
sessionCache.add(localCache);

// Using the hierarchical cache
localCache.set('user', { name: 'Alice' });
console.log(memoryCache.get('user')); // Retrieves from the lower-level cache

The composite pattern enables the construction of multi-level cache systems, such as memory cache → session cache → persistent cache hierarchies, automatically handling cache penetration issues.

State Pattern for Cache Lifecycle Management

The state pattern allows an object to alter its behavior when its internal state changes, making it suitable for implementing cache lifecycle management.

class CacheItem {
  constructor(value) {
    this.value = value;
    this.state = new FreshState(this);
  }

  get() {
    return this.state.get();
  }

  expire() {
    this.state.expire();
  }

  refresh(newValue) {
    this.state.refresh(newValue);
  }
}

class CacheState {
  constructor(cacheItem) {
    this.cacheItem = cacheItem;
  }

  get() {
    throw new Error('Abstract method');
  }

  expire() {
    throw new Error('Abstract method');
  }

  refresh(newValue) {
    throw new Error('Abstract method');
  }
}

class FreshState extends CacheState {
  get() {
    return this.cacheItem.value;
  }

  expire() {
    this.cacheItem.state = new ExpiredState(this.cacheItem);
  }

  refresh(newValue) {
    this.cacheItem.value = newValue;
  }
}

class ExpiredState extends CacheState {
  get() {
    return null;
  }

  expire() {
    // Already expired
  }

  refresh(newValue) {
    this.cacheItem.value = newValue;
    this.cacheItem.state = new FreshState(this.cacheItem);
  }
}

// Using the state pattern for cache management
const item = new CacheItem({ data: 'value' });
console.log(item.get()); // Retrieves fresh value

item.expire();
console.log(item.get()); // null

item.refresh({ data: 'new value' });
console.log(item.get()); // Retrieves new value

The state pattern provides clear management of cache item lifecycles, making transitions from fresh to expired states explicit and easy to maintain.

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.