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).