Адаптер(adapter)

Адаптер(adapter)

KarmanovichDev

Сегодня мы поговорим о таком паттерне проектирования как адаптер.


Из вики: адаптер — структурный шаблон проектирования, предназначенный для организации использования функций объекта, недоступного для модификации, через специально созданный интерфейс. Другими словами — это структурный паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе.


Для начала рассмотрим несколько примеров из реальной жизни, чтобы помочь нам понять основную суть:

  • Один из самых распространённых примеров, это конечно же адаптер для розетки
С помощью такого адаптера можно использовать несовместимые изначально интерфейсы(вилки и розетки).


  • Или картридер для вашего ПК или музыкального центра
С помощью картридера мы соединяем два изначально несовместимых интерфейса.


  • Различные адаптеры для конвертации потока данных при работе с ПК
Несмотря на то, что наш ноутбук имеет возможность передачи изображения только по HDMI, нам ничего не мешает использовать адаптеры для конвертации потока данных в требуемый формат для принимающей стороны.


Обратите внимание, что в примерах ни разу не пришлось изменять ни то, к чему мы адаптируем, ни то, что мы адаптируем. Для использования паттерна адаптер подразумевается, что оба объекта с несовместимыми интерфейсами уже существуют и нам нужно модифицировать ни один из них.


А теперь давайте посмотрим на примеры, когда паттерн адаптер может помочь нам как программистам:

  • Представьте, что у вас есть приложение, которое возвращает усреднённый курс по ценным бумагам, полученный из множества доступных бирж. До какой-то поры всё было хорошо, так как все популярные биржи придерживались очень похожего интерфейса общения по протоколу REST. Вы не успели заметить, как ваш код стал завязываться на эти детали реализации, и большая часть вашего приложения стала ожидать в своих интерфейсах(порты) взаимодействия такой формат(REST). В один прекрасный(нет!) день к вам пришёл менеджер и сказал, что, чтобы улучшить свои позиции на рынке агрегаторов, вы должны подключить ещё две конкретные биржи. Одна была довольно старая и использовала ранее неиспользуемый вами протокол обмена данными SOAP, а вторая биржа использовала JSONRPC, что так-же было для вас новым видом взаимодействия. В этот момент вы осознали, что текущий код не готов работать ни с одним из новых для вас форматов в данный момент, и начали судорожно думать, как сделать систему отвязанной от способа передачи данных, либо как сделать костыль, который бы мог дать вам принимать решения не на таком уровне стресса. Представили? Так вот, таким "костылём" может стать паттерн адаптер.
"Костыль" в кавычках здесь потому, что это вполне неплохое решение, как с точки зрения оставить его насовсем, если добавление бирж с протоколом общения не по REST(HTTP) это исключительная ситуация. Также это подходит как быстрое решение, для возможности отсрочить более глобальный пересмотр системы, если возможности работы с другими способами передачи данных предстоит стать обыденностью. Этот вариант так-же оставит ваш код более чистым чем впопыхах написанный костыль.


  • Также может быть, вы встречали в бизнес-коде if(ы), которые завязаны на детали реализации инфраструктуры?😏
Кто-то до вас, не подумал о модульности приложения и open-close(sOlid), и сделал "неплохое", как ему показалось решение, написав другую реализацию получения пользователей, которая никак не пересекается с имеющимся репозиторием(ни по способу взаимодействия, так как один из них синхронный, ни по интерфейсу взаимодействия).


Конечно, по-хорошему нужно выделить общий интерфейс и "загнать" под него оба эти класса, но что если в данный момент это сделать достаточно сложно? Что, если сейчас на это эти классы завязана большАя часть вашего приложения? В таком случае вы не сможете сделать это быстро без существенных правок в коде. Учитывая, что масштабный рефакторинг "за раз" в 99% случаев невозможен в силу нехватки времени, вам приходится делать очередное TODO и надеяться, что когда-нибудь вы сможете это исправить.

С другой стороны, вы могли бы достаточно малыми силами начать этот рефакторинг прямо сейчас в рамках своей задачи! Но как? Даже в самой плохой ситуации, когда вы не можете по каким-либоб причинам отвязаться от деталей реализации и выделить интерфейс, начните с малого и сделайте адаптер одних деталей реализации под другие, которые с большей вероятностью будут являться основой для интерфейса в дальнейшем:

Обратите внимание, как сильно мы разгрузили код сценария использования от деталей реализации!

Если у вас есть возможность сделать больший шаг, то вы можете выделить интерфейс:


UML схема:

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


Вы можете задаться вопросом, почему RedisUserRepository так же не вынесен под интерфейс BaseUserRepository? Вспомним, что я говорил выше: ваш текущий код может быть сильно завязан на уже имеющиеся детали реализации, что мешает сделать все изменения, которые вы запланировали за один шаг.

Также, делая адаптер в данной ситуации вам уже, не нужно проверять все инварианты и сложность RedisUserRepository, они уже протестированы ранее. Вам нужно лишь добавить пару тестов на проверку работы адаптера.

Всё вышесказанное не отменяет того, что если полная доработка кода не занимает много сил и времени, её можно сделать и за один шаг. Но знание того, что можно сделать шаг поменьше с помощью адаптера, а не оставлять очередное TODO может помочь вам начать приводить ваш код в порядок сейчас а не в будущем.

Два простых примера, когда мы можем использовать адаптер и где при всём желании не можем изменить адаптируемый объект:

  • InMemoryRepository/FakeRepository
В данном случае мы адаптируем словарь для работы как репозиторий пользователей. Словарь является адаптируемым объектом, InMemoryUserRepository адаптером. Изменить словарь мы не можем(даже не думайте об этом).


  • Очередь/стек на основе списка
В этот раз адаптируемым под требуемый интерфейс(BaseStack) объектом является список, который мы при всём желании не можем изменить(даже не думайте об этом).


По идее большинство классов, которые мы пишем для взаимодействия с инфраструктурой(сторонними библиотеками) без большого кол-ва вызовов и без логики являются адаптерами. Если появляется большое кол-во вызовов или сложная логика, это скорее уже паттерн фасад, но нужно смотреть конкретные примеры, что бы сказать точно.

Report Page