Comprehensive GoF design patterns analyzer with stack-aware suggestions. **Features**: - Detects 23 GoF patterns (Creational, Structural, Behavioral) - Stack detection (React, Angular, NestJS, Vue, Express, RxJS, Redux, ORMs) - Code smell detection with pattern suggestions - Quality evaluation (5 criteria scoring) - Prefers stack-native alternatives (e.g., React Context over Singleton) **Structure**: - 9 files: SKILL.md + reference docs + detection rules + evaluation checklists - 3 operating modes: Detection, Suggestion, Evaluation - Pattern-specific documentation for all 23 GoF patterns **Documentation**: - Added comprehensive example in guide section 5.4 - Updated examples/README.md with skill entry - Updated template count: 65 → 66 **Use cases**: - Analyze existing patterns in codebase - Suggest refactoring with stack-native patterns - Evaluate pattern implementation quality Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
22 KiB
Structural Design Patterns
Patterns that deal with object composition and relationships between entities, providing ways to assemble objects and classes into larger structures.
Adapter
Definition
Converts the interface of a class into another interface clients expect, allowing incompatible interfaces to work together.
When to Use
- Want to use an existing class with an incompatible interface
- Need to integrate third-party libraries with different interfaces
- Want to create a reusable class that cooperates with unrelated classes
- Legacy code must work with new systems
TypeScript Signature
// Target interface (what client expects)
interface Target {
request(): string;
}
// Adaptee (existing incompatible class)
class Adaptee {
specificRequest(): string {
return '.eetpadA eht fo roivaheb laicepS';
}
}
// Adapter (makes Adaptee compatible with Target)
class Adapter implements Target {
private adaptee: Adaptee;
constructor(adaptee: Adaptee) {
this.adaptee = adaptee;
}
public request(): string {
const result = this.adaptee.specificRequest().split('').reverse().join('');
return `Adapter: ${result}`;
}
}
// Client code
function clientCode(target: Target) {
console.log(target.request());
}
// Usage
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
clientCode(adapter);
Real-World Example: Third-Party Library Integration
// Third-party library (can't modify)
class XMLDataProvider {
getXMLData(): string {
return '<data><item>1</item></data>';
}
}
// Your application expects JSON
interface JSONDataProvider {
getJSONData(): object;
}
// Adapter
class XMLToJSONAdapter implements JSONDataProvider {
constructor(private xmlProvider: XMLDataProvider) {}
getJSONData(): object {
const xml = this.xmlProvider.getXMLData();
// Convert XML to JSON (simplified)
return { data: { item: '1' } };
}
}
// Usage
const xmlProvider = new XMLDataProvider();
const adapter = new XMLToJSONAdapter(xmlProvider);
const data = adapter.getJSONData();
Detection Markers
- Class implements target interface
- Holds reference to adaptee
- Delegates to adaptee with interface conversion
- Names like
*Adapter,*Wrapper
Code Smells It Fixes
- Incompatible interfaces: Makes legacy or third-party code compatible
- Interface proliferation: Single adapter vs modifying multiple client calls
Common Mistakes
- Two-way adapters: Bidirectional conversion is complex; create two adapters
- Adapter chains: Multiple adapters in sequence indicate design issues
- Overusing for new code: Design compatible interfaces from the start
Bridge
Definition
Decouples an abstraction from its implementation so the two can vary independently.
When to Use
- Want to avoid permanent binding between abstraction and implementation
- Both abstractions and implementations should be extensible by subclassing
- Changes in implementation shouldn't affect clients
- Want to share implementation among multiple objects (Flyweight-like)
TypeScript Signature
// Implementation interface
interface Implementation {
operationImpl(): string;
}
// Concrete implementations
class ConcreteImplementationA implements Implementation {
operationImpl(): string {
return 'ConcreteImplementationA';
}
}
class ConcreteImplementationB implements Implementation {
operationImpl(): string {
return 'ConcreteImplementationB';
}
}
// Abstraction
class Abstraction {
constructor(protected implementation: Implementation) {}
public operation(): string {
return `Abstraction: ${this.implementation.operationImpl()}`;
}
}
// Refined abstraction
class ExtendedAbstraction extends Abstraction {
public operation(): string {
return `ExtendedAbstraction: ${this.implementation.operationImpl()}`;
}
}
// Usage
const implA = new ConcreteImplementationA();
const abstraction1 = new Abstraction(implA);
console.log(abstraction1.operation());
const implB = new ConcreteImplementationB();
const abstraction2 = new ExtendedAbstraction(implB);
console.log(abstraction2.operation());
Real-World Example: UI Components with Multiple Renderers
// Implementation: Renderers
interface Renderer {
renderCircle(radius: number): string;
renderSquare(side: number): string;
}
class VectorRenderer implements Renderer {
renderCircle(radius: number): string {
return `Drawing circle (vector) with radius ${radius}`;
}
renderSquare(side: number): string {
return `Drawing square (vector) with side ${side}`;
}
}
class RasterRenderer implements Renderer {
renderCircle(radius: number): string {
return `Drawing circle (pixels) with radius ${radius}`;
}
renderSquare(side: number): string {
return `Drawing square (pixels) with side ${side}`;
}
}
// Abstraction: Shapes
abstract class Shape {
constructor(protected renderer: Renderer) {}
abstract draw(): string;
}
class Circle extends Shape {
constructor(renderer: Renderer, private radius: number) {
super(renderer);
}
draw(): string {
return this.renderer.renderCircle(this.radius);
}
}
class Square extends Shape {
constructor(renderer: Renderer, private side: number) {
super(renderer);
}
draw(): string {
return this.renderer.renderSquare(this.side);
}
}
// Usage: Can mix any shape with any renderer
const vectorCircle = new Circle(new VectorRenderer(), 5);
const rasterSquare = new Square(new RasterRenderer(), 10);
Detection Markers
- Abstraction holds reference to implementation interface
- Constructor injects implementation
- Two parallel hierarchies (abstraction and implementation)
Common Mistakes
- Confusion with Adapter: Bridge is design-time; Adapter is runtime fix
- Over-engineering simple scenarios: Use only when both hierarchies need to vary
Composite
Definition
Composes objects into tree structures to represent part-whole hierarchies, letting clients treat individual objects and compositions uniformly.
When to Use
- Want to represent part-whole hierarchies of objects
- Want clients to ignore difference between compositions and individual objects
- Tree structures are natural for the domain (file systems, UI components, org charts)
TypeScript Signature
// Component interface
interface Component {
operation(): string;
add?(component: Component): void;
remove?(component: Component): void;
getChild?(index: number): Component;
}
// Leaf (no children)
class Leaf implements Component {
constructor(private name: string) {}
operation(): string {
return this.name;
}
}
// Composite (has children)
class Composite implements Component {
private children: Component[] = [];
constructor(private name: string) {}
add(component: Component): void {
this.children.push(component);
}
remove(component: Component): void {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
}
}
getChild(index: number): Component {
return this.children[index];
}
operation(): string {
const results = this.children.map(child => child.operation());
return `${this.name}(${results.join(', ')})`;
}
}
// Usage
const tree = new Composite('root');
const branch1 = new Composite('branch1');
branch1.add(new Leaf('leaf1'));
branch1.add(new Leaf('leaf2'));
const branch2 = new Composite('branch2');
branch2.add(new Leaf('leaf3'));
tree.add(branch1);
tree.add(branch2);
tree.add(new Leaf('leaf4'));
console.log(tree.operation());
// Output: root(branch1(leaf1, leaf2), branch2(leaf3), leaf4)
Real-World Example: File System
interface FileSystemComponent {
getName(): string;
getSize(): number;
print(indent: string): void;
}
class File implements FileSystemComponent {
constructor(private name: string, private size: number) {}
getName(): string {
return this.name;
}
getSize(): number {
return this.size;
}
print(indent: string): void {
console.log(`${indent}📄 ${this.name} (${this.size} bytes)`);
}
}
class Directory implements FileSystemComponent {
private children: FileSystemComponent[] = [];
constructor(private name: string) {}
add(component: FileSystemComponent): void {
this.children.push(component);
}
getName(): string {
return this.name;
}
getSize(): number {
return this.children.reduce((sum, child) => sum + child.getSize(), 0);
}
print(indent: string): void {
console.log(`${indent}📁 ${this.name} (${this.getSize()} bytes)`);
this.children.forEach(child => child.print(indent + ' '));
}
}
// Usage
const root = new Directory('root');
const home = new Directory('home');
home.add(new File('photo.jpg', 2048));
home.add(new File('document.pdf', 4096));
const work = new Directory('work');
work.add(new File('report.docx', 8192));
root.add(home);
root.add(work);
root.print('');
Detection Markers
- Tree structure with uniform interface
- Collection of children components
add(),remove(),getChild()methods- Recursive operation calls
Code Smells It Fixes
- Type checking for composition vs leaf: Uniform interface eliminates
instanceofchecks - Different handling for parts vs wholes: Clients treat both uniformly
Common Mistakes
- Violating uniformity: Leaf and Composite should have same interface
- Incorrect child management: Not handling removal properly
- Deep recursion: Can cause stack overflow on very deep trees
Decorator
Definition
Attaches additional responsibilities to an object dynamically, providing a flexible alternative to subclassing for extending functionality.
When to Use
- Need to add responsibilities to objects dynamically and transparently
- Responsibilities can be withdrawn
- Extension by subclassing is impractical (many possible combinations)
- Want to add features incrementally
TypeScript Signature
// Component interface
interface Component {
operation(): string;
}
// Concrete component
class ConcreteComponent implements Component {
operation(): string {
return 'ConcreteComponent';
}
}
// Base decorator
abstract class Decorator implements Component {
constructor(protected component: Component) {}
operation(): string {
return this.component.operation();
}
}
// Concrete decorators
class DecoratorA extends Decorator {
operation(): string {
return `DecoratorA(${super.operation()})`;
}
}
class DecoratorB extends Decorator {
operation(): string {
return `DecoratorB(${super.operation()})`;
}
}
// Usage: Stack decorators
const simple = new ConcreteComponent();
const decorated1 = new DecoratorA(simple);
const decorated2 = new DecoratorB(decorated1);
console.log(decorated2.operation());
// Output: DecoratorB(DecoratorA(ConcreteComponent))
Stack-Native Alternatives
React - Higher-Order Components:
// HOC decorator
function withAuth<P extends object>(
Component: React.ComponentType<P>
): React.ComponentType<P> {
return (props: P) => {
const { user } = useAuth();
if (!user) return <Redirect to="/login" />;
return <Component {...props} />;
};
}
// Usage: Stack decorators
const AuthenticatedProfile = withAuth(Profile);
const AuthenticatedAdminProfile = withLogging(withAuth(Profile));
NestJS - Interceptors:
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
return next.handle().pipe(
tap(() => console.log('After...'))
);
}
}
// Apply decorator
@UseInterceptors(LoggingInterceptor)
@Controller('users')
export class UsersController {}
Detection Markers
- Implements same interface as wrapped object
- Holds reference to wrapped object
- Delegates to wrapped, adding behavior
- Can be stacked
Code Smells It Fixes
- Class explosion: Avoid creating subclass for every feature combination
- Rigid feature addition: Add/remove features dynamically
Common Mistakes
- Order dependency: DecoratorA(DecoratorB(x)) ≠ DecoratorB(DecoratorA(x))
- Decorator explosion: Too many small decorators can be hard to manage
- Breaking interface: Decorator must maintain interface contract
Facade
Definition
Provides a unified interface to a set of interfaces in a subsystem, making the subsystem easier to use.
When to Use
- Want to provide a simple interface to a complex subsystem
- Many dependencies exist between clients and implementation classes
- Want to layer subsystems
- Need to decouple subsystem from clients
TypeScript Signature
// Complex subsystem classes
class SubsystemA {
operationA(): string {
return 'SubsystemA';
}
}
class SubsystemB {
operationB(): string {
return 'SubsystemB';
}
}
class SubsystemC {
operationC(): string {
return 'SubsystemC';
}
}
// Facade
class Facade {
private subsystemA: SubsystemA;
private subsystemB: SubsystemB;
private subsystemC: SubsystemC;
constructor() {
this.subsystemA = new SubsystemA();
this.subsystemB = new SubsystemB();
this.subsystemC = new SubsystemC();
}
// Simplified interface
public simpleOperation(): string {
const resultA = this.subsystemA.operationA();
const resultB = this.subsystemB.operationB();
const resultC = this.subsystemC.operationC();
return `Facade coordinates: ${resultA}, ${resultB}, ${resultC}`;
}
}
// Client code
const facade = new Facade();
console.log(facade.simpleOperation());
// Instead of:
// const a = new SubsystemA(); const b = new SubsystemB(); const c = new SubsystemC();
// a.operationA(); b.operationB(); c.operationC();
Real-World Example: Payment Processing
// Complex subsystems
class PaymentValidator {
validate(amount: number, card: string): boolean {
// Complex validation logic
return amount > 0 && card.length === 16;
}
}
class PaymentGateway {
charge(amount: number, card: string): string {
return `Charged $${amount} to ${card}`;
}
}
class NotificationService {
sendReceipt(email: string, transactionId: string): void {
console.log(`Receipt sent to ${email}: ${transactionId}`);
}
}
class TransactionLogger {
log(transaction: string): void {
console.log(`Logged: ${transaction}`);
}
}
// Facade
class PaymentFacade {
private validator = new PaymentValidator();
private gateway = new PaymentGateway();
private notifications = new NotificationService();
private logger = new TransactionLogger();
processPayment(amount: number, card: string, email: string): boolean {
// Simplified interface for complex process
if (!this.validator.validate(amount, card)) {
return false;
}
const result = this.gateway.charge(amount, card);
this.logger.log(result);
this.notifications.sendReceipt(email, result);
return true;
}
}
// Client code (simple!)
const payment = new PaymentFacade();
payment.processPayment(100, '1234567890123456', 'user@example.com');
Detection Markers
- Class with multiple subsystem dependencies
- Simple public methods coordinating subsystems
- Named
*Facade,*API,*Service
Code Smells It Fixes
- Complex subsystem usage: Clients don't need to know subsystem details
- Tight coupling: Clients depend on facade, not many classes
Common Mistakes
- God Facade: Facade does too much; should coordinate, not contain logic
- Leaky abstraction: Exposing subsystem details defeats the purpose
Flyweight
Definition
Uses sharing to support large numbers of fine-grained objects efficiently by storing shared state externally.
When to Use
- Application uses large number of objects
- Storage cost is high due to object quantity
- Most object state can be made extrinsic (externalized)
- Many groups of objects may be replaced by relatively few shared objects
TypeScript Signature
// Flyweight
class Flyweight {
constructor(private sharedState: string) {}
operation(uniqueState: string): void {
console.log(`Flyweight: Shared (${this.sharedState}) and unique (${uniqueState}) state.`);
}
}
// Flyweight factory
class FlyweightFactory {
private flyweights: Map<string, Flyweight> = new Map();
constructor(initialFlyweights: string[][]) {
for (const state of initialFlyweights) {
this.flyweights.set(this.getKey(state), new Flyweight(state.join('_')));
}
}
private getKey(state: string[]): string {
return state.join('_');
}
getFlyweight(sharedState: string[]): Flyweight {
const key = this.getKey(sharedState);
if (!this.flyweights.has(key)) {
console.log('Creating new flyweight');
this.flyweights.set(key, new Flyweight(key));
} else {
console.log('Reusing existing flyweight');
}
return this.flyweights.get(key)!;
}
listFlyweights(): void {
console.log(`FlyweightFactory: ${this.flyweights.size} flyweights:`);
for (const key of this.flyweights.keys()) {
console.log(key);
}
}
}
// Usage
const factory = new FlyweightFactory([
['Chevrolet', 'Camaro2018', 'pink'],
['Mercedes Benz', 'C300', 'black'],
]);
const flyweight1 = factory.getFlyweight(['Chevrolet', 'Camaro2018', 'pink']);
flyweight1.operation('license-123');
const flyweight2 = factory.getFlyweight(['Chevrolet', 'Camaro2018', 'pink']);
flyweight2.operation('license-456'); // Reuses same flyweight
Real-World Example: Text Editor Characters
// Flyweight: Character formatting (shared)
class CharacterFormat {
constructor(
public font: string,
public size: number,
public color: string
) {}
}
// Flyweight factory
class FormatFactory {
private formats = new Map<string, CharacterFormat>();
getFormat(font: string, size: number, color: string): CharacterFormat {
const key = `${font}_${size}_${color}`;
if (!this.formats.has(key)) {
this.formats.set(key, new CharacterFormat(font, size, color));
}
return this.formats.get(key)!;
}
}
// Character with extrinsic state
class Character {
constructor(
private char: string,
private format: CharacterFormat // Shared flyweight
) {}
render(position: number): string {
return `'${this.char}' at ${position} (${this.format.font}, ${this.format.size}px, ${this.format.color})`;
}
}
// Document
const formatFactory = new FormatFactory();
const arial12Black = formatFactory.getFormat('Arial', 12, 'black');
const arial12Red = formatFactory.getFormat('Arial', 12, 'red');
// 10,000 characters, but only 2 format objects
const characters: Character[] = [];
for (let i = 0; i < 10000; i++) {
const format = i % 2 === 0 ? arial12Black : arial12Red;
characters.push(new Character('A', format));
}
Detection Markers
- Factory managing pool of shared objects
- Intrinsic (shared) vs extrinsic (unique) state separation
- Map/cache of flyweights
Common Mistakes
- Premature optimization: Only use if memory is actually a problem
- Incorrect state separation: Mixing intrinsic and extrinsic state
Proxy
Definition
Provides a surrogate or placeholder for another object to control access to it.
When to Use
- Lazy initialization (virtual proxy): Create expensive object only when needed
- Access control (protection proxy): Control access to original object
- Local representative of remote object (remote proxy)
- Logging, caching, or monitoring access
TypeScript Signature
// Subject interface
interface Subject {
request(): void;
}
// Real subject
class RealSubject implements Subject {
request(): void {
console.log('RealSubject: Handling request');
}
}
// Proxy
class Proxy implements Subject {
private realSubject: RealSubject | null = null;
request(): void {
// Access control
if (this.checkAccess()) {
// Lazy initialization
if (!this.realSubject) {
this.realSubject = new RealSubject();
}
// Logging
this.logAccess();
// Delegate to real subject
this.realSubject.request();
}
}
private checkAccess(): boolean {
console.log('Proxy: Checking access');
return true;
}
private logAccess(): void {
console.log('Proxy: Logging access time');
}
}
// Usage
const proxy = new Proxy();
proxy.request();
// Output:
// Proxy: Checking access
// Proxy: Logging access time
// RealSubject: Handling request
Modern JavaScript Proxy
const target = {
message: 'Hello',
getValue() {
return this.message;
}
};
const handler = {
get(target: any, prop: string) {
console.log(`Accessing property: ${prop}`);
return target[prop];
},
set(target: any, prop: string, value: any) {
console.log(`Setting property: ${prop} = ${value}`);
target[prop] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.message); // Logs: Accessing property: message
proxy.message = 'World'; // Logs: Setting property: message = World
Detection Markers
- Implements same interface as real subject
- Holds reference to real subject
- Controls access (checks, logging, caching)
- Lazy initialization of real subject
Common Mistakes
- Proxy chains: Multiple proxies wrapping each other
- Performance overhead: Every access goes through proxy
- Confusion with Decorator: Proxy controls access; Decorator adds behavior
Summary Table
| Pattern | Complexity | Use Frequency | Main Benefit |
|---|---|---|---|
| Adapter | Low | High | Interface compatibility |
| Bridge | High | Low | Decouple abstraction from implementation |
| Composite | Medium | High | Uniform tree structure handling |
| Decorator | Medium | High | Dynamic responsibility addition |
| Facade | Low | Very High | Simplified subsystem interface |
| Flyweight | High | Low | Memory optimization |
| Proxy | Medium | Medium | Controlled access |
Best Practices
- Adapter vs Bridge: Adapter fixes incompatibility; Bridge designs flexibility
- Decorator vs Proxy: Decorator adds features; Proxy controls access
- Facade simplicity: Should coordinate, not contain business logic
- Composite uniformity: Leaf and Composite must share interface
- Use native Proxy: JavaScript
Proxyobject for dynamic property access
References
- Design Patterns: Elements of Reusable Object-Oriented Software (Gang of Four)
- MDN: Proxy
- Refactoring Guru: Structural Patterns