Harness-engineering gof-decorator-pattern

GOF Decorator Pattern

install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/gof-decorator-pattern" ~/.claude/skills/intense-visions-harness-engineering-gof-decorator-pattern && rm -rf "$T"
manifest: agents/skills/claude-code/gof-decorator-pattern/SKILL.md
source content

GOF Decorator Pattern

Attach additional behavior to objects at runtime by wrapping them in decorator objects.

When to Use

  • You want to add behavior to individual objects without affecting other objects of the same class
  • Subclassing would create a combinatorial explosion of classes for each feature combination
  • You need to stack multiple behaviors in a flexible order at runtime
  • The behavior you're adding is cross-cutting (logging, caching, validation, retry) and belongs outside core logic

Instructions

Classic structural decorator:

// Component interface
interface DataSource {
  write(data: string): Promise<void>;
  read(): Promise<string>;
}

// Concrete component
class FileDataSource implements DataSource {
  constructor(private readonly path: string) {}

  async write(data: string): Promise<void> {
    // Write to file
    console.log(`Writing to ${this.path}: ${data}`);
  }

  async read(): Promise<string> {
    // Read from file
    return `data from ${this.path}`;
  }
}

// Base decorator — implements the interface and wraps a component
abstract class DataSourceDecorator implements DataSource {
  constructor(protected readonly wrapped: DataSource) {}

  async write(data: string): Promise<void> {
    return this.wrapped.write(data);
  }

  async read(): Promise<string> {
    return this.wrapped.read();
  }
}

// Concrete decorator: encryption
class EncryptionDecorator extends DataSourceDecorator {
  async write(data: string): Promise<void> {
    const encrypted = Buffer.from(data).toString('base64'); // simplified
    await this.wrapped.write(encrypted);
  }

  async read(): Promise<string> {
    const data = await this.wrapped.read();
    return Buffer.from(data, 'base64').toString('utf8'); // simplified
  }
}

// Concrete decorator: compression
class CompressionDecorator extends DataSourceDecorator {
  async write(data: string): Promise<void> {
    const compressed = `[compressed:${data}]`; // simplified
    await this.wrapped.write(compressed);
  }

  async read(): Promise<string> {
    const data = await this.wrapped.read();
    return data.replace(/^\[compressed:/, '').replace(/\]$/, '');
  }
}

// Stack decorators in any order
const source: DataSource = new CompressionDecorator(
  new EncryptionDecorator(new FileDataSource('/data/users.dat'))
);

await source.write('hello world');

Function-based decorator (idiomatic TypeScript for async functions):

type AsyncFn<T extends unknown[], R> = (...args: T) => Promise<R>;

// Retry decorator
function withRetry<T extends unknown[], R>(
  fn: AsyncFn<T, R>,
  maxAttempts = 3,
  delayMs = 500
): AsyncFn<T, R> {
  return async (...args: T): Promise<R> => {
    let lastError: Error;
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        return await fn(...args);
      } catch (err) {
        lastError = err as Error;
        if (attempt < maxAttempts) {
          await new Promise((r) => setTimeout(r, delayMs * attempt));
        }
      }
    }
    throw lastError!;
  };
}

// Cache decorator
function withCache<T extends unknown[], R>(fn: AsyncFn<T, R>, ttlMs = 60_000): AsyncFn<T, R> {
  const cache = new Map<string, { value: R; expiresAt: number }>();
  return async (...args: T): Promise<R> => {
    const key = JSON.stringify(args);
    const cached = cache.get(key);
    if (cached && Date.now() < cached.expiresAt) return cached.value;
    const value = await fn(...args);
    cache.set(key, { value, expiresAt: Date.now() + ttlMs });
    return value;
  };
}

// Compose decorators
const fetchUser = async (id: string): Promise<User> => {
  return db.users.findOneOrFail(id);
};

const robustFetchUser = withCache(withRetry(fetchUser, 3), 30_000);

Details

Decorator vs. Proxy: Both wrap an object. The Decorator adds behavior; the Proxy controls access. In practice the implementation is similar — the distinction is intent. Use Decorator for feature stacking (logging, caching), use Proxy for access control (authorization, lazy loading).

TypeScript class decorators vs. GOF Decorator pattern: TypeScript's

@Decorator
syntax is a different mechanism (metaprogramming on class metadata). The GOF Decorator is a runtime object-wrapping pattern. The GOF pattern works without
experimentalDecorators
and is preferred for production code.

Anti-patterns:

  • Decorator that reads but doesn't write — all interface methods must be delegated, even if the decorator only augments one
  • Decorators with ordering dependencies — if the order matters, document it clearly or use a pipeline builder
  • Too many decorator layers — consider a middleware pipeline instead when you have more than 3-4 stacked behaviors

Middleware pipeline (alternative for many cross-cutting concerns):

type Middleware<T> = (value: T, next: () => Promise<T>) => Promise<T>;

async function pipeline<T>(value: T, middlewares: Middleware<T>[]): Promise<T> {
  const run = async (index: number): Promise<T> => {
    if (index >= middlewares.length) return value;
    return middlewares[index](value, () => run(index + 1));
  };
  return run(0);
}

Source

refactoring.guru/design-patterns/decorator

Process

  1. Read the instructions and examples in this document.
  2. Apply the patterns to your implementation, adapting to your specific context.
  3. Verify your implementation against the details and edge cases listed above.

Harness Integration

  • Type: knowledge — this skill is a reference document, not a procedural workflow.
  • No tools or state — consumed as context by other skills and agents.

Success Criteria

  • The patterns described in this document are applied correctly in the implementation.
  • Edge cases and anti-patterns listed in this document are avoided.