iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Adapter

The Adapter pattern wires together two incompatible interfaces. The adapter wraps the “adaptee” and exposes the interface the rest of the code expects. Use it for legacy code, third-party APIs, and gradual migrations.

Class + object + functional adapters

EXAMPLE
// 1) Problem — incompatible interfaces
interface ModernLogger {
    log(level: 'info' | 'warn' | 'error', msg: string, context?: object): void;
}

// Legacy logger has a different API
class LegacyLogger {
    write(message: string) { /* writes to file */ }
    writeError(message: string) { /* writes errors to stderr */ }
}

// 2) Adapter — translates calls
class LegacyLoggerAdapter implements ModernLogger {
    constructor(private legacy: LegacyLogger) {}

    log(level: 'info' | 'warn' | 'error', msg: string, context?: object) {
        const formatted = context ? `${msg} ${JSON.stringify(context)}` : msg;
        if (level === 'error') this.legacy.writeError(formatted);
        else                   this.legacy.write(`[${level.toUpperCase()}] ${formatted}`);
    }
}

function process(logger: ModernLogger) {
    logger.log('info', 'Processing', { userId: 42 });
}

process(new LegacyLoggerAdapter(new LegacyLogger()));

// 3) Third-party API adapter
interface PaymentProvider {
    charge(amount: number, currency: string): Promise<{ ok: boolean; ref: string }>;
}

// Stripe SDK has a different shape
class StripeAdapter implements PaymentProvider {
    constructor(private stripe: Stripe) {}

    async charge(amount: number, currency: string) {
        const intent = await this.stripe.paymentIntents.create({
            amount:   Math.round(amount * 100),     // dollars → cents
            currency: currency.toLowerCase(),
            confirm:  true,
        });
        return { ok: intent.status === 'succeeded', ref: intent.id };
    }
}

// PayPal SDK — completely different API
class PayPalAdapter implements PaymentProvider {
    constructor(private paypal: PayPalSdk) {}

    async charge(amount: number, currency: string) {
        const order = await this.paypal.orders.create({
            intent: 'CAPTURE',
            purchase_units: [{ amount: { value: amount.toFixed(2), currency_code: currency } }],
        });
        const captured = await this.paypal.orders.capture(order.id);
        return { ok: captured.status === 'COMPLETED', ref: order.id };
    }
}

// App code only knows the PaymentProvider interface
class Checkout {
    constructor(private payments: PaymentProvider) {}
    async pay(order: Order) {
        return this.payments.charge(order.total, order.currency);
    }
}

// Wire at composition root
const checkout = new Checkout(new StripeAdapter(new Stripe(STRIPE_KEY)));

// 4) Functional adapter — closure-based (no classes)
type ChargeFn = (amount: number, currency: string) => Promise<{ ok: boolean; ref: string }>;

const stripeChargeAdapter = (stripe: Stripe): ChargeFn =>
    async (amount, currency) => {
        const intent = await stripe.paymentIntents.create({ /* … */ });
        return { ok: intent.status === 'succeeded', ref: intent.id };
    };

const charge = stripeChargeAdapter(new Stripe(STRIPE_KEY));
await charge(99.99, 'USD');

// 5) Adapter as a translation layer between two systems
// Modern web app reads from a legacy SOAP API
class UserService {
    constructor(private legacy: LegacySoapClient) {}

    async getUser(id: number): Promise<User> {
        const soap = await this.legacy.GetUserById({ userId: id.toString() });
        return {
            id:    parseInt(soap.UserDto.Id),
            name:  soap.UserDto.FullName,
            email: soap.UserDto.EmailAddress.toLowerCase(),
            createdAt: new Date(soap.UserDto.CreationDate),
            roles: soap.UserDto.RoleList.Role.map(r => r.RoleName),
        };
    }
}

// 6) Logging / metrics / tracing — adapter for cross-cutting concerns
interface MetricsClient {
    increment(name: string, tags?: Record<string, string>): void;
    timing(name: string, ms: number, tags?: Record<string, string>): void;
}

class DatadogAdapter implements MetricsClient {
    constructor(private dd: DogStatsD) {}
    increment(name: string, tags = {}) { this.dd.increment(name, 1, this.tagToArray(tags)); }
    timing(name: string, ms: number, tags = {}) { this.dd.histogram(name, ms, this.tagToArray(tags)); }
    private tagToArray(tags: Record<string, string>) {
        return Object.entries(tags).map(([k, v]) => `${k}:${v}`);
    }
}

class PrometheusAdapter implements MetricsClient {
    constructor(private registry: Registry) {}
    increment(name: string, tags = {}) {
        const counter = new Counter({ name, help: name, labelNames: Object.keys(tags) });
        counter.inc(tags as any);
    }
    timing(name: string, ms: number, tags = {}) { /* histogram */ }
}

// 7) Object adapter vs class adapter
// JavaScript / Python typically use OBJECT adapter — composition:
class Adapter {
    constructor(private adaptee: Adaptee) {}
    method() { return this.adaptee.differentMethod(); }
}

// Java / C# can use CLASS adapter — inheritance:
class Adapter2 extends Adaptee implements Target {
    method() { return this.differentMethod(); }
}

// Object adapter is more flexible — wrap multiple adaptees, dynamic swap.

// 8) Two-way adapter — translates both directions
class TwoWayAdapter implements ModernApi, LegacyApi {
    // Implement both interfaces; route to appropriate underlying calls
}

// 9) Adapter for testing — mock external systems
class FakePaymentProvider implements PaymentProvider {
    private charges: { amount: number; currency: string }[] = [];

    async charge(amount: number, currency: string) {
        this.charges.push({ amount, currency });
        return { ok: true, ref: 'fake-' + Math.random() };
    }

    getCharges() { return this.charges; }
}

const checkout = new Checkout(new FakePaymentProvider());
await checkout.pay({ total: 100, currency: 'USD' });
expect((checkout.payments as FakePaymentProvider).getCharges()).toHaveLength(1);

// 10) Gradual migration pattern
// Old API → adapter → new internal API
// Then move callers off the adapter as they migrate.

// Phase 1: introduce adapter
class NewApiAdapter {
    constructor(private old: OldApi) {}
    fetchUser(id: number) { return this.old.GetUserSync(id); }
}

// Phase 2: code uses NewApiAdapter
// Phase 3: replace internals with real new implementation
// Phase 4: delete the OldApi

// 11) When to use Adapter
//   ✅ Working with legacy code you can't modify
//   ✅ Integrating third-party libraries with different shapes
//   ✅ Gradual migration between two APIs
//   ✅ Testing — fake/stub external dependencies
//   ✅ Switching between providers (multi-cloud, multi-payment)

// 12) When NOT to use Adapter
//   ❌ The 'adaptee' already matches your interface
//   ❌ Only one adapter — just use the underlying API directly
//   ❌ When the gap requires too much logic — consider a Facade or refactor

// 13) Adapter vs Facade vs Bridge
// Adapter — makes existing class compatible with a different interface
// Facade  — simplifies a complex subsystem (one class for many)
// Bridge  — separates abstraction from implementation (two parallel hierarchies)

// 14) Common bugs
//   • Adapter does TOO much (becomes a god class) — extract logic
//   • Adapter exposes adaptee's bugs through translation — verify carefully
//   • Wrong shape mapping (e.g. integer → string conversion off-by-one)
//   • Async/sync mismatch — adapter may need to fake one
//   • Different error semantics — wrap exceptions consistently

// 15) Real-world examples (in standard libraries)
//   • Java: InputStreamReader (adapts byte stream to char stream)
//   • .NET: AdoNetClient adapters (SqlClient, OracleClient, etc.)
//   • Node: Streams interop with Web Streams via Readable.toWeb / Readable.fromWeb
//   • React: useSyncExternalStore (adapts external state to React)

// 16) Best practices
//   • Keep the adapter THIN — translate, don't add behaviour
//   • Name clearly: SomethingAdapter or SomethingProvider
//   • Test the adapter in isolation
//   • Document the translation rules
//   • Inject the adapter at the composition root — don't construct mid-flow
//   • One adapter per provider — don't merge into a giant switch

Why it matters

Adapter wraps an existing API to fit a new interface; clients depend on the new interface, you swap providers freely. Pair with dependency injection — test fakes become trivial adapters of the same shape.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Wrap a class with one interface to make it look like another.
class StripeAdapter {
    constructor(stripe) { this.stripe = stripe; }
    charge(amount) { return this.stripe.createCharge({ amount }); }
}
Try it Yourself »

Discussion

Loading…