Adapter design pattern

Adapter design pattern #

Name: Adapter

Type: Structural

Intent: #

“Convert the interface of a class into another interface the client expects. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.”

In other words, this pattern makes incompatible interfaces compatible.

When to use: #

Sometimes, objects just don’t fit together as they should. Either the object is too complex to work with or their interfaces are incompatible. The adapter design pattern lets you adapt what an object or class has to offer, by converting the interface of a class into another one. There are some common use cases like: integrating external libraries with different interfaces, working with legacy systems or adapting data formats

Real-life analogy #

One of the real-life encounters with an Adapter is with headphone jacks. These jacks aren’t universal, and we won’t find the same jacks in all phone models. In these cases we might need adapters to convert a 3.5mm jack to a USB-C or Lightning connector, allowing headphones to work with different devices.

In all of these cases, the adapter is a middleman that translates the signals or data from one system to another, making it possible to connect incompatible objects or interfaces.

Imagine two people who speak different languages trying to have a conversation. The Adapter acts like a helpful translator, listening to one person (object 1) in their language and then seamlessly converting their message into terms understandable by the other person (object 2). This allows them to communicate effectively despite their initial incompatibility.

Structure #

For the Adapter design pattern, we have three main entities involved.

  • The interfaces are the abstractions that need to be made compatible.
  • The Adaptee - is the original class that needs to be adapted. In our case, the iPhone needs to be charged with a different charger
  • The Adapter - LightningToTypeCAdapter is the class that knows how to talk with both the client and the Adaptee(i.e knows how to adapt a Lightning port to type C one)

You can see in the diagram below the interaction between classes.

Implementation #

 public interface ITypeCPort
 {
     void Recharge();
     void UseTypeC();
 }

 public interface ILightningPort
 {
     void Recharge();
     void UseLightning();
 }

Then we have an IPhone that implements the ILightningPort.

 public class IOSPhone : ILightningPort
    {
        private bool isConnected = false;

        public void UseLightning()
        {
            isConnected = true;
            Console.WriteLine("Lightning connection established");
        }


        public void Recharge()
        {
            if (isConnected)
            {
                Console.WriteLine("Recharging iPhone...");
            }
            else
            {
                Console.WriteLine("Please connect Lightning first");
            }
        }
    }

Then we need to have an adapter.

 public class LightningToTypeCAdapter : ITypeCPort
 {
     private ILightningPort lightningPhone;

     public LightningToTypeCAdapter(ILightningPort lightningPhone)
     {
         this.lightningPhone = lightningPhone;
     }

     public void UseTypeC()
     {
         Console.WriteLine("Adapter converts Type-C to Lightning");
         lightningPhone.UseLightning();
     }

     public void Recharge()
     {
         lightningPhone.Recharge();
     }
 }

From the client's perspective, now we can have an iPhone, use an adapter and recharge our phone.


IOSPhone myIPhone = new IOSPhone();
LightningToTypeCAdapter typeCAdapter = new LightningToTypeCAdapter(myIPhone);

Console.WriteLine("Attempting to recharge iPhone with Type-C via adapter:");
typeCAdapter.UseTypeC();
typeCAdapter.Recharge();

Structurally speaking, the Adapter design pattern is very similar to the Proxy design pattern. But unlike Proxy, The Adapter pattern is visible to the client, and the client knows that it interacts with an adapter.

Pros & Cons #

Pro #

  • enables integration of systems with incompatible interfaces with the rest of the codebase
  • Code reuse - can reuse existing functionality that is not compatible with the desired interfaces by using adapters
  • enhances flexibility and maintainability by decoupling the client code from specific implementations of the Adaptees
  • promotes ** loose coupling **
  • Extensibility : adapters can be easily extended to accommodate new interfaces without modifying the adaptee code
  • Reusability: Adapters can be reused with different Adaptees
  • Open/ Closed principle - we can introduce new adapter types without modifying existing code
  • the Adapter pattern hides the implementation details of the Adaptee, shielding client code from the complexities of its internal workings. This promotes abstraction and makes the code more maintainable and testable.

cons: #

  • can introduce complexity in the system due to adapter classes, and the code can be harder to understand
  • Requires thoughtful design considerations to ensure that adapters are appropriately implemented and don't introduce unintended consequences.
  • Requires more testing

Summary #