Coupling: wrap a third-party library
Keep a vendor's library behind one class of yours, so a switch touches one class.
Watch the narrated video
Every step
Problem
Steps 1–3Refactor
Steps 4–9Payoff
Steps 10–11Recap
Steps 12–13The code: before and after
Before
class BookingService
{
private readonly CardPayClient pay = new("desk-key");
private readonly Dictionary<string, int> rates =
new() { ["Golf"] = 40, ["Polo"] = 35 };
public string Book(string car, int days)
{
int total = rates[car] * days;
CardPayResult r = pay.Charge(total * 100L, "USD");
if (r.Status != "succeeded") return "declined";
return $"{car} booked, {total} USD";
}
}
class ReturnDesk
{
private readonly CardPayClient client = new("desk-key");
public string LateFee(string car, int hours)
{
int fee = hours * 15;
CardPayResult result = client.Charge(fee * 100L, "USD");
if (result.Status != "succeeded") return "declined";
return $"{car} late fee, {fee} USD";
}
}
static class Desk
{
public static void Main()
{
Console.WriteLine(new BookingService().Book("Golf", 3));
Console.WriteLine(new ReturnDesk().LateFee("Golf", 2));
}
}After
interface IPayments
{
bool Charge(int dollars);
}
class CardPayPayments(string apiKey) : IPayments
{
private readonly CardPayClient pay = new(apiKey);
public bool Charge(int dollars)
{
CardPayResult r = pay.Charge(dollars * 100L, "USD");
return r.Status == "succeeded";
}
}
class PayLanePayments(string account) : IPayments
{
private readonly PayLane lane = new(account);
public bool Charge(int dollars)
{
try
{
lane.Send(dollars, "USD");
return true;
}
catch (PayLaneException)
{
return false;
}
}
}
class BookingService(IPayments payments)
{
private readonly Dictionary<string, int> rates =
new() { ["Golf"] = 40, ["Polo"] = 35 };
public string Book(string car, int days)
{
int total = rates[car] * days;
if (!payments.Charge(total)) return "declined";
return $"{car} booked, {total} USD";
}
}
class ReturnDesk(IPayments payments)
{
public string LateFee(string car, int hours)
{
int fee = hours * 15;
if (!payments.Charge(fee)) return "declined";
return $"{car} late fee, {fee} USD";
}
}
static class Desk
{
public static void Main()
{
IPayments payments = new PayLanePayments("desk-account");
Console.WriteLine(new BookingService(payments).Book("Golf", 3));
Console.WriteLine(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. Desk is the caller, and the run prints 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 run prints 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 prints 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 run prints 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 primary constructor takes the API key, and charge is now public 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 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 the Payments interface, with the one charge method our desk needs. Card Pay Payments implements it, and both fields are now typed as the interface. 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 a primary constructor instead. Desk now builds one wrapper and passes it to both services. The run still prints 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 the interface and takes the desk account in its primary constructor. Its charge sends dollars to Pay Lane and returns true, or returns false when Pay Lane throws. Desk 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. The run prints 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 Desk.
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 libraries that are stable and everywhere, like the standard collections, 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. Dot net already does this with I Logger, which puts one logging interface in front of libraries like Serilog.