Scraper Обновлено: 3 October, 2019

Хорошо продуманная функциональная архитектура: порты и адаптеры

  Перевод   Ссылка на автора

В Salam.io Мы разрабатываем современную социальную платформу, содержащую огромное количество функций.
Разработка такого продукта довольно сложна и сложна - нам нужно, чтобы наше программное обеспечение было надежным, масштабируемым, отказоустойчивым, производительным и в то же время мы хотели бы, чтобы его было легко расширять, тестировать, поддерживать и поддерживать.

Все эти проблемы неизбежны при росте приложения, но их наверняка можно упростить или даже избежать, выбрав, оптимизировав архитектуру программного обеспечения.

Мы также используем функциональные языки программирования - Clojure & Эликсир - для бэкэнда и веб-интерфейса в максимально возможной степени. Поэтому нам необходимо адаптировать существующие архитектурные подходы к мощным возможностям и тонкостям современного функционального программирования.

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

Почему порты и адаптеры?

Даже если вы разрабатываете относительно небольшое программное обеспечение, вам все равно нужно сначала его спроектировать - и правильно спроектировать. Чем раньше вы начнете заботиться о своей архитектуре, тем раньше вы сможете извлечь из нее пользу, и тем позже появится много проблем, вызванных плохой архитектурой.

Основная идея архитектуры портов и адаптеров заключается в том, что создаваемое вами приложение являетсязакрытая зона, Это означает, что вся ваша бизнес-логика должна быть отделена от технических деталей в этой области. Часто архитектура ограничена границами портов и адаптеров.

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

шестиугольный

Архитектура портов и адаптеров также имеет другое название:Гексагональная архитектура, Согласно этой терминологии, внутренняя часть вашего программного обеспечения - место, где вы размещаете свою бизнес-логику, - это шестиугольник, а ваши адаптеры окружают его.

Шестиугольник не должен содержать ссылок на другие фреймворки, сервисы реального мира, библиотеки и т. Д. - все эти элементы должны быть адаптерами. В то же время архитектура не предписывает вам разрабатывать свой шестиугольник определенным образом - вы можете использовать архитектуру Layer, Onion, DDD или любую другую подходящую архитектуру внутри, или это может быть чисто бизнес-логика без каких-либо изощрений - это зависит от ты.

Почему шестигранник? Ну, любая геометрическая фигура с границами могла бы работать, но шестиугольник лучше представляет концепцию, что у вас есть порты на краях вашего приложения и адаптеры за ним. Аналогично, это симметричная фигура, и ниже мы опишем, почему это важно.

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

Наименование портов очень важно - вам не следует использовать какое-либо технологическое имя в вашем порту, а вместо этого сосредоточиться на его предназначении. Некоторые из примеров:

  • Всплывающие напоминания
  • Поиск
  • Упорство
  • Аутентификация

Большинство языков программирования обычно содержат функцию интерфейсов / протоколов, позволяющую вам построить порт. В Clojure, например, вы можете использоватьмультиметодыилипротоколыдля достижения этой цели. Но сейчас давайте посмотрим, как мы можем реализовать реализацию порта для Elixir, используя его способность создаватьповедения:

Приведенный выше пример - не более чем абстракция для использования push-уведомлений отCore, Мы объявляем поведение и один обратный вызов, который указывает, что мы отправляем и что мы можем ожидать в результате. Точная реализация - адаптер - должна быть размещена в конфигурации вашего приложения следующим образом:

Если вы хотите вызвать этот порт из своего приложения, вам просто нужно использовать делегированную функцию:

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

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

Адаптеры драйверов

Мы указываем два типа адаптеров:Водительа такжеуправляемый,

Первые - что-то с левой стороны рисунка выше. Это может быть страница HTML, конечная точка API, приложение CLI, графический интерфейс или что-тодискиваше приложение Это также означает, что адаптер драйвера должен использовать интерфейс порта драйвера, чтобы ваше приложение получало технологически независимый запрос на своих границах.

Давайте предположим, что у нас также есть веб-приложение, которое использует нашиядро, Если мы хотим зарегистрировать пользователя, то нам нужно позвонитьCore.register_user/1функция изнутри нашего контроллера. В таком случаеUserControllerнаш адаптер драйвера иCoreэто вызываемое приложение. К счастью, в Elixir у нас есть спецификации типов, которые могут играть роль спецификации порта драйвера, поэтому вы всегда сможете увидеть, что нам нужно отправить и что мы должны ожидать в ответ.

В подходе выше вы можете видеть, что мы используемCore.register_user/1функционировать как порт драйвера - потому что это спецификация описывает интерфейс - иWeb.UserController.index/2в качестве адаптера драйвера.

Управляемые адаптеры

управляемыйАдаптер реализует интерфейс, предоставляемый управляемым портом. Это означает, что теперь управляемый адаптер зависит от нашего приложения, а не наоборот. Как и драйвер, этот адаптер также должен быть размещен за пределами нашего шестиугольника и представляет собой устройство технологии / библиотеки / реального мира.

Типичные примеры:

  • Постоянные адаптеры - базы данных SQL, NoSQL или даже в памяти / хранилище файлов
  • Адаптеры кэша - Redis / Memcached / ETS или хранилище в памяти
  • Адаптеры электронной почты - SMTP или сторонние сервисы
  • Адаптеры очереди сообщений
  • Сторонние API

Давайте продолжим решение push-уведомлений, которое мы начали раньше. Теперь, чтобы реализовать адаптер драйвера, нам нужно использовать портCore.PushNotificationsи это обратный звонокsend_notifications, Мы адаптируем реализацию отправки push-уведомлений через APNS согласно спецификации, предоставленной нам этим портом:

Теперь наши push-уведомления практически завершены. Мы всегда можем изменить реализацию - например, с APNS на Firebase - или использовать стороннюю библиотекубез изменения нашего основного приложения- так что мы можем сказать, что это технологически независимый подход.

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

Конечно, главное преимущество архитектуры портов и адаптеров - улучшенная тестируемость. Вместо того, чтобы вручную насмехаться над вызовами реальных поставщиков, нам просто нужно создать тестовый адаптер, который удовлетворяет условиям тестирования. В идеальном случае каждыйведомый адаптердолжен иметь аналог теста, а также все виды поведенияdriver portsдолжен быть проверен. Давайте напишем тестовый адаптер для порта PushNotifications:

Как видите, мы не отправляем данные во внешний мир, а вместо этого используем чистую функцию. В случае любого входящего ввода мы точно узнаем его выход. Теперь, когда мы тестируемCoreМодуль нам просто нужно выбрать тестовый адаптер в качестве реализацииPushNotificationsинтерфейс. В экосистеме Elixir у нас есть отличная библиотека под названиемMoxчто можно использовать для такого случая:

В этом примере вы можете видеть, что мы не отправляем push-уведомления в реальном мире, а вместо этого используем локальный тестовый макет. Мы можем изменить тестовый адаптер для любых целей тестирования, если захотим.

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

Плюсы и минусы

Теперь мы рассмотрели основы архитектуры портов и адаптеров. Давайте подведем итог, что мы имеем:

Pros

  • способность быть свидетелем в суде
  • заменяемость
  • Технологически-независимый подход - вы можете задержать технологические решения
  • Изоляция чистого кода от нечистого
  • Изолировать побочные эффекты
  • Ремонтопригодность

Cons

  • Иногда это может быть накладные расходы, особенно для небольшого программного обеспечения
  • Возможно, вам это не понадобится, если вы уверены, что технологический стек вашего проекта останется неизменным на протяжении многих лет.

Мы применили архитектуру портов и адаптеров на Salam.io когда стало ясно, что наше программное обеспечение будет использовать множество сервисов, которые могут быть заменены в будущем. Такой подход уже дал много преимуществ и позволил нам сделать наше программное обеспечение еще более тестируемым и гибким.

Если вы хотите узнать больше об этой архитектуре, вы можете взглянуть на оригинальная статья Алистер Кокберн,

В следующей статье этой серии мы покажем, как вы можете применить архитектуру портов и адаптеров в Clojure, используя его языковые инструменты и библиотеки компонентов.

Следите за обновлениями!


Первоначально опубликовано на мой веб-сайт.