Harness-engineering gof-adapter-pattern

GOF Adapter 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/codex/gof-adapter-pattern" ~/.claude/skills/intense-visions-harness-engineering-gof-adapter-pattern-4041a0 && rm -rf "$T"
manifest: agents/skills/codex/gof-adapter-pattern/SKILL.md
source content

GOF Adapter Pattern

Wrap incompatible interfaces to make them work together without modifying source code.

When to Use

  • You want to use an existing class but its interface doesn't match what you need
  • You're integrating a third-party library with a fixed interface into a system that expects a different one
  • You're wrapping a legacy component to work with new code
  • You need to keep the original class unchanged (read-only dependency, vendor code)

Instructions

Object adapter (composition — preferred in TypeScript):

// What your system expects
interface Logger {
  info(message: string, context?: object): void;
  error(message: string, error?: Error): void;
  warn(message: string, context?: object): void;
}

// What you have (third-party SDK, fixed interface)
class PinoLogger {
  log(level: 'info' | 'warn' | 'error', payload: { msg: string; [key: string]: unknown }): void {
    console.log(JSON.stringify({ level, ...payload }));
  }
}

// Adapter wraps PinoLogger to satisfy Logger interface
class PinoLoggerAdapter implements Logger {
  constructor(private readonly pino: PinoLogger) {}

  info(message: string, context?: object): void {
    this.pino.log('info', { msg: message, ...context });
  }

  error(message: string, error?: Error): void {
    this.pino.log('error', {
      msg: message,
      err: error ? { message: error.message, stack: error.stack } : undefined,
    });
  }

  warn(message: string, context?: object): void {
    this.pino.log('warn', { msg: message, ...context });
  }
}

// Usage — client depends only on Logger interface
const logger: Logger = new PinoLoggerAdapter(new PinoLogger());
logger.info('Server started', { port: 3000 });

Adapting an async legacy API:

// Legacy callback-based interface
interface LegacyStorage {
  readFile(path: string, callback: (err: Error | null, data: Buffer) => void): void;
  writeFile(path: string, data: Buffer, callback: (err: Error | null) => void): void;
}

// Modern promise-based interface expected by new code
interface AsyncStorage {
  read(path: string): Promise<Buffer>;
  write(path: string, data: Buffer): Promise<void>;
}

class LegacyStorageAdapter implements AsyncStorage {
  constructor(private readonly legacy: LegacyStorage) {}

  read(path: string): Promise<Buffer> {
    return new Promise((resolve, reject) => {
      this.legacy.readFile(path, (err, data) => {
        if (err) reject(err);
        else resolve(data);
      });
    });
  }

  write(path: string, data: Buffer): Promise<void> {
    return new Promise((resolve, reject) => {
      this.legacy.writeFile(path, data, (err) => {
        if (err) reject(err);
        else resolve();
      });
    });
  }
}

Two-way adapter (adapt in both directions):

// When you need to satisfy two incompatible interfaces simultaneously
class BidirectionalAdapter implements InterfaceA, InterfaceB {
  constructor(
    private readonly a: ComponentA,
    private readonly b: ComponentB
  ) {}

  // InterfaceA methods delegate to ComponentA
  doSomethingA(): void {
    this.a.operationA();
  }

  // InterfaceB methods delegate to ComponentB
  doSomethingB(): void {
    this.b.operationB();
  }
}

Details

Adapter vs. Facade: Both simplify usage, but serve different purposes. An Adapter makes one interface compatible with another — the client expects a specific interface. A Facade simplifies a complex subsystem — the client doesn't care about the interface, just wants it simpler. If you're translating interfaces, use Adapter. If you're hiding complexity, use Facade.

Adapter vs. Decorator: Both wrap an object. An Adapter changes the interface. A Decorator adds behavior while keeping the same interface.

Anti-patterns:

  • Leaky adapter — letting the adaptee's types bleed into the adapter's public API
  • Adapter that does too much business logic — adapters should translate, not transform
  • Multiple adapters stacked — signals you need a proper abstraction layer, not more wrapping

Testing with adapters: Mock the target interface, not the adaptee. This is exactly the value of the pattern — tests use the interface, not the concrete third-party dependency.

// In tests, use a mock Logger instead of PinoLoggerAdapter
const mockLogger: Logger = {
  info: jest.fn(),
  error: jest.fn(),
  warn: jest.fn(),
};

Source

refactoring.guru/design-patterns/adapter

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.