Introduction to Adapter design pattern

During your journey through life as a software developer, you will face various problems. Naturally, you want to follow the best practices while writing code and make life easier for yourself and your colleagues. Knowledge of software design patterns which are reusable solutions of common problems in software development, is of great value.

Adapter design pattern is a structural design pattern which enables two classes with different interfaces but same underlying functionality to work together. It converts the interface of one class to another expected interface.

You are working on expanding a feature in your application and you want to use reuse some legacy class. However, that legacy class does not provide the interface you require and you do not want to change it. In this situation, adapter pattern is a good solution.

In another scenario, you are required to switch from an internal component to an external one which provides the same functionality but is better suited. The issue you run into is that the interface of the external component is slightly different. Your existing component is used all across the project and replacing it requires code modifications in multiple places which could easily lead to bugs. Using the adapter pattern, you can write an adapter class which will wrap around an external component and provide the required interface. In that way, the client does not know they are interacting with a different underlying component, and our existing component can now be replaced with the adapter. How do we implement it?

To summarize the issue, objects that participate in adapter pattern are often defined as following:

  • Target Interface – This is the required interface which client expects
  • Adaptee – Class which has an incompatible interface and has to be adapted
  • Adapter – Class that wraps around the adaptee, implements the target interface and delegates the requests to the adaptee
  • Client – Class which uses and interacts with the adapter class

It is worth mentioning that there are two types of adapter implementations which use different approaches. Class adapters use inheritance for implementation and are mostly used in languages that support multiple inheritances. Object adapters use composition to achieve the same results and are a common way of implementation, since they follow the composition over inheritance principle. This tech bite shows the implementation of an object adapter through the example below.

Example

You are working on a big project which involves multiple services/applications whose usage statistics have to be analyzed. Your application uses the following interface to obtain data from specific services. This is our target interface:

public interface IAppStatistics {
    Map<String, List<String>> getUserActivity(List<User> users, Date fromDate, Date untilDate)
    List<Triple<String, String, String>> getBrowserUsage(List<Pair<String,String>> browsers)
    Map<String, String> getGeneralAppStatistics();
    String getErrorSummary(String errorType, Date fromDate, Date untilDate)
}

A new service has been added to the ecosystem, developed by an external team who has already provided an interface for fetching all the data you need, which looks like this:

interface IExternalServiceStatistics {
    Map<String, List<String>> getUserActivity(List<String> userIds, Date fromDate, Date untilDate)
    String getBrowserActivity(String browser, String os)
    Map<String, String> getGeneralStatistics();
    String getErrorSummary(String errorType, Date fromDate, Date untilDate)
}

At our disposal, we also have the ExternalServiceStatistics class which implements the IExternalServiceStatistics interface. This is the adaptee class which we need to adapt for our own use.

Before writing our Adapter class, we need to check the differences in the two interfaces:

  • Method getUserActivity of ExternalServiceStatistics class receives a list of user ID’s rather than a list of User objects.
  • Method getBrowserActivity differs by return type as well as method parameters.
  • Method getGeneralAppStatistics is differently named in the ExternalServiceStatistics class.
  • Method getErrorSummary has same signature and can be delegated without any changes.

Once we know what we need to adapt, we can now write the adapter class. We are going to achieve this through composition. Our adapter class called ExternalServiceStatisticsAdapter will have one property, which is the adaptee and will implement our target interface IAppStatistics:

public class ExternalServiceStatisticsAdapter implements IAppStatistics {
    // Instance of adaptee class
    ExternalServicesStatistics adaptee;

    public ExternalServiceStatisticsAdapter(ExternalServicesStatistics adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public Map<String, List<String>> getUserActivity(List<User> users, Date fromDate, Date untilDate) {
        // Extract list of IDs for adaptee method call
        final List<String> userIds = users.stream().map(user -> user.getId()).collect(Collectors.toList());
        return adaptee.getUserActivity(userIds, fromDate, untilDate);
    }

    @Override
    public List<Triple<String, String, String>> getBrowserActivity(List<Pair<String,String> browsers) {
        // Note: for this you need org.apache.commons.lang3 dependency
        final List<Triple<String, String, String>> results = new ArrayList<>();
        browsers.forEach(pair -> results.add(Triple.of(pair.getLeft(), pair.getRight(), adaptee.getBrowserActivity(pair.getLeft(), pair.getRight())));
        return results;
    }

    @Override
    public Map<String, String> getGeneralAppStatistics() {
        //Just a different name
        return adaptee.getGeneralStatistics();	
    }

    @Override
    public String getErrorSummary(String errorType, Date fromDate, Date untilDate) {
        //Method signature matches, just delegate it to adaptee
        return adaptee.getErrorSummary(errorType, fromDate, untilDate)	
    }
}

Now our client class can use the ExternalServiceStatisticsAdapter class to obtain data using the desired interface.


“Adapter Pattern in Java” Tech Bite was brought to you by Mirza Mesihović, Software Engineer at Atlantbh.

Tech Bites are tips, tricks, snippets or explanations about various programming technologies and paradigms, which can help engineers with their everyday job.

Leave a Reply