Structural Design Pattern: Adapter

Adapter pattern enables integration of new interfaces with existing implementations, allowing reuse of legacy code without rewriting them for newer interfaces.

Photo by Claudio Schwarz on Unsplash

This article is part of a series exploring design patterns using the Java programming language. The goal of this series is to help readers develop a solid understanding of design patterns while also sharing real-world examples from actual codebases that make use of these patterns.

In this article, we’ll be discussing the Adapter pattern.

Symbols for better navigation:

🤔 Hypothetical Scenario/Imagination

⚠️ Warning

👉 Point to note

📝 Common Understanding

Adapter Pattern

The adapter pattern is one of my favourite design patterns as it allows developers to reuse the logic of older implementations with newer interfaces.

The adapter pattern is mainly used to enable existing code to be used with newer APIs.

🤔 For example, let’s suppose your API infrastructure was originally written using the RPC (Remote Procedure Call) protocol. However, the new system requires it to be written over HTTP. Instead of rewriting the business logic for HTTP, you can create an adapter or translator that converts the HTTP request into an RPC request.

This is the whole idea of the adapter pattern 👉 Convert the input in the newer format into the older format so that the system can understand and process it to produce the required output.

A very common example of an adapter we can see in the Java programming language is how it handles input/output streams between bytes and characters.

Java
InputStreamReader reader = new InputStreamReader(System.in);

Here, InputStreamReader acts as an adapter between the bytes coming from the System.in and the characters that are read by the reader.

Recipe to cook the Adapter Pattern for objects

📝 Making an adapter for an object is quite simple. It requires a few steps:

  1. Create an adapter class that implements the interface of the target class you want to adapt to.
  2. In the constructor of the adapter class, pass in the object of the old interface type.
  3. Implement the methods in the adapter class that redirect to the appropriate methods in the old class implementation.

Case-Study

🤔 Let’s take an example of a hypothetical company that is currently rewriting its logic to manage employee details in the company’s Master Data record.

The newer API that the company is working on is faster and more reliable. However, the company has to rewrite many interfaces to suit the overall design of the new application.

Because of this, the Employee object is now replaced with NewEmployee. However, there is a lot of logic that remains common to both Employee and NewEmployee. For example, fields like id, name, etc., will remain common to both classes.

The Employee class looks like this.

Java
interface Employee {
  int getId();
  String getFirstName();
  String getSurname();
}

Post update of the system, the application accepts the ID of the employee in String format and Surname field is replaced with the lastname as follows. Additionally, a new method is added for getting a value hash.

Java
interface NewEmployee {
  String getEmployeeId();
  String getFirstName();
  String getLastName();
  String getValueHash()
}

These changes only affect the application level; however, the data of the existing employees still remains the same. Hence, the Employee object will tend to return the same literal values as the NewEmployee.

The challenge here is: how do we reuse the logic written everywhere using the Employee interface as the type, so that we don’t have to rewrite all the logic for the NewEmployee objects?

👉 This is where we create a special class called NewEmployeeAdapter. It can be designed simply as follows:

Java
public class NewEmployeeAdapter implements NewEmployee {
  Employee emp;
  
  public NewEmployeeAdapter(Employee emp) {
    this.emp = emp;
  }
  
  String getEmployeeId() {
    return new String(this.emp.getId());
  }
  
  String getFirstName() {
    return this.emp.getFirstName();
  }
  
  String getLastName() {
    return this.emp.getSurname();
  }
  
  String getValueHash() {
    return null;
  }
}

Post the creation of the adapter, we can easily reuse the logic of the older implementation for the current use.

Java
public class AdapterUseDemo {
  public static void main(String[] args) {
    Employee oldEmployee = Db.getEmployee("123");
    
    NewEmployeeAdapter employee = new NewEmployeeAdapter(oldEmployee);
    employee.getLastName(); // returns surname
    employee.getEmployeeId(); // returns id
    employee.getValueHash(); // returns null -- missing in old interface
  }
}

Real-World Use Case of Adapter

Let’s take a real-world example of an adapter: KeyAdapter in Java JDK.

KeyAdapter is a class in the java.awt.event package that implements the KeyListener interface.

It provides empty implementations of all the interface methods, so you can override only the ones you need.

In Java’s AWT/Swing event model, every listener interface often has multiple methods you must implement.

For example, KeyListener defines three:

Java
public interface KeyListener extends EventListener {
    void keyTyped(KeyEvent e);
    void keyPressed(KeyEvent e);
    void keyReleased(KeyEvent e);
}

If you want to respond only to a key press, you’d still be forced to implement all three.

KeyAdapter implements KeyListener but provides default method bodies. You can extend it and override only the method(s) you need.

Java
public class MyKeyHandler extends KeyAdapter {
    @Override
    public void keyPressed(KeyEvent e) {
        System.out.println("Key pressed: " + e.getKeyChar());
    }
}

The real-world code from the official JDK source code is as follows.

Up Next in the Series

Composite Design Pattern

Subscribe to my newsletter today!

Share it on

Leave a Reply

Your email address will not be published. Required fields are marked *