Это первая из серии статей, в которых мы с @hecrj делимся тем, что мы узнали после работы над большой, быстро меняющейся кодовой базой в течение последних трех лет и остались полностью довольны результатом!

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

Мы находимся в бесконечном поиске лучших инструментов и шаблонов, но означает ли это, что наш код обречен стать старым и морщинистым?

Как вы закрепите свой проект, чтобы он противостоял ветрам тренда? Вот 5 советов, которые нам очень пригодились.

1. Разделите код на основе концепций предметной области, а не технических концепций.

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

# Split by tech concepts        # Split by domain concepts
|- src                          |- auth
|  |- controllers               |  |- controllers
|  |  |- auth                   |  |- models
|  |  |- profile                |  |- views
|  |  |- article                |  |- tests
|  |- models                    |- profile
|  |- views                     |- article
|- test                         (...)
|  |- controllers
|  |  |- auth
(...)

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

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

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

Что легче поддерживать: кодовую базу из 10 файлов или базу из 100 файлов?

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

2. Предоставьте публичный контракт (API) для всех концепций вашего домена.

Представьте, что в вашем проекте есть каталог payments, в котором вы храните весь код, связанный с 💰. У нас есть ряд компонентов для хранения наших платежей в базе данных или для подключения к сторонним сервисам, таким как Stripe.

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

Чтобы внести ясность, мы не говорим о HTTP API, которое ваше мобильное приложение будет вызывать для взимания платы с пользователей. Мы говорим о внутреннем API, который превращает ваш payments каталог в его собственный «микросервис» (используя этот термин свободно).

Почему ты спрашиваешь?

Поскольку наличие явного API обеспечивает:

  • Четкая картина ожидаемого поведения.
  • Минимальное тестовое покрытие, с которым каждый может согласиться и принять на себя обязательство.
  • Свобода изменять что-либо из базовой реализации.

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

Например, мы можем представить, что у вас есть:

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

3. Положитесь на небольшие интерфейсы

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

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

  • Издатель мероприятия
  • Подписчик на мероприятие
  • Зарядное устройство для кредитной карты
  • Отправитель электронной почты

Все эти интерфейсы имеют небольшую и четко определенную роль. Позже мы внедрим конкретные реализации:

production = Payments.new(
 event_publisher: rabbitmq,
 event_subscriber: rabbitmq_replicas,
 credit_card_charger: stripe,
 email_sender: mailgun,
)
development = Payments.new(
 event_publisher: in_memory_bus,
 event_subscriber: in_memory_bus,
 credit_card_charger: stripe_test_mode,
 email_sender: muted_mailer,
)

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

4. Отделите данные от стратегии хранения.

Давайте разберемся с этим: мы думаем, что ORM неправильные (или, может быть, это люди, которые делают их неправильно). Взгляните на этот код Ruby on Rails:

class Article < ActiveRecord::Base
  belongs_to :user
  has_many :comments, dependent: :destroy
  scope :authored_by, ->(username) { where(user: User.where(username: username)) }
  validates :title, presence: true, allow_blank: false
  validates :body, presence: true, allow_blank: false
  before_validation do
    self.slug ||= “#{title.to_s.parameterize}-#{rand(36**6).to_s(36)}”
  end
end

Здесь есть что распаковать.

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

А теперь давайте задумаемся на мгновение. Что для нас важно при оформлении статьи ?:

  • Мы должны иметь возможность использовать всю мощь языка, который мы используем. Когда мы используем Java, мы хотим иметь возможность свободно использовать объектно-ориентированные шаблоны и наследование. Когда мы используем Haskell, мы хотим использовать типы объединения и записи.
  • Мы должны иметь возможность хранить наши данные в разных форматах и ​​базах данных. Это позволяет нам использовать ElasticSearch для эффективного поиска, PostgreSQL для согласованного состояния и Redis, чтобы наша функция автосохранения оставалась достаточно быстрой.

Модели ORM не предлагают ни того, ни другого, потому что они всего лишь способ взаимодействия с базой данных SQL. Нам все еще нужно где-то еще представлять наши данные и манипулировать ими. Проблема в том, что если вы примете это утверждение, использование ORM покажется неудобным или излишним. Вот что мы имеем в виду:

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

5. Используйте события, чтобы ваше приложение оставалось подключенным, а код развязан.

Если две части приложения связаны, код должен каким-то образом быть связан, не так ли?

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

Основная идея состоит в том, что каждое изменение в вашем домене представляется как атомарное событие.

article_published(…) 1 minute ago
article_draft_created(…) 5 minutes ago
user_signed_in(…) 25 minutes ago

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

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

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

  • Слушайте комментарии к статье и увеличивайте счетчик (цель: более быстрый подсчет комментариев).
  • Отправьте приветственное письмо новому пользователю.
  • Сообщите автору статьи о новых комментариях.

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

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

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

Мы будем рады узнать, что вы думаете об этих идеях в разделе комментариев!