Two existing classes do similar work but expose different method names, parameter shapes, or argument orders. Client code that wants to treat them uniformly branches on which one it has; each branch contains its own translation logic; new branches appear as new client sites do their own variant of the same translation.
An Adapter wraps the off-shape class and exposes the canonical interface. Clients receive whichever conformant instance the composition root produces; no client code knows about the underlying difference.
Before the refactoring
// Two classes do similar work through different shapes; callers branch on which they have.class Logger {log(level, message) { /* writes to internal log */ }}class ThirdPartyReporter {report(severityName, text, context) { /* delivers to vendor */ }}function emitDiagnostic(emitter, level, message) {if (emitter instanceof Logger) {emitter.log(level, message);} else if (emitter instanceof ThirdPartyReporter) {emitter.report(level.toUpperCase(), message, {});} else {throw new Error('unknown emitter');}}
After the refactoring
// One adapter gives the vendor class the internal interface; callers stop branching.class Logger {log(level, message) { /* writes to internal log */ }}class ThirdPartyReporter {report(severityName, text, context) { /* delivers to vendor */ }}class ReporterAsLogger {constructor(reporter) {this.reporter = reporter;}log(level, message) {this.reporter.report(level.toUpperCase(), message, {});}}function emitDiagnostic(logger, level, message) {logger.log(level, message);}// At the composition root:const adaptedReporter = new ReporterAsLogger(new ThirdPartyReporter());emitDiagnostic(adaptedReporter, 'error', 'something failed');
Alternative-interface branches at every consumer duplicate the translation rules. Subtle translation bugs (wrong case, missing context argument, swapped order) ship as misrouted diagnostic data; tests for the translation live inside each consumer's tests rather than once where they belong.
Each adapter is one new class; for two-variant systems with stable interfaces, the adapter may add more friction than the inline conversion did. Wrapping a third-party class in an adapter ties the adapter to that vendor's surface — vendor changes ripple through the adapter; consumers stay insulated.
Consumers operate against one interface; translation lives in one file; new consumers stop knowing the vendor's shape. Vendor swaps become an adapter change, not a consumer-population edit.
Adapters that grow large translation logic (multi-step shaping, async/sync bridging, error-model translation) become a hidden host for non-trivial behaviour. When the adapter starts containing business rules, what was Adapter has drifted into Anti-Corruption Layer; rename and refactor accordingly.