Adapter design pattern is a structural design pattern which is used to connect new code with legacy code without changing the contracts created earlier.

What is Adapter Pattern?

Adapter design pattern connects two incompatible interfaces by fitting between them and providing a compatible interface to both.

Imagine that you’ve a data integration application which collects data from various systems and integrates into single database. You can also take a scenario where you have an existing application which uses data in one format but now want to integrate with another format data which is available through third party. In this case, the existing code was using an interface to operate with User data. However, the new system does not provide the data in the same format, then how do you integrate this data into single database?

The solution is to create an adapter which translates data in the format provided by the third party into the format that our system is expecting. We cannot change the code by third party provider so we have to always add it in our code. We could easily create a function to integrate with new format, however, that would be less maintainable because it will break the single-responsibility principle. It is better to create a new class with this functionality.

This design pattern is used when client wants to communicate with existing application interface. This is usually useful for integrating with legacy systems. This is client centric design pattern. The main parts of this pattern include below:

  • Target class is the class that the client expects the object to implement.
  • Adaptee Class is the existing class with incompatible interface.
  • Adapter class is the class that implements target interface and converts the calls to the method of the adaptee class.

How to implement Adapter Pattern?

There are two ways to implement adapter pattern. (1) using object Adapter and (2) using class adapter

1. Object Adapter:

In this case, the adapter class holds and instance of the adaptee class and delegates the calls to its methods. This is more flexible as it does not depend on inheritance relationship and can be used generally.

Below is an example of object adapter pattern.

Let’s say we have existing users which we can retrieve using ExistingUserRepository. Below is my User interface to model all users in my application.

1public interface User {
2    int getId();
3    String getFirstName();
4    String getLastName();
5    String getEmail();
6}

We also have ExistingUser which implements this User interface. For brevity, I have excluded all unnecessary code like setters, hashCode() and equals() methods and additional business logic methods.

 1public class ExistingUser implements User {
 2    private int id;
 3    private String firstName;
 4    private String lastName;
 5    private String email;
 6
 7    public ExistingUser(int id, String firstName, String lastName, String email) {
 8        this.id = id;
 9        this.firstName = firstName;
10        this.lastName = lastName;
11        this.email = email;
12    }
13
14    public int getId() {
15        return id;
16    }
17
18    public String getFirstName() {
19        return firstName;
20    }
21
22    public String getLastName() {
23        return lastName;
24    }
25
26    public String getEmail() {
27        return email;
28    }
29
30    @Override
31    public String toString() {
32        return "User{" +
33                "id=" + id +
34                ", firstName='" + firstName + '\'' +
35                ", lastName='" + lastName + '\'' +
36                ", email='" + email + '\'' +
37                '}';
38    }
39}

Now, let’s assume we have ExistingUserRepository with method getAllUsers() which returns all existing users. For the demo, I have written them in a simple List<User> but in reality, this data might come from backend database.

 1public class ExistingUserRepository {
 2
 3    public final List<User> getAllUsers() {
 4        List<User> users = new ArrayList<>();
 5        User user1 = new ExistingUser(1, "John", "Doe", "john.doe@gmail.com");
 6        User user2 = new ExistingUser(2, "Jane", "Doe", "jane.doe@hotmail.com");
 7        User user3 = new ExistingUser(3, "Alice", "Smith", "andrew@yahoo.com");
 8        users.addAll(List.of(user1, user2, user3));
 9        return users;
10    }
11}

The DataClient can simply retrieve all users using this repository and life is good.

 1public class DataClient {
 2    private final ExistingUserRepository exitingUserRepository;
 3    private final NewAcquisitionUserRepository newAcquisitionUserRepository;
 4    private final ThirdPartyUserRepository thirdPartyUserRepository;
 5
 6    public DataClient(ExistingUserRepository exitingUserRepository) {
 7        this.exitingUserRepository = exitingUserRepository;
 8    }
 9
10    public List<User> getAllUsers() {
11        List<User> users = exitingUserRepository.getAllUsers();
12
13        return users;
14    }
15}

Now, let’s say we get users from another third party API, however the problem is that they provide the users we acquired in their own format. Their data model for User do not have the same interface as what we are using. As you can notice from below class that this model does not include firstName or lastName, but they have user’s name in single name field. Similarly, email is stored in emailAddress field. So, it does not directly implement User interface.

 1public class UserFromThirdParty {
 2    private int id;
 3    private String name;
 4    private String emailAddress;
 5
 6    public UserFromThirdParty(int id, String name, String emailAddress) {
 7        this.id = id;
 8        this.name = name;
 9        this.emailAddress = emailAddress;
10    }
11
12    public int getId() {
13        return id;
14    }
15
16    public String getName() {
17        return name;
18    }
19
20    public String getEmailAddress() {
21        return emailAddress;
22    }
23
24    @Override
25    public String toString() {
26        return "UserThirdParty{" +
27                "id=" + id +
28                ", name='" + name + '\'' +
29                ", emailAddress='" + emailAddress + '\'' +
30                '}';
31    }
32}

Now, for retrieving these users, we have defined ThirdPartyUserRepository like below. Again for brevity, I have excluded unnecessary code and represented this in very simple format.

1public class ThirdPartyUserRepository {
2    public List<UserFromThirdParty> getAllUsers() {
3        UserFromThirdParty user1 = new UserFromThirdParty(1, "Rocky Balboa" , "rocky.rocks@gmail.com");
4        UserFromThirdParty user2 = new UserFromThirdParty(2, "John Rambo", "rambo@example.com");
5        List<UserFromThirdParty> users = List.of(user1, user2);
6        return users;
7    }
8}

Now, if we directly try to add these users to our List<User>, it will not compile because User and UserFromThirdParty do not have same interface. This is where we will have to define an adapter which will convert UserFromThirdParty into User.

 1public class ThirdPartyUserAdapter implements User {
 2    private final UserFromThirdParty userFromThirdParty;
 3
 4    public ThirdPartyUserAdapter(UserFromThirdParty userFromThirdParty) {
 5        this.userFromThirdParty = userFromThirdParty;
 6    }
 7
 8    @Override
 9    public int getId() {
10        return userFromThirdParty.getId();
11    }
12
13    @Override
14    public String getFirstName() {
15        return userFromThirdParty.getName().split(" ")[0];
16    }
17
18    @Override
19    public String getLastName() {
20        return userFromThirdParty.getName().split(" ")[1];
21    }
22
23    @Override
24    public String getEmail() {
25        return userFromThirdParty.getEmailAddress();
26    }
27
28    @Override
29    public String toString() {
30        return "ThirdPartyUserAdapter{" +
31                "userFromThirdParty=" + userFromThirdParty +
32                '}';
33    }
34}

With this adapter in place, we can simply convert the UserFromThirdParty into User and our DataClient can easily combine these new users with the existing users.

 1public class DataClient {
 2    private final ExistingUserRepository exitingUserRepository;
 3    private final ThirdPartyUserRepository thirdPartyUserRepository;
 4
 5    public DataClient(ExistingUserRepository exitingUserRepository, ThirdPartyUserRepository thirdPartyUserRepository) {
 6        this.exitingUserRepository = exitingUserRepository;
 7        this.thirdPartyUserRepository = thirdPartyUserRepository;
 8    }
 9
10    public List<User> getAllUsers() {
11        List<User> users = exitingUserRepository.getAllUsers();
12
13        // to get users from third party, we need ThirdParty to ExistingUser adapter
14        List<UserFromThirdParty> thirdPartyUsers = thirdPartyUserRepository.getAllUsers();
15//        users.add(thirdPartyUsers.get(0)); // This does not work, we need to convert UserFromThirdParty to User
16        List<User> usersFromThirdParty = thirdPartyUsers.stream()
17                .map(ThirdPartyUserAdapter::new)
18                .collect(Collectors.toList());
19        users.addAll(usersFromThirdParty);
20
21        return users;
22    }
23}

If we want to add any other kind of users, we will simply have to define Adapter class for the new source system and convert them into User model. We will not need to modify the client code. When using adapter with target class, the client calls adapter class methods.

2. Class Adapter:

In this the adapter class inherits from the adaptee class and implements the target interface. This is useful when there is inheritance relationship between two objects.

For example, let’s assume we have an existing music system which can play all modern file formats like mp4. It has method which can handle playing audio from those file formats. We have an interface MusicPlayer which defines the contract.

1public interface MusicPlayer {
2    void playMusic(String musicType, String fileName);
3}

We have concrete implementation of this interface which defines this method.

 1public class Mp4MusicPlayer implements MusicPlayer {
 2    @Override
 3    public void playMusic(String musicType, String fileName) {
 4        if (musicType.equalsIgnoreCase("mp4")) {
 5            System.out.println("Playing mp4 file. Name: " + fileName);
 6        } else {
 7            throw new UnsupportedOperationException("Unsupported music type: " + musicType);
 8        }
 9    }
10}

Our music system gets popular and we want to now support legacy music as well. We had a vendor providing a legacy music capability with their own interface. They have a class LegacyMusicPlayer which can handle this operation for us.

1public class LegacyPlayer {
2    public void playVlcMusic(String fileName) {
3        System.out.println("Playing vlc file. Name: " + fileName);
4    }
5}

In this case, we know LegacyPlayer is a kind of MusicPlayer even though it has a different interface than our modern music player. In this case, we have to write adapter but because we are naturally in this inheritance relationship, we can define adapter which returns MusicPlayer but extends LegacyPlayer which means we can use existing capability of the LegacyPlayer to play VLC music. Here, we can use class adapter.

 1public class LegacyMusicAdapter extends LegacyPlayer implements MusicPlayer {
 2
 3    @Override
 4    public void playMusic(String musicType, String fileName) {
 5        if (musicType.equalsIgnoreCase("vlc")) {
 6            playVlcMusic(fileName);
 7        } else {
 8            throw new UnsupportedOperationException("Unsupported music type: " + musicType);
 9        }
10    }
11}

With this in place, our AudioClient can play both types of music.

 1public class AudioClient {
 2    private MusicPlayer musicPlayer;
 3
 4    public AudioClient(MusicPlayer musicPlayer) {
 5        this.musicPlayer = musicPlayer;
 6    }
 7
 8    public void playMusic(String musicType, String fileName) {
 9        musicPlayer.playMusic(musicType, fileName);
10    }
11
12    public void play() {
13        MusicPlayer mp4MusicPlayer = new Mp4MusicPlayer();
14        MusicPlayer legacyMusicAdapter = new LegacyMusicAdapter();
15
16        AudioClient audioClient = new AudioClient(mp4MusicPlayer);
17        audioClient.playMusic("mp4", "file1.mp4");
18
19        audioClient = new AudioClient(legacyMusicAdapter);
20        audioClient.playMusic("vlc", "file2.vlc");
21    }
22}

Advantages:

  • It increases usability by allowing you to use existing clases which may be incompatible.
  • Provides a way to integrate two systems which have different interfaces. This promotes code reusability.
  • It makes code more readable by keeping adaption code in separate classes keeping code clean and maintainable.

Disadvantages:

  • This requires a brand new adapter class for each system we want to integrate with resulting in more number of classes.
  • Having this extra layer of abstraction for adapter can introduce little overhead as we need new instance of adapter for every class we want to integrate.

Summary

  • Adapter pattern provides a simple solution to common problem.
  • This is easy to implement, we simply have to add a new Adapter class which handles the conversion from one interface to another.