Pull to refresh
0

Чем dry-rb (не) полезен мне

Reading time 8 min
Views 3.2K

В конце сентября мы провели уже четырнадцатую конференцию Ruby Russia. На ней было много полезного, и чтобы эта польза не пропала, мы оформили доклады в тексты, которые будем публиковать здесь. Автор первого Егор Шморгун, Ruby-разработчик Level Travel.


Компания Level Travel предлагает помощь в приобретении туров быстро, недорого, онлайн. Для этого компания использует монолит Ruby. Монолит достаточно большой, и в нем, разумеется, много Ruby-кода. Когда Ruby-кода много, хочется, чтобы хотя бы часть кода была читаемой.

Читаемый код — понятие достаточно субъективное. Один из лучших способов создания читаемого кода на Ruby, по моему мнению, — это dry-rb. Мы используем микросервисы, полностью написанные на dry-rb, он частично используется в монолите, и это позволило нам составить собственное мнение о dry-rb.

dry-rb

dry-rb — это целая экосистема, состоящая из 24 различных гемов, каждый из которых решает определенные задачи, не решенные в стандартной библиотеке.

Такие задачи включают:

  • Описание объектов, их типов, и валидация объектов-контейнеров для данных.

ПРИМЕЧАНИЕ. Можно вспомнить RBS, который пришел с третьей версией Ruby, но сейчас он не такой популярный и распространенный. Кроме того RBS, как мне кажется, предназначен для других целей.

  • Работа с ошибками с использованием монад — достаточно удобного инструмента, пришедшего из функционального программирования.

  • Структурирование кода, т. е. микросервисов, приложений и т. д.

  • а также еще около 15 элементов со своим предназначением и возможностями.

dry-struct  — описание объектов данных

dry struct — это основа dry и одна из самых старых библиотек. Как выглядит код без dry struct? Мы объявляем какой-то класс, обозначаем его атрибуты, пишем конструктор и т. д. Но это не говорит нам о многом, поскольку мы не знаем, что хранится в этих атрибутах. Кроме того, на этих атрибутах отсутствует валидация.

Если использовать dry-struct, становится намного лучше.

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

Преимущества dry-struct

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

Нет необходимости писать конструктор и копипастить в него атрибуты, что становится удобно, когда атрибутов очень много.

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

Приведение типов (coercion).

Enums — наличие перечислений, которых обычно не хватает в стандартной библиотеке, и часто приходится определять отдельные модули с константами.

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

Недостатки dry-struct

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

dry-struct проводит валидацию объектов в runtime. В случае передачи некорректного хеша, вываливается исключение, в результате чего какая-то важная часть бизнес-процесса может прерваться.

dry-validation

dry-struct не предназначен для валидации. Для валидации должна использоваться специальная библиотека — dry-validation, которая работает со схемами и контрактами.

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

Без dry-validation все становится довольно громоздко, нечитаемо и плохо используемо.

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

У нас dry-validation также используется для описания API. До dry-validation использовался открытый API, но он был недостаточно документирован, и было непонятно, какие именно входные параметры были доступны. Использование контрактов позволяет не только их описать, но и сразу валидировать.

Преимущества dry-validation

Разнообразные и удобные правила валидации, которые упрощают написание кода, различные предикаты, условия и т. д.

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

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

Доступно переиспользование enum и constraint из dry-types, что позволяет использовать как встроенные, так и кастомные типы dry-types с различными ограничениями, что делает код еще более читаемым.

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

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

Недостатки dry-validation

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

dry-validation используется только с интерфейсом хеша, т. е. нельзя написать какой-то класс, написать для него атрибуты и передать в контракт. Сразу появится уведомление об ошибке. Однако это будет работать, если сделать метод приведения объекта в хеш.

Это очень полезный гем, и мы им регулярно пользуемся.
Это очень полезный гем, и мы им регулярно пользуемся.

Если совместить dry-struct и dry-validation, которые как будто хорошо стыкуются, можно ожидать, что все будет удобно. Есть контракты, которые валидируют параметры, есть структура, которая их агрегирует (т. е. исключения отсутствуют).

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

Можно изменить подход и использовать dry-struct и dry-validation для разных вещей. Например, контракты использовать для входных параметров, а структуры для хранимых данных, полученных в результате их обработки.

dry-system. Структурирование кода

Отдельные гемы также используются для изменения структуры кода.

dry-system является совокупностью трех геймов — dry-container, dry-auto_inject и dry-configurables.

  • dry-container — простой потокобезопасный контейнер, предназначенный для использования в качестве половины реализации контейнера с инверсией управления.

  • dry-auto_inject — обеспечивает простое внедрение и разрешение зависимостей.

  • dry-configurables — простой миксин, добавляющий поведение конфигурации в класс.

Позднее связывание. У нас есть основной класс приложения, в котором автозагружаем код, конфигурируем и т. д., как и в Rails.

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

Возможно использование как напрямую, так и путем импорта в класс с помощью в dry-auto_inject, т. е. мы подгружаем в класс имеющиеся зависимости, избавляемся от конструктора и используем зависимости в классе.

Такой импорт позволяет делать и более сложные вещи. Например, импортировать классы с другими зависимостями и требованиями, которые можно подготовить заранее.

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

Преимущества dry-system

dry-system не имеет привязки к конкретной архитектуре. Если Rails ограничивает вас рамками MVC, в dry-system таких ограничений нет. Это свойство сыграло решающую роль в решении о выносе функционала, который был в монолите, в отдельный сервис. Рефакторить наш устаревший функционал было опасно, кроме того, не хотелось нагружать монолит, как и выносить в отдельный сервис на Rails. dry-system стал оптимальным решением.

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

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

Удобная подмена зависимостей для тестов, чему способствуют не только встроенные методы (stub, mock и т. д.), но и сам принцип импорта зависимостей, которые легко подменять при исполнении.

Удобная работа с глобальным состоянием. Если на этапе инициализации контейнера загрузить некоторые важные вещи, такие как Redis, метрики и т. д., их можно использовать непосредственно в программе. Это будет безопасно и удобно.

Недостатки dry-system

Много материала для изучения. Не только библиотеки, но и базовые концепты, такие как dependency injection, dependency inversion и т. д.

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

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

Неканоническая совместимость с Rails. Есть, конечно, dry-rails, но он не очень распространен и не очень хорошо развивается, поэтому не является популярным решением.

dry-monads

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

Типичный код без использользования dry-monads будет выглядеть следующим образом:

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

При использовании dry-monads (справа) код становится чище, особенно если знаком синтаксис. Ошибки контролируются, т. е. мы решаем, обрабатывать ли их на месте или перекинуть их на один уровень выше (что менее беспорядочно). Ошибки удобно обрабатывать с помощью case, что еще удобнее с появлением Ruby 3.0 и наличием в нем pattern-matching. В целом код становится более понятным.

Преимущества dry-monads

Success/Failure – удобное обозначение статуса завершения функции. Перед каждым вставал вопрос, что вернуть, если возникает ошибка. Например, функция должна возвращать integer, а ошибку через integer выражать не хочется. При использовании dry-monads думать об этом необязательно, все интуитивно понятно. Если успешно, возвращается Success, в противном случае — Failure.

value_or — обработка ошибок в функциональном виде, т. е. если все успешно, получаем значение (Success), в противном случае обрабатываем внутреннюю ошибку. Все это выполняется без if-else.

Do-нотация (синтаксический сахар для функции bind, типичной для монад (если все хорошо, получаем значение, в противном случае — перекидываем на один уровень выше)) реализует rail-way паттерн, когда успешный путь читается без запинок (без if-else, обработки ненужных ошибок, которые обрабатываются на уровне выше).

Недостатки dry-monads

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

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

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

В dry-экосистеме есть много других инструментов, с которыми я рекомендую ознакомиться, например:

  • dry-effects

  • dry-transformations

  • dry-monitor

  • ... и несколько совсем редких штук.


Посмотреть все доклады прошедшей Ruby Russia можно на YouTube-канале конференции.

Tags:
Hubs:
+3
Comments 0
Comments Leave a comment

Articles

Information

Website
singula.team
Registered
Founded
2008
Employees
31–50 employees
Location
США