При написании материалов я так или иначе касаюсь темы шаблонов проектирования, однако я обратил своё внимание на то, что нередко путаюсь в их названиях. Поэтому я решил перечитать книгу «банды четырёх», а заодно написать цикл материалов по шаблонам проектирования с примерами кода на языке программирования 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). Но на самом деле все три шаблона используются для решения разных задач и по-разному работают с интерфейсом исходного класса.
- Адаптер предоставляет новый интерфейс для взаимодействия с классом или объектом.
- Декоратор предоставляет либо исходный интерфейс объекта, либо расширенный.
- Заместитель предоставляет исходный интерфейс объекта.
Вдогонку хочу упомянуть о возможности реализации множественных адаптеров (двойных, тройных и т.д.), когда класс-адаптер приводит логику адаптируемого класса или объекта в соответствие не к одному, а к двум и более интерфейсам.