К поддерживаемому эликсиру: границы

Предыдущая статья рассказывала о высокоуровневом дизайне проектов Very Big Things. Сегодня мы немного углубимся и посмотрим на структуру пространства имен. Слово пространство имен здесь относится к именам модулей, разделенных точками. Например, пространство имен MySystem будет включать модуль MySystem, а также подмодули, такие как MySystem.Account или MySystem.Repo. Точно так же MySystemWeb - это еще одно пространство имен, содержащее такие модули, как MySystemWeb.Endpoint и MySystemWeb.Router.

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

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

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

Основными границами являются Xyz (ядро) и XyzWeb (интерфейс). Вместе эти две границы содержат большую часть кода проекта. Остальные границы возникли со временем и вводятся для решения некоторых практических вопросов. Схема именования границ верхнего уровня следует соглашению, предложенному Phoenix, где суффикс добавляется непосредственно к имени контекста, вместо использования разделителя . (например, XyzConfig вместо Xyz.Config). Давайте кратко рассмотрим эти границы.

Конфигурация оператора

Граница XyzConfig - это граница одного модуля, которая объединяет то, что мы называем конфигурацией оператора. Это системные параметры, которые должны быть предоставлены на целевой машине (например, постановка или выпуск), такие как общедоступный URL-адрес сайта, строка подключения к базе данных, учетные данные для сторонних служб и т. Д. Модуль конфигурации обертывает доступ к этим параметрам. Клиентский код вызывает что-то вроде XyzConfig.database_url(), и значение извлекается из некоторого источника, такого как env ОС.

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

Схемы экто

Несколько спорно, но мы решили сгруппировать наши модули схемы Ecto под одной границей верхнего уровня. Мы в основном избегаем объединения вещей на основе их технических свойств, но мы сделали здесь исключение, потому что схемы Ecto имеют тенденцию довольно быстро взрываться. Например, в этом конкретном проекте 50 из 100 модулей являются схемами Ecto. Преобладание схемных модулей становится еще более поразительным, если мы сосредоточимся на границе контекста (Xyz), которая состоит всего из 23 модулей. Размещение модулей схемы в этих папках значительно усложнит навигацию по коду.

Более того, схемы Ecto на границе контекста приводят к некоторым странным именам модулей, таким как Xyz.Accounts (контекст) и Xyz.Account (схема) или, альтернативно, Xyz.Accounts.Account (схема внутри пространства имен контекста). Чтобы предотвратить эти проблемы и улучшить навигацию по коду, мы решили объединить схемы под одной границей.

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

Приложение OTP

Граница XyzApp содержит логику, необходимую для приложения и выпуска OTP. Эта граница была введена, чтобы разорвать цикл зависимости между интерфейсом и ядром. В нашем высокоуровневом дизайне мы не хотим, чтобы ядро ​​зависело от интерфейса. Однако при создании нового проекта Phoenix это правило сразу же нарушается. А именно, генератор проекта Phoenix помещает модуль приложения OTP внутри границы контекста. Но модуль приложения зависит от модуля конечной точки на границе сети, и поэтому мы получаем цикл зависимости. Чтобы разорвать этот цикл, мы переместили модуль приложения на границу верхнего уровня. Наш собственный генератор проектов делает это автоматически для всех новых проектов.

Смешивание

Граница XyzMix содержит весь код, специфичный для наших пользовательских задач смешивания, которые в основном вращаются вокруг настройки базы данных для локального разработчика и CI. Это единственная часть кода, где во время выполнения разрешен вызов функций смешивания, таких как Mix.env(). Вызовы микширования во время компиляции разрешены везде.

Интерфейс

Веб-граница содержит логику, специфичную для Феникса и Абсента. Внутренняя структура этой границы соответствует соглашениям, предложенным генератором проектов Phoenix и официальной документацией. В последних проектах мы начали экспериментировать с альтернативной организацией на веб-уровне, отойдя от консолидации контроллеров / представлений / шаблонов в пользу подхода, при котором каждая логическая область (например, учетная запись, администратор и т. Д.) Получает свою собственную папку, которая содержит контроллер, представление, шаблоны и другие файлы, зависящие от области.

Основной

Ядро - единственная граница, которая далее делится на субграницы, некоторые из которых представлены на следующей диаграмме:

Xyz.Infra - это «граница приемника», что означает, что все остальное в ядре зависит от нее. Эта граница содержит модули, поддерживающие доступ к инфраструктурным сервисам, таким как AWS или ActiveDirectory. Инфра-граница также содержит два репозитория Ecto. Дополнительное репо необходимо для поддержки мультиарендности через динамические базы данных. Инфраструктура экспортирует эти модули, чтобы их можно было использовать из других границ. Однако, поскольку инфра является подчиненной границей, доступ может быть предоставлен только другим соседним границам. Другими словами, такая конструкция не позволяет кому-либо за пределами ядра напрямую использовать репозитории, клиент AWS и другие инфраструктуры.

Остальные субграницы ядра обрабатывают некоторые функциональные аспекты поведения системы. Например, Xyz.Account занимается операциями с учетной записью (например, регистрацией, входом в систему, сбросом пароля, уведомлениями), а Xyz.Tenant управляет управлением арендаторами (например, созданием и удалением арендатора). Есть еще пара таких границ, которые для краткости здесь не приводятся.

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

Резюме

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

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

Следует отметить, что эта структура возникла со временем. Мы начали гораздо скромнее, спрятав все основные операции в Xyz модуле верхнего уровня. Когда этот модуль вырос в размерах, нам потребовалось некоторое время, чтобы определить потенциальные стратегии разделения на основе существующей функциональности и соответствующего кода, а не на неопределенные догадки. По сути, это легкий гибкий подход к разработке кода.

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