JavaRush /Java Blog /Random EN /What problems does the Adapter design pattern solve?

What problems does the Adapter design pattern solve?

Published in the Random EN group
Software development is often complicated by incompatibility between components that work with each other. For example, if you need to integrate a new library with an old platform written in earlier versions of Java, you may encounter incompatibility of objects, or more precisely, interfaces. What to do in this case? Rewrite the code? But this is impossible: analyzing the system will take a lot of time or the internal logic of the work will be broken. What problems does the Adapter design pattern solve - 1To solve this problem, they came up with the Adapter pattern, which helps objects with incompatible interfaces work together. Let's see how to use it!

More details about the problem

First, let's simulate the behavior of the old system. Let's say it generates reasons for being late for work or school. To do this, we have an interface Excusethat contains the methods generateExcuse(), likeExcuse()and dislikeExcuse().
public interface Excuse {
   String generateExcuse();
   void likeExcuse(String excuse);
   void dislikeExcuse(String excuse);
}
This interface is implemented by the class WorkExcuse:
public class WorkExcuse implements Excuse {
   private String[] reasonOptions = {"по невероятному стечению обстоятельств у нас в доме закончилась горячая вода и я ждал, пока солнечный свет, сконцентрированный через лупу, нагреет кружку воды, чтобы я мог умыться.",
   "искусственный интеллект в моем будильнике подвел меня и разбудил на час раньше обычного. Поскольку сейчас зима, я думал что еще ночь и уснул. Дальше все How в тумане.",
   "предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице."};
   private String[] sorryOptions = {"Это, конечно, не повторится, мне очень жаль.", "Прошу меня извинить за непрофессиональное поведение.", "Нет оправдания моему поступку. Я недостоин этой должности."};

   @Override
   public String generateExcuse() { // Случайно выбираем отговорку из массива
       String result = "Я сегодня опоздал, потому что " + reasonOptions[(int) Math.round(Math.random() + 1)] + "\n" +
               sorryOptions[(int) Math.round(Math.random() + 1)];
       return result;
   }

   @Override
   public void likeExcuse(String excuse) {
       // Дублируем элемент в массиве, чтобы шанс его выпадения был выше
   }

   @Override
   public void dislikeExcuse(String excuse) {
       // Удаляем элемент из массива
   }
}
Let's test the example:
Excuse excuse = new WorkExcuse();
System.out.println(excuse.generateExcuse());
Conclusion:
Я сегодня опоздал, потому что предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице. 
Прошу меня извинить за непрофессиональное поведение.
Now let’s imagine that you launched the service, collected statistics and noticed that the majority of service users are university students. To improve it for the needs of this group, you ordered an excuse generation system from another developer specifically for her. The development team conducted research, compiled ratings, connected artificial intelligence, added integration with traffic jams, weather, and so on. Now you have a library for generating excuses for students, but the interface for interacting with it is different - StudentExcuse:
public interface StudentExcuse {
   String generateExcuse();
   void dislikeExcuse(String excuse);
}
The interface has two methods: generateExcuse, which generates an excuse, and dislikeExcuse, which blocks the excuse so that it does not appear in the future. A third-party library is closed for editing - you cannot change its source code. As a result, in your system there are two classes that implement the interface Excuse, and a library with a class SuperStudentExcusethat implements the interface StudentExcuse:
public class SuperStudentExcuse implements StudentExcuse {
   @Override
   public String generateExcuse() {
       // Логика нового функционала
       return "Невероятная отговорка, адаптированная под текущее состояние погоды, пробки or сбои в расписании общественного транспорта.";
   }

   @Override
   public void dislikeExcuse(String excuse) {
       // Добавляет причину в черный список
   }
}
The code cannot be changed. The current scheme will look like this: What problems does the Adapter - 2 design pattern solve?This version of the system only works with the Excuse interface. You cannot rewrite the code: in a large application, such changes can take a long time or break the application logic. You can suggest introducing the main interface and increasing the hierarchy: What problems does the Adapter design pattern solve - 3To do this, you need to rename the interface Excuse. But the additional hierarchy is undesirable in serious applications: introducing a common root element breaks the architecture. You should implement an intermediate class that will allow you to use new and old functionality with minimal loss. In short, you need an adapter .

How the Adapter pattern works

An adapter is an intermediate object that makes calls to methods on one object understandable to another. Let's implement an adapter for our example and call it Middleware. Our adapter must implement an interface that is compatible with one of the objects. Let it be Excuse. Thanks to this, Middlewareit can call methods of the first object. Middlewarereceives calls and passes them to the second object in a compatible format. MiddlewareThis is what the implementation of a method with methods generateExcuseand looks like dislikeExcuse:
public class Middleware implements Excuse { // 1. Middleware становится совместимым с an objectом WorkExcuse через интерфейс Excuse

   private StudentExcuse superStudentExcuse;

   public Middleware(StudentExcuse excuse) { // 2. Получаем ссылку на адаптируемый an object
       this.superStudentExcuse = excuse;
   }

   @Override
   public String generateExcuse() {
       return superStudentExcuse.generateExcuse(); // 3. Адаптер реализовывает метод интерфейса
   }

    @Override
    public void dislikeExcuse(String excuse) {
        // Метод предварительно помещает отговорку в черный список БД,
        // Затем передает ее в метод dislikeExcuse an object superStudentExcuse.
    }
   // Метод likeExcuse появятся позже
}
Testing (in client code):
public class Test {
   public static void main(String[] args) {
       Excuse excuse = new WorkExcuse(); // Создаются an objectы классов,
       StudentExcuse newExcuse = new SuperStudentExcuse(); // Которые должны быть совмещены.
       System.out.println("Обычная причина для работника:");
       System.out.println(excuse.generateExcuse());
       System.out.println("\n");
       Excuse adaptedStudentExcuse = new Middleware(newExcuse); // Оборачиваем новый функционал в an object-адаптер
       System.out.println("Использование нового функционала с помощью адаптера:");
       System.out.println(adaptedStudentExcuse.generateExcuse()); // Адаптер вызывает адаптированный метод
   }
}
Conclusion:
Обычная причина для работника:
Я сегодня опоздал, потому что предпраздничное настроение замедляет метаболические процессы в моем организме и приводит к подавленному состоянию и бессоннице.
Нет оправдания моему поступку. Я недостоин этой должности. Использование нового функционала с помощью адаптера
An incredible excuse, adapted to the current weather conditions, traffic jams or disruptions in the public transport schedule. The method generateExcusesimply transfers the call to another object, without additional transformations. The method dislikeExcuserequired first placing the excuse on a database blacklist. Additional intermediate data processing is the reason why the Adapter pattern is loved. But what about a method likeExcusethat is in the interface Excuse, but not in StudentExcuse? This operation is not supported in the new functionality. For this case, they came up with an exception UnsupportedOperationException: it is thrown if the requested operation is not supported. Let's use this. This is what the new class implementation looks like Middleware:
public class Middleware implements Excuse {

   private StudentExcuse superStudentExcuse;

   public Middleware(StudentExcuse excuse) {
       this.superStudentExcuse = excuse;
   }

   @Override
   public String generateExcuse() {
       return superStudentExcuse.generateExcuse();
   }

   @Override
   public void likeExcuse(String excuse) {
       throw new UnsupportedOperationException("Метод likeExcuse не поддерживается в новом функционале");
   }

   @Override
   public void dislikeExcuse(String excuse) {
       // Метод обращается за дополнительной информацией к БД,
       // Затем передает ее в метод dislikeExcuse an object superStudentExcuse.
   }
}
At first glance, this solution does not seem successful, but simulating the functionality can lead to a more complex situation. If the client is attentive and the adapter is well documented, this solution is acceptable.

When to use the Adapter

  1. If you need to use a third-party class, but its interface is not compatible with the main application. The example above shows how a shim object is created that wraps calls in a format understandable to the target object.

  2. When several existing subclasses must have common functionality. Instead of additional subclasses (their creation will lead to code duplication), it is better to use an adapter.

Advantages and disadvantages

Advantage: The adapter hides from the client the details of processing requests from one object to another. The client code doesn't think about formatting the data or handling calls to the target method. It's too complicated, and programmers are lazy :) Disadvantage: The project's code base is complicated by additional classes, and if there are a large number of incompatible points, their number can grow to uncontrollable sizes.

Not to be confused with Facade and Decorator

Upon superficial examination, Adapter can be confused with the Façade and Decorator patterns. The difference between an Adapter and a Facade is that a Facade introduces a new interface and wraps an entire subsystem. Well, the Decorator, unlike the Adapter, changes the object itself, not the interface.

Step-by-step implementation algorithm

  1. First, make sure that there is a problem that this pattern can solve.

  2. Define a client interface on behalf of which another class will be used.

  3. Implement an adapter class based on the interface defined in the previous step.

  4. In the adapter class, make a field that stores a reference to the object. This reference is passed in the constructor.

  5. Implement all client interface methods in the adapter. The method can:

    • Transfer the call without modification;

    • Change data, increase/decrease the number of calls to the target method, further expand the composition of the data, etc.

    • As a last resort, if a specific method is incompatible, throw an UnsupportedOperationException, which strictly needs to be documented.

  6. If the application uses the adapter only through the client interface (as in the example above), this will allow adapters to be extended painlessly in the future.

Of course, a design pattern is not a panacea for all ills, but with its help you can elegantly solve the problem of incompatibility of objects with different interfaces. A developer who knows basic patterns is several steps above those who simply know how to write algorithms, because they are needed to create serious applications. Reusing code becomes less difficult and maintaining is a pleasure. That's all for today! But we will soon continue our acquaintance with different design patterns :)
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION