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

What tasks does the Adapter design pattern solve?

Published in the Random EN group
Often software development is complicated by the incompatibility of components that work with each other. For example, if you need to integrate a new library with an old platform written in early versions of Java, you may encounter object incompatibility, or rather, interfaces. What to do in this case? Rewrite code? But this is impossible: the analysis of the system will take a lot of time, or the internal logic of work will be violated. What tasks 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 about the problem

First, let's simulate the behavior of the old system. Suppose 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 implements 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 an 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. In order to improve it for the needs of this group, you ordered an excuse generation system from another developer specifically for it. 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 again. A third-party library is closed for editing - you cannot change its source code. As a result, your system has 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) {
       // Добавляет причину в черный список
   }
}
You cannot change the code. The current diagram will look like this: What tasks does the Adapter design pattern solve - 2This version of the system only works with the Excuse interface. You cannot rewrite the code: in a large application, such edits can be delayed or break the application logic. You can suggest the introduction of the main interface and increase the hierarchy: What tasks does the Adapter design pattern solve - 3To do this, you need to rename the interface Excuse. But the extra 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 the new and old functionality with minimal loss. In short, you need an adapter .

The principle of operation of the pattern Adapter

An adapter is an intermediate object that makes method calls from one object understandable to another. Let's implement the adapter for our example and call it Middleware. Our adapter must implement an interface compatible with one of the objects. Let it be Excuse. Because of this, Middlewareit can call the methods of the first object. Middlewarereceives calls and passes them to the second object in a compatible format. This is how the implementation of the method Middlewarewith methods generateExcuseand 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 state of the weather, traffic jams or disruptions in public transport schedules. The method generateExcusesimply transfers the call to another object, without additional transformations. The method dislikeExcuserequired the pretense to blacklist the database beforehand. Additional intermediate data processing is the reason why the Adapter pattern is loved. But what about a method likeExcusethat is in the interface Excusebut not in the StudentExcuse? This operation is not supported in the new functionality. For such a case, an exception was invented UnsupportedOperationException: it is thrown if the requested operation is not supported. Let's use this. This is what the new implementation of the class 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 imitation of the functionality can lead to a more complicated 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 wrapper object is created that wraps the calls in a format that the target object understands.

  2. When multiple existing subclasses need to share common functionality. Instead of additional subclasses (creating them will lead to duplication of code), it is better to use an adapter.

Advantages and disadvantages

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

Not to be confused with Facade and Decorator

On a superficial examination, the Adapter can be confused with the Facade and Decorator patterns. The difference between an Adapter and a Facade is that a Facade implements a new interface and wraps an entire subsystem. Well, the Decorator, unlike the Adapter, changes the object itself, and not the interface. What tasks does the Adapter design pattern solve - 4

Step by step implementation algorithm

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

  2. Define the client interface on behalf of which the other class will be used.

  3. Implement the 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 methods of the client interface in the adapter. The method can:

    • Transfer the call without change;

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

    • As a last resort, if a particular method is incompatible, throw an UnsupportedOperationException, which must be strictly documented.

  6. If the application will only use the adapter through the client interface (as in the example above), this will painlessly extend the adapters in the future.

Of course, the design pattern is not a panacea for all ills, but it can be used to elegantly solve the problem of incompatibility of objects with different interfaces. A developer who knows the basic patterns is a few steps above those who just know how to write algorithms, because they are needed to create serious applications. Reusing code becomes not so 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