Design Patterns · Structural Patterns
Adapter
Translate an existing interface into the contract a client needs. Keep format, unit, and error conversions at that boundary.
This lesson follows Singleton and starts the structural patterns. These patterns describe how objects fit together.
Adapter makes incompatible interfaces cooperate. It implements the client's expected contract and translates calls to an existing service. The cost is a translation layer that must preserve meaning.
Find the mismatch before writing the wrapper
Checkout charges in integer cents. A legacy payment library accepts decimal dollars and calls its method submitPayment. The library cannot be changed. Renaming a method is easy. Charging 100 times the expected amount is also easy.
Try it
Charge 1250 cents through the two boundaries
submitPayment(1250)
legacy service interprets dollars → 1250.00
charge(1250)
submitPayment(12.5)
meaning preserved → 12.50 dollars
A compatible method signature does not prove compatible units.
Implement the client's contract
interface PaymentGateway {
charge(cents: number): Promise<string>;
}
interface LegacyPay {
submitPayment(dollars: number): Promise<{ transaction_id: string }>;
}
class LegacyPaymentAdapter implements PaymentGateway {
constructor(private legacy: LegacyPay) {}
async charge(cents: number): Promise<string> {
if (!Number.isSafeInteger(cents) || cents <= 0) {
throw new Error("Charge requires positive integer cents");
}
const reply = await this.legacy.submitPayment(cents / 100);
return reply.transaction_id;
}
}
class Checkout {
constructor(private gateway: PaymentGateway) {}
pay(cents: number) { return this.gateway.charge(cents); }
}
Checkout knows PaymentGateway. The adapter knows both contracts. LegacyPay does its original job and does not know an adapter exists. The money conversion only illustrates the interface. A real payment integration must follow its provider's exact representation and precision rules.
Step through
Trace one translated call
Receive the target operation
The client supplies 1250 cents. Validate the target contract before translating.
Translate and delegate
1250 cents → 12.5 dollars
legacy.submitPayment(12.5)
Do not move checkout discounts into the adapter. They belong to business logic, not interface translation.
Translate the response
{ transaction_id: 'tx-42' } → 'tx-42'
Map failures too when the two contracts expose different errors. Preserve failures that the client needs to act on.
Prefer composition for an existing service
An object adapter holds the service in a field. It works with existing instances and keeps the service's class hierarchy separate. A class adapter inherits service behavior while satisfying the target interface. In languages with suitable multiple inheritance, it can inherit both sides. That ties it more tightly to the inherited implementation.
Translation cannot invent a missing guarantee
An adapter cannot make a best-effort operation transactional by renaming it commit(). If the old service cannot meet the target promise, change the promise or provide the missing mechanism.
Check yourself
A service cannot provide atomic writes. Can an adapter promise atomic commit by renaming write()?
Correct. The adapter needs an additional mechanism or a weaker target contract.
Not quite. The target's behavioral promise must hold as well as its signature.
Map the roles
| Role | In this example | Job |
|---|---|---|
| Client | Checkout | Contains the business logic and uses only the client interface |
| Client interface | PaymentGateway | The contract other classes must follow to work with the client |
| Service (adaptee) | LegacyPay | A useful class, often third-party or legacy, whose interface the client cannot use directly |
| Adapter | LegacyPaymentAdapter | Implements the client interface, wraps the service, and converts each call into a form the service accepts |
Check yourself
Payments must also go through a second legacy provider with yet another interface. What changes in Checkout?
Not quite. That is the coupling the adapter exists to prevent.
Correct. Checkout depends only on the client interface, so new adapters plug in without client edits.
Reach for it when a useful class has the wrong interface
- you want to use an existing class, but its interface does not match the rest of your code. The adapter is a middle layer that translates between your code and a legacy class, a third-party class, or any class with an odd interface.
- several existing subclasses lack the same feature, and you cannot add it to their superclass. Copying the feature into a new subclass of each one duplicates code. Instead, put the missing feature in an adapter and wrap any of those objects in it. The objects must share a common interface, and the adapter's field must use that interface. This looks a lot like Decorator.
Check yourself
Three report classes from a vendor library all lack toCsv(), and you cannot edit their shared base class. What does the second Adapter use case suggest?
Not quite. That repeats the same code three times, which is the smell this use case avoids.
Correct. Wrapping objects in one adapter avoids three copy-paste subclasses.
Implement it in six steps
Refactor existing code toward the pattern in this order:
- Confirm there are at least two classes with incompatible interfaces: a useful service you cannot change, and one or more clients that would benefit from using it.
- Declare the client interface. It describes how clients talk to the service.
- Create the adapter class and make it implement the client interface. Leave its methods empty for now.
- Add a field that references the service object. Usually the constructor sets it, though sometimes it is easier to pass the service into each method call.
- Implement every method of the client interface. Delegate the real work to the service. The adapter handles only interface or data-format conversion.
- Make clients use the adapter through the client interface. You can then change or extend adapters without touching client code.
Check yourself
During step 5, a teammate adds a 10% loyalty discount inside charge() in the adapter. Is that the adapter's job?
Not quite. The adapter should stay a thin translation layer.
Correct. Mixing business rules into translation code makes both harder to change.
Name what it costs
| You gain | You pay |
|---|---|
| Interface and data conversion live apart from business logic (single responsibility) | More interfaces and classes, so overall complexity rises |
| New adapters without breaking client code, as long as clients use the client interface (open/closed) | Sometimes simply changing the service class to match is cheaper |
Check yourself
Your team owns both small classes and can change either one in an afternoon. Is an adapter worth it?
Correct. Adapter earns its cost when you cannot change the service, or changing it would ripple through many callers.
Not quite. A wrapper you did not need is extra code to maintain.
Do not confuse it with its neighbors
| Pattern | How it relates |
|---|---|
| Bridge | Bridge is usually designed up front so parts of an application can be built independently. Adapter is usually applied to existing code to make incompatible classes work together. |
| Decorator | Adapter changes an object's interface. Decorator enhances an object while keeping its interface, and it supports recursive stacking, which Adapter does not. |
| Proxy | Adapter gives the wrapped object a different interface. Proxy gives it the same interface. Decorator gives it an enhanced interface. |
| Facade | Facade defines a new interface over existing objects, often a whole subsystem. Adapter makes an existing interface usable and usually wraps one object. |
| Strategy, State | Bridge, State, Strategy, and to some degree Adapter share a structure built on composition: delegating work to another object. They solve different problems. |
Check yourself
A wrapper keeps the exact same interface as the service and checks permissions before delegating. Which pattern is it?
Correct. Same interface plus controlled access is Proxy. Adapter would change the interface.
Not quite. Nothing is being translated. The interfaces already match.
Retrieve and apply
Check yourself
A wrapper changes fetchTemperature() into read(), but still returns Fahrenheit where the client expects Celsius. Is the adapter complete?
Correct. Matching names is only part of adapting an interface. Convert units and state the conversion's limits.
Not quite. The client receives a valid number with the wrong meaning. That is a contract failure.
List the unit, format, and failure conversions for one external API. Continue to Bridge for two dimensions that should grow separately.
Source: Alexander Shvets, Dive Into Design Patterns (深入设计模式), Chinese edition v2021-1.25. Adapter, printed pages 143–154. Explanations, examples, and exercises are adapted for this course.