← Back to PatternPip

ADAPTER / QUICK REFERENCE

Translate once.
Keep the contract.

Wrap an existing object so clients can use it through the interface they expect.

What does not fit?
An existing service has a different interface or data format.
What stays stable?
The interface the client already expects.
Where does translation go?
In an adapter that wraps the incompatible service.

The roles

Client · readPanel
Works only through WeatherSource. It does not inspect provider names or Fahrenheit fields.
Target · WeatherSource
The interface the app expects: read() returning Celsius and a supported named condition.
Adaptee · LegacyForecastFeed
The existing incompatible object: getForecast() returns temp_f and sky_code.
Adapter · FahrenheitAdapter
Implements the target, wraps the feed and translates calls and response meaning.

This is an object adapter using composition. A separate adapter for an already-compatible provider is unnecessary.

The shape in TypeScript

1. Define the target

export type WeatherReading = Readonly<{
  celsius: number;
  condition: "sunny" | "rainy";
}>;
export interface WeatherSource {
  read(): WeatherReading;
}

2. Keep the client stable

// The display only knows the app's WeatherSource contract.
export function readPanel(source: WeatherSource): WeatherReading {
  return source.read();
}

3. Wrap the incompatible feed

export class FahrenheitAdapter implements WeatherSource {
  private feed: LegacyForecastFeed;
  constructor(feed: LegacyForecastFeed) {
    this.feed = feed;
  }
  read(): WeatherReading {
    const raw = this.feed.getForecast();
    return makeReading(
      ((raw.temp_f - 32) * 5) / 9,
      conditionFromCode(raw.sky_code),
    );
  }
}

4. Choose the provider in wiring

export function chooseWeatherSource(
  provider: "station" | "legacy",
  station: CelsiusStation,
  feed: LegacyForecastFeed,
): WeatherSource {
  // Wiring still chooses the provider. Translation belongs to its adapter.
  return provider === "station" ? station : new FahrenheitAdapter(feed);
}
Provider types and validation helpers
export class CelsiusStation implements WeatherSource {
  private reading: WeatherReading;
  constructor(celsius: number, condition: WeatherReading["condition"]) {
    this.reading = makeReading(celsius, condition);
  }
  read(): WeatherReading {
    return this.reading;
  }
}
export type LegacyForecast = Readonly<{ temp_f: number; sky_code: number }>;
// Represents an existing vendor API. Local fixtures keep the lesson offline.
export class LegacyForecastFeed {
  private forecast: LegacyForecast;
  constructor(forecast: LegacyForecast) {
    this.forecast = Object.freeze({ ...forecast });
  }
  getForecast(): LegacyForecast {
    return this.forecast;
  }
}
export function makeReading(
  celsius: number,
  condition: WeatherReading["condition"],
): WeatherReading {
  if (!Number.isFinite(celsius)) throw new Error("Temperature must be finite.");
  if (condition !== "sunny" && condition !== "rainy")
    throw new Error("Unsupported condition.");
  return Object.freeze({ celsius, condition });
}
export function conditionFromCode(code: number): WeatherReading["condition"] {
  if (code === 1) return "sunny";
  if (code === 2) return "rainy";
  throw new Error(`Unsupported weather code: ${code}.`);
}

These excerpts come from the TypeScript implementation used by the live lesson.

Translate meaning, not just names.

  • Units: (°F − 32) × 5 / 9 produces Celsius. Keep precision in the model; the display rounds to one decimal.
  • Codes: this sample vendor uses 1 for sunny and 2 for rainy. Other codes throw an error; they never silently become sunny.
  • Values: temperatures must be finite. Provider failures propagate. The UI clears the previous reading on a new request and reports errors instead of presenting stale weather as current.
  • Ownership: fixture responses are copied/frozen, and conversion does not modify the vendor response. The target reading is an immutable flat value.
  • Limits: these are local synchronous samples. A real network contract also needs explicit asynchronous, authentication, freshness, timeout and error policies.

Use it where interfaces meet.

A good fit

Existing vendor libraries, old systems or reusable services that cannot conveniently change to match the client.

Keep simpler alternatives in view

A function can adapt a single value. If you own both sides and can safely align their contracts, an extra wrapper may be unnecessary.

Common mistakes

  • Renaming Fahrenheit as Celsius without converting the value.
  • Letting raw vendor fields escape through the target interface.
  • Putting provider-selection branches back inside every client.
  • Guessing missing or unsupported data to force a successful result.
  • Confusing Adapter with Strategy (interchangeable behaviors), Decorator (added behavior through a compatible interface), or Facade (a simpler entry point to a subsystem).