Шаблон проектирования адаптер

При написании материалов я так или иначе касаюсь темы шаблонов проектирования, однако я обратил своё внимание на то, что нередко путаюсь в их названиях. Поэтому я решил перечитать книгу «банды четырёх», а заодно написать цикл материалов по шаблонам проектирования с примерами кода на языке программирования Java.

Открывает цикл материалов статья о шаблоне проектирования «адаптер». Адаптер, так же известный как «обёртка» (wrapper), применяется в тех случаях, когда некоторый существующий класс необходимо адаптировать под использование с другим целевым интерфейсом без внесения изменений в адаптируемый класс, дабы не нарушать принцип открытости/закрытости.

В качестве примера адаптера из реального мира могу привести зарядное устройство смартфона. В электрических розетках в наших домах напряжение равняется примерно 220 вольтам, в то время как для зарядки смартфонов требуется 5, 9, 10 или 40 вольт, да и разъём у смартфонов может быть разный: Micro USB Type B, USB C или Lightning. Адаптируемым объектом в данном случае является розетка, а целевым — смартфон.

Реализовать шаблон проектирования можно двумя способами: через наследование (адаптер класса) и через композицию (адаптер объекта). Давайте разберём оба способа подробнее.

Адаптер класса

При реализации через наследование класс-адаптер расширяет адаптируемый класс и реализует целевой интерфейс, через который используется адаптируемый класс. Допустим, в моём проекте существует интерфейс FindTaskByIdSpi для поиска объекта TaskData по идентификатору в источнике данных:

В то же время в библиотеке Spring Framework JDBC есть абстрактный класс MappingSqlQuery, при помощи которого можно описывать SQL-запросы, возвращающие данные (SELECT-запросы). Однако MappingSqlQuery не реализует интерфейс FindTaskByIdSpi, и я не могу использовать его без изменения кода. Для решения этой задачи я могу применить шаблон проектирования «адаптер» и создать новый класс — FindTaskByIdMappingSqlQuery, который будет расширять класс MappingSqlQuery и реализовывать интерфейс FindTaskByIdSpi, и который я смогу использовать без изменений в коде.

Адаптер класса

Для реализации шаблона проектирования «адаптер» через наследование требуется, чтобы адаптируемый класс, в данном случае — MappingSqlQuery, соответствовал двум требованиям: не был финальным и имел доступную область видимости.

Тестирование

При тестировании адаптера класса не нужно стараться покрыть всю логику, достаточно протестировать только ту логику, что реализуется в самом классе-адаптере, в то время как логика адаптируемого класса должна быть покрыта собственными тестами. Ниже приведён пример такого тестирования с использованием JUnit 5 и Mockito:

Как видно из этого примера фактическая логика метода findObjectByNamedParam из MappingSqlQuery не используется, так как она уже протестирована разработчиками Spring Framework. Вместо этого поведение метода findObjectByNamedParam в тесте имитируется.

Такой способ тестирования уместен даже в том случае, когда класс-адаптер объявлен финальным, в этом случае потребуется дополнительная настройка Mockito или другого фреймворка для создания объектов-имитаций.

Адаптер объекта

Как уже было сказано выше, адаптер класса применим в случаях, когда есть возможность расширить адаптируемый класс. В противном случае на помощь приходит реализация шаблона проектирования «адаптер» через композицию, которая так же называется «Адаптер объекта». В этом случае экземпляр адаптируемого класса или даже интерфейса должен помещаться в свойства класса-адаптера, а это позволяет адаптировать сразу несколько объектов.

Давайте представим, что описанный ранее интерфейс FindTaskByIdSpi я решил реализовать при помощи NamedParameterJdbcOperations:

Адаптер объекта

В этом случае класс-адаптер FindTaskByIdAdapter реализует целевой интерфейс FindTaskByIdSpi, а так же содержит адаптируемый объект NamedParameterJdbcOperations, как это показано в следующем примере кода:

Адаптер объекта более универсальный, так как может применяться для адаптирования не только классов, но и интерфейсов. Единственным исключением могут являться абстрактные классы, так как их в любом случае нужно реализовывать, а это делать удобнее в адаптере класса.

Тестирование

Логика тестирования адаптера объекта та же — тестировать нужно только логику самого класса-адаптера, как это продемонстрировано ниже:

Адаптер и другие

Я частенько путаю «адаптер» с двумя другими шаблонами проектирования: декоратором и заместителем (proxy). Но на самом деле все три шаблона используются для решения разных задач и по-разному работают с интерфейсом исходного класса.

  • Адаптер предоставляет новый интерфейс для взаимодействия с классом или объектом.
  • Декоратор предоставляет либо исходный интерфейс объекта, либо расширенный.
  • Заместитель предоставляет исходный интерфейс объекта.

Вдогонку хочу упомянуть о возможности реализации множественных адаптеров (двойных, тройных и т.д.), когда класс-адаптер приводит логику адаптируемого класса или объекта в соответствие не к одному, а к двум и более интерфейсам.

Ссылки на видео