// Patterns already in YOUR code:
Observer:
addEventListener, useEffect subs,
WebSocket events, EventEmitter
Command:
useReducer actions:
{ type: 'SET_FILTER', payload: {...} }
Undo stack = command history
Factory:
createFilterSlider(name, min, max)
apiGet<T>(path) — typed API factory
Singleton:
Single DB connection
Single Redis client, single logger
Strategy:
Cache: cache-first vs network-first
Filters: different algorithms,
same interface
Middleware:
Express: logger → auth → validate
→ handler → errorHandler
Pipeline:
Filter pipeline: original
→ brightness → contrast → sepia
class EventEmitter<
T extends Record<string, any>
> {
private listeners =
new Map<keyof T, Set<Function>>();
on<K extends keyof T>(
event: K,
callback: (data: T[K]) => void
) {
if (!this.listeners.has(event))
this.listeners.set(event,
new Set());
this.listeners.get(event)!
.add(callback);
// Return unsubscribe function
return () =>
this.listeners.get(event)!
.delete(callback);
}
emit<K extends keyof T>(
event: K, data: T[K]
) {
this.listeners.get(event)
?.forEach(cb => cb(data));
}
}
// Usage — fully typed:
type EditorEvents = {
'filter-changed': {
name: string; value: number };
'image-loaded': {
width: number; height: number };
'export-complete': {
filename: string };
};
const bus =
new EventEmitter<EditorEvents>();
bus.on('filter-changed',
({ name, value }) => {
// TS knows: name is string,
// value is number
console.log(name, value);
});
bus.emit('filter-changed',
{ name: 'brightness', value: 50 });
// Strategy: same interface,
// swappable behavior
interface FilterStrategy {
name: string;
apply(
imageData: ImageData
): ImageData;
}
const sepiaFilter: FilterStrategy = {
name: 'sepia',
apply(imageData) {
return applyColorMatrix(
imageData, SEPIA_MATRIX);
},
};
const blurFilter: FilterStrategy = {
name: 'blur',
apply(imageData) {
return applyConvolution(
imageData, BLUR_KERNEL);
},
};
// The editor doesn't care WHICH
// filter — it just calls .apply()
function applyFilter(
image: ImageData,
strategy: FilterStrategy
): ImageData {
return strategy.apply(image);
}
// Swap strategy at runtime:
applyFilter(image, sepiaFilter);
applyFilter(image, blurFilter);
// ❌ OVER-ENGINEERING:
// Using a Factory + Strategy + Observer
// + Command pattern for a simple
// utility function
// When in doubt, use a function.
// ❌ Singleton abuse:
// Making everything global.
// Only singleton what truly needs
// to be shared (DB connection, logger).
// ❌ Pattern matching:
// "I need to use Observer here"
// → Wrong. Start with the problem,
// discover the pattern fits.
// ✅ The right approach:
// 1. Write simple code
// 2. Notice a recurring problem
// 3. Recognize a pattern applies
// 4. Apply it
// NOT: "which pattern should I use?"
git switch -c refactor/PIXELCRAFT-085-design-patterns
git add src/ docs/patterns.md
git commit -m "Document and formalize design patterns (PIXELCRAFT-085)"
git push origin refactor/PIXELCRAFT-085-design-patterns
# PR → Review → Merge → Close ticket ✅
The "Gang of Four" cataloged 23 design patterns in 1994.
These are solutions to recurring software design problems. They transcend languages. The real skill isn't memorizing patterns — it's recognizing when a problem matches one.