Skip to Content
Course content

194: The Adapter Pattern in Practice

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably been there: you're integrating a third-party library or dealing with a legacy module that does exactly what you need, but its interface is just... wrong. It doesn't match your project's current architecture, and the types it uses are alien to the rest of your codebase. Your first instinct might be to just "make it work" wherever you need it, but that's where the technical debt starts piling up.

The Trap of Direct Integration

Let's say we have a modern logging system in our app that expects an ILogger interface. Everything in our business logic uses this interface so we can swap loggers (to console, file, or cloud) without touching the core logic. Now, imagine your boss tells you that you must use a specific, old vendor library for auditing because of legal requirements. The vendor's class, LegacyAuditLog, doesn't know about our ILogger interface; it has its own method called WriteEntry(const char* msg, int priority).

The naive approach is to just instantiate LegacyAuditLog directly inside your services. It looks like this:

// The "quick and dirty" way
void ProcessPayment(PaymentRequest req) {
    // ... business logic ...
    
    // Directly coupling our business logic to a vendor's weird API
    LegacyAuditLog vendorLog; 
    vendorLog.WriteEntry("Payment processed", 1); 
}

I've seen this happen in countless projects. It seems faster in the moment, but you've just poisoned your business logic. Now, your ProcessPayment function depends on a concrete vendor class. If that vendor updates their library and changes WriteEntry to LogMessage, you're not changing one file—you're searching and replacing across your entire project. You've lost the ability to mock the logger for unit tests, and you've leaked vendor-specific types into your high-level logic.

Decoupling via the Adapter

The better way is to treat that legacy library as a black box and build a bridge—the Adapter pattern. Instead of forcing our logic to speak the vendor's language, we create a class that speaks both. We make the Adapter implement our ILogger interface, but internally, it wraps the LegacyAuditLog.

class ILogger {
public:
    virtual ~ILogger() = default;
    virtual void Log(const std::string& message) = 0;
};

// This is the Adapter
class AuditLogAdapter : public ILogger {
private:
    LegacyAuditLog m_legacyLog; // The "Adaptee"

public:
    void Log(const std::string& message) override {
        // We translate the modern call to the legacy format
        // Maybe we map "Log" to a default priority of 1
        m_legacyLog.WriteEntry(message.c_str(), 1);
    }
};

Now, our ProcessPayment function doesn't even know the LegacyAuditLog exists. It just takes an ILogger&. We inject the AuditLogAdapter at the top level of the application. If we ever fire that vendor or move to a different auditing tool, we only have to write one new Adapter class. The rest of the system remains blissfully unaware that anything changed.

The Cost of the Middleman

I'll be honest with you: the Adapter pattern adds a bit of boilerplate. You're creating more classes and adding a layer of indirection. In a tight loop where every nanosecond counts, a virtual function call (which is what ILogger::Log is) has a tiny overhead. But in 99% of application code, that cost is negligible compared to the cost of a developer spending three days refactoring a codebase because a vendor changed an API signature.

The real trade-off here is up-front effort vs. long-term agility. The naive way is a sprint; the Adapter way is a marathon. When you're working on a professional team, always bet on the marathon. It's much easier to remove an adapter later if you realize it's unnecessary than it is to excise a legacy dependency that has spread like a virus through your entire service layer.




📋 Practical Task

Exercise: Integrating the Legacy Fahrenheit Thermal Sensor

You are building a climate control system that operates entirely in Celsius using a IThermometer interface. You have been handed a legacy hardware library for a sensor that only provides readings in Fahrenheit as integers.

Your task:

  1. Implement the IThermometer interface: double getTemperature().
  2. Create a LegacyFahrenheitSensor class (the Adaptee) that has a method int readFahrenheit() which returns a hardcoded value (e.g., 98).
  3. Implement a TemperatureAdapter that wraps the LegacyFahrenheitSensor, calls its method, converts the integer Fahrenheit value to a double Celsius value (Formula: (F - 32) * 5/9), and returns it through the IThermometer interface.
  4. In main, demonstrate that you can treat the adapter as an IThermometer and print the Celsius value.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.