The combination of caching strategies and design patterns
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
上一篇:懒加载与预加载的模式选择
下一篇:算法复杂度与设计模式的关系