Adapter Pattern with Real world example
adapter-pattern

Why do we need Adapter pattern?

Check out the following conversation between two programmers.

Programmer 1: Hey, we need to integrate a new shipping service into our e-commerce platform. The problem is that it uses a completely different API than our existing shipping service.

Programmer 2: I see. We don't want to modify our existing codebase and create a new codebase for the new shipping service.

Programmer 1: Exactly. Do you have any idea how we can integrate the new shipping service without changing our existing code?

Solution - Lets use Adapter pattern

The Adapter pattern is a structural design pattern that allows incompatible interfaces to work together by wrapping an object with another object that acts as a translator or adapter between the incompatible interfaces.

In other words, the Adapter pattern is used when we have two interfaces that are incompatible with each other and we want to use an object that implements one of the interfaces with the other interface. Instead of modifying the existing objects, we can create a new object that adapts the existing object's interface to the required interface.

The Adapter pattern consists of three main components:

Target Interface: This is the interface that the client code expects to work with.

Adaptee: This is the existing interface that needs to be adapted to work with the Target Interface.

Adapter: This is the object that adapts the Adaptee's interface to the Target Interface.

adapter-pattern-components

Implementation

The Adapter pattern can be implemented in two ways:

Class Adapter: In this approach, the Adapter class extends the Adaptee class and implements the Target Interface. This approach requires multiple inheritance, which may not be possible in some programming languages.

Object Adapter: In this approach, the Adapter class holds a reference to the Adaptee object and implements the Target Interface. This approach uses composition instead of inheritance and is preferred in most cases.

Lets take below example using Object Adapter


// Target interface
public interface IShippingService
{
    decimal CalculateShippingCost(string address);
}

// Adaptee interface
public interface IThirdPartyShippingAPI
{
    float GetShippingCost(object shippingInfo);
}

// Adaptee class
public class ThirdPartyShippingAPI : IThirdPartyShippingAPI
{
    public float GetShippingCost(object shippingInfo)
    {
        // Call the third-party API and get the shipping cost
        return 10.0f; // return a dummy value for demonstration purposes
    }
}

// Adapter class
public class ThirdPartyShippingAPIAdapter : IShippingService
{
    private IThirdPartyShippingAPI _thirdPartyShippingAPI;

    public ThirdPartyShippingAPIAdapter(IThirdPartyShippingAPI thirdPartyShippingAPI)
    {
        _thirdPartyShippingAPI = thirdPartyShippingAPI;
    }

    public decimal CalculateShippingCost(string address)
    {
        // Convert the address to the shipping info object expected by the third-party API
        object shippingInfo = new object();

        // Call the GetShippingCost method of the third-party API using the converted shipping info object
        float cost = _thirdPartyShippingAPI.GetShippingCost(shippingInfo);

        // Convert the shipping cost to decimal and return it
        return (decimal)cost;
    }
}

Challenges & Limitations

Complexity: Implementing an adapter can add additional complexity to the codebase, especially if the Adaptee has a large and complex interface. Careful design and implementation are needed to ensure that the adapter is as simple and easy to use as possible.

Performance: If the adapter has to perform a lot of data conversions or other complex operations, it can negatively impact the performance of the application. It's important to carefully measure and optimize the adapter code to ensure that it doesn't have a significant impact on performance.

Tight coupling: The Adapter pattern can sometimes result in tight coupling between the adapter and the Adaptee, especially if the Adaptee's interface changes frequently. This can make it difficult to maintain and update the adapter code.

Limited functionality: The Adapter pattern can only be used to adapt the interface of an existing object or class. It cannot be used to add new functionality or modify the behavior of the object or class.

Overuse: It's important to use the Adapter pattern judiciously and only when it's really necessary. Overuse of the pattern can lead to unnecessary complexity and make the codebase harder to maintain.

Summary

  • The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to work together.
  • The pattern involves three main components: the Target interface, the Adaptee interface, and the Adapter class that adapts the Adaptee interface to the Target interface.
  • The Target interface is the interface that the client code expects to interact with. The Adaptee interface is the interface of the object that needs to be adapted.
  • The Adapter class acts as a bridge between the client code and the Adaptee object, and translates requests from the client into a format that the Adaptee can understand.
  • The Adapter pattern can be useful in situations where you need to use an existing object with a different interface, or when you want to make two incompatible objects work together.
  • Some common challenges and limitations of the Adapter pattern include complexity, performance, tight coupling, limited functionality, and overuse.