I'll Be Backend
Principles · TypeScript

Coupling: wrap a third-party library

Keep a vendor's library behind one class of yours, so a switch touches one class.

13 steps4 chapters~5 min at 1.25×#architecture#coupling#wrapper#adapter pattern
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the TypeScript video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–3

Refactor

Steps 4–9

Payoff

Steps 10–11

Recap

Steps 12–13

The code: before and after

Before

class BookingService {
  private readonly pay = new CardPayClient("desk-key");
  private readonly rates: Record<string, number> = {
    Golf: 40, Polo: 35,
  };

  book(car: string, days: number): string {
    const total = this.rates[car] * days;
    const r = this.pay.charge(total * 100, "USD");
    if (r.getStatus() !== "succeeded") {
      return "declined";
    }
    return `${car} booked, ${total} USD`;
  }
}

class ReturnDesk {
  private readonly client = new CardPayClient("desk-key");

  lateFee(car: string, hours: number): string {
    const fee = hours * 15;
    const result = this.client.charge(fee * 100, "USD");
    if (result.getStatus() !== "succeeded") {
      return "declined";
    }
    return `${car} late fee, ${fee} USD`;
  }
}

function desk(): string[] {
  return [
    new BookingService().book("Golf", 3),
    new ReturnDesk().lateFee("Golf", 2),
  ];
}

After

interface Payments {
  charge(dollars: number): boolean;
}

class CardPayPayments implements Payments {
  private readonly pay: CardPayClient;

  constructor(apiKey: string) {
    this.pay = new CardPayClient(apiKey);
  }

  charge(dollars: number): boolean {
    const r = this.pay.charge(dollars * 100, "USD");
    return r.getStatus() === "succeeded";
  }
}

class PayLanePayments implements Payments {
  private readonly lane: PayLane;

  constructor(account: string) {
    this.lane = new PayLane(account);
  }

  charge(dollars: number): boolean {
    try {
      this.lane.send(dollars, "USD");
      return true;
    } catch (err) {
      if (err instanceof PayLaneException) return false;
      throw err;
    }
  }
}

class BookingService {
  private readonly rates: Record<string, number> = {
    Golf: 40, Polo: 35,
  };

  constructor(private readonly payments: Payments) {}

  book(car: string, days: number): string {
    const total = this.rates[car] * days;
    if (!this.payments.charge(total)) {
      return "declined";
    }
    return `${car} booked, ${total} USD`;
  }
}

class ReturnDesk {
  constructor(private readonly billing: Payments) {}

  lateFee(car: string, hours: number): string {
    const fee = hours * 15;
    if (!this.billing.charge(fee)) {
      return "declined";
    }
    return `${car} late fee, ${fee} USD`;
  }
}

function desk(): string[] {
  const payments: Payments = new PayLanePayments("desk-account");
  return [
    new BookingService(payments).book("Golf", 3),
    new ReturnDesk(payments).lateFee("Golf", 2),
  ];
}

Transcript, step by step

Problem

Step 1: Two classes take money Before

This is the same car rental desk from last time, and now it takes card payments. Booking Service prices a rental and charges the card, and Return Desk charges a late fee of fifteen dollars an hour. Both call Card Pay, a third-party library, which is code another company writes and ships, that we call but cannot change. Each class keeps a Card Pay client here, calls charge in cents here, and checks the status string here. The desk function is the caller, and it returns Golf booked for a hundred and twenty dollars, then a Golf late fee of thirty. The library's own guide shows exactly these lines, so pasting them wherever money is taken was the shortest path.

Step 2: Finance moves to PayLane Change request

Finance moves card payments to Pay Lane next month. The quick edit swaps the field in both classes for a Pay Lane client with the desk account. Pay Lane takes dollars instead of cents and says no by throwing an exception, so each charge becomes a send inside a try catch. It compiles, and the desk function returns the same two lines as before.

Step 3: Coupled to the vendor Code smell

Coupling is how much one part of the code must change when another part changes. Here our business classes are coupled to a third-party library, because they use its class, its account, its units and its way of saying no. You can see the field here, the send call here, and the catch here, once in each class. So a vendor switch edits every class that takes money, and here that's two classes, each with a new field, a new call, dollars in place of cents, and a try catch in place of the status check.

Refactor

Step 4: Refactor first Back it out

So let's undo the edit, refactor first, and then switch. Both classes are back on Card Pay, and every step from here returns the same two lines.

Step 5: Name the charge Extract Method

First we extract a method in Booking Service called charge, which takes dollars and returns true when the card goes through. It holds the cents math, the call to the library, and the status check. Book now asks charge and returns declined when the answer is no, so the desk gets the same lines.

Step 6: The library gets a class Extract Class

Next we extract a class, Card Pay Payments, and move the client field and the charge method into it. Its constructor takes the API key, and charge is no longer private, so other classes can call it. Booking Service holds one with the desk key and calls its charge method. A wrapper is one small class of ours that is the only code that calls the library. Return Desk still calls Card Pay itself, so this class isn't the only caller yet.

Step 7: Return desk uses it too Replace Inline Code with Function Call

Return Desk drops its own Card Pay client and holds a Card Pay Payments called billing instead. Late fee replaces the call and the status check with one call to charge, the same way book does. Card Pay Payments is now the wrapper, because it is the only class that calls the Card Pay library.

Step 8: Name what the desk needs Extract Interface

We extract an interface, Payments, with the one charge method our desk needs. Card Pay Payments implements it, and both fields are now typed as Payments. Nothing about cents or status strings is left in that interface, so another vendor can fit behind it.

Step 9: Pass the wrapper in Constructor Injection

Each service still built its own wrapper with new, so the choice of vendor sat inside two business classes. With constructor injection, a form of dependency injection, each class takes its Payments in the constructor instead. The desk function now builds one wrapper and passes it to both services. It still returns the same two lines, Golf booked and the Golf late fee.

Payoff

Step 10: Switch to PayLane again Payoff

Now we make the switch to Pay Lane again. We add Pay Lane Payments, which implements Payments and takes the desk account in its constructor. Its charge sends dollars to Pay Lane and returns true, or returns false when Pay Lane throws. The desk function passes the new wrapper, and that is the only line outside it that changed. Booking Service, Return Desk and the Payments interface did not change at all. It returns the same two lines, now paid through Pay Lane.

Step 11: The vendor lives in one class Payoff

The Pay Lane field and its call and its catch used to sit in both business classes. Now you can see the field here, the send call here, and the catch here, once, inside the wrapper. This is the Adapter pattern, because each wrapper turns a vendor's interface into the one our code asked for. So a vendor switch now costs one new class and one line in the desk function.

Recap

Step 12: Where we started, where we ended Before / after

On the left, both business classes made a Card Pay client, charged it in cents, and checked its status string. On the right, Booking Service only knows the Payments interface and asks it to charge dollars. Card Pay Payments is the one class that calls Card Pay, with the cents math and the status check inside it. So the vendor's names and units live in one class instead of in every class that takes money.

Step 13: When to reach for it Takeaways

Reach for it when a vendor library is called from more than one business class, or when a switch of vendors is likely. Skip it for code that is stable and everywhere, like the built-in Map and Array, because the extra class hides nothing worth hiding. You'll spot the need by vendor class names, keys, units or status strings showing up in more than one business class.