java – What is the Adapter pattern?

Question:

What is and how the Adapter pattern in Java works, I'm trying to understand this pattern because I'm going to use it in a project.

Answer:

In short, the Adapter design pattern consists of adapting the interface of a class or object to be used in another way, but without changing the interface or implementation.

Example: persistence in different services

A common case is when we develop client code that intends to use several heterogeneous fonts.

For example, you have multiple implementations capable of reading and writing files from multiple services:

class DropBox {
    void upload(DropBoxFile dbFile) {}
    DropBoxFile download(String id) {} 
}

class AWS {
    void save(InputStream input, int id) {}
    InputStream restore(int id) {} 
}

class GoogleDrive {
    void send(byte[] data, String name) {}
    byte[] get(String name) {} 
}

Each of these classes is in a different library or project. You don't want to make changes or duplicate the code.

Now imagine that you have a system where the user can decide which service the data will be saved to. One way out is to write procedural code full of if and else s each time the system needs to use one of the available services, for example:

if (dropbox) {
  //faz alguma coisa
} else if (aws) {
 //faz outra
} else if (drive) {
 // outra ainda
}

But you won't want this multiple times in the system, plus the code needed to convert system types to service-specific types. Think about how much maintenance it would take to add a new service

So we can define a persistence interface:

interface Persistencia {
    void gravar(File file);
    File ler(String id);
}

The entire system would be implemented using just this interface. Wonderful from an Object Orientation point of view, no if s, no need to change code if any service changes.

But the problem is not completely resolved. The classes of services that we don't want to change don't implement our interface.

So, for each service, we must implement an adapter. For example:

class DropBoxAdapter implements Persistencia {
  DropBox dropBox;
  DropBoxAdapter(DropBox dropBox) {
    this.dropBox = dropBox;
  }
  void gravar(File file) {
    dropBox.upload(new DropBoxFile(file.getAbsolutePath());
  }
  File ler(String id) {
    DropBoxFile dbFile = dropBox.download(id);
    return new File(dbFile.getLocalPath());
  }
}

Note that we just created a class that allows us to use a DropBox object using the Persistencia interface. We adapt the original class to the desired interface.

Other implementations of adapters should then be provided for the other services using the same principle.

Example: improving legacy code

Now imagine that we have to maintain a poorly designed system that implements logs manually using the following class:

class HomeMadeLog {
    public void log(int nivel, String mensagem, Throwable erro) {
        String s = formatarLog(nivel, mensagem, erro);
        adicionaLogNoArquivo(s);
    }
    private String formatarLog(...) { }
    private String adicionaLogNoArquivo(...) { }
}

We need to modernize the logs using a framework like Log4j, but there are thousands of calls to the old method and many of them are too complicated to simply do an automatic replacement.

Furthermore, we want to avoid having to change 99% of the project files, as this would create a big problem of conflicts in the version control system for all other developers.

One solution is to extend the HomeMadeLog class so that it is an adapter to Log4j. Example:

class HomeMadeLogToLog4jAdaptor extends HomeMadeLog {
    Logger logger = Logger.getLogger();
    @Override
    public void log(int nivel, String mensagem, Throwable erro) {
        if (nivel == 0) logger.debug(mensagem, erro);
        else if (nivel == 1) logger.info(mensagem, erro);
        else if (nivel == 2) logger.error(mensagem, erro);
    }
}

Now just provide an instance of HomeMadeLogToLog4jAdaptor instead of HomeMadeLog and all classes will work with Log4j without modification.

Considerations

Note that the main asset of the Adapter design pattern is that it allows consistent code reuse while maintaining compatibility with other libraries and earlier versions of code.

An adapter class is nothing more than a class that implements the interface we want to use and delegates the actual execution to a third class that has the implementation we want to use.

Also note that my examples are purposefully simplified. Some details and complexities that are unrelated to the pattern itself have been omitted so as not to complicate the examples.

Scroll to Top