You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Сегодня попался рельсовый код, в котором используются монады, сервисы и прочее. Решил сделать обзор с объяснением того, что в коде не нравится и что можно исправить.
Данный разбор основан только на личном опыте и избегает попытку написать самый идеальный код на свете. К сожалению пошарить ссылку на код не могу, потому что автор попросил опубликовать анонимно.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Сегодня попался рельсовый код, в котором используются монады, сервисы и прочее. Решил сделать обзор с объяснением того, что в коде не нравится и что можно исправить.
Данный разбор основан только на личном опыте и избегает попытку написать самый идеальный код на свете. К сожалению пошарить ссылку на код не могу, потому что автор попросил опубликовать анонимно.
# Исходные данные
Главная операция, которая вызывается из контроллера выглядит следующим образом:
user =yield user_creator.new(validated_params).call
record.users << user
returnSuccess(:created)
end
record.users << user_check_result.value!
Success(:created)
end
end
end
end
```
Два сервиса зависимостей, которые используются в этом примере выглядят так:
```ruby
moduleUsers
moduleService
classCheckExists < Service
definitialize(email, model:User)
@email= email
@model= model
end
defcall
returnSuccess(model.find_by(email: email)) if model.where(email: email).exists?
Success(:user_not_exists)
end
private
attr_reader:email, :model
end
end
end
moduleUsers
moduleServices
classCreate < Service
definitialize(params,
input_validator:Users::Validators::NewUser.new,
model:User
)
@params= params
@input_validator= input_validator
@model= model
end
defcall
yield input_validate
save
end
private
attr_reader:params, :model, :input_validator
definput_validate
input_validator.call(params).to_monad
end
defsave
Try() { model.create!(params) }
end
end
end
end
```
# Проблемы которые нашел
## Контекст > реализация
Убрал общие слова из именования объектов, сервисов и данных. Как пример, в изначальном варианте есть строчка `save(validated_params.to_h)` (`Excursion::Services::JoinUser#call`), как оказалось - это запись пользователя на экскурсию, т.е. с точки зрения кода - это сохранение чего-то в базе, но с точки зрения логики это совершенно другая операция. Если использовать общее именование - придется каждый раз думать зачем этот код, а если использовать описание процесса - появляется контекст, в котором понять реализацию проще, чем по реализации понять контекст. Поэтому переименовал этот шаг в `enroll`.
## Избыточность сервис объектов
В изначальном примере можно увидеть 2 глобальные проблемы:
1. Зависимость от кучи зависимостей
2. Оба сервиса реализуют логику работы с базой данных (проверка существования пользователя и пользователя сохранение в базе).
Большой соблазн сделать 2 отдельных сервиса и положить эту логику туда, как сделал автор оригинального примера. К сожалению, можно заметить, что такой подход приводит к усложнению бизнес логики, яркий пример - две строчки из `Excursion::Services::JoinUser#save`:
# вот тут приходится делать страшную проверку с использованием монады
#
# так же, сама монада самоисключающий случай и вне контекста логики не понятно
# почему отсутствие пользователя успех
if user_check_result ==Success(:user_not_exists)
# в данном случае приходится использовать DO notations во вложенном методе,
# таким образом появляется вложенность, что приводит к усложению data flow
user =yield user_creator.new(validated_params).call
```
Получается, что автор лишний раз обернул операцию с базой данных и тем самым раздул собственный код. Поэтому, кажется, что подобные методы должны находиться в моделе/репозитории. Так же, судя по неймингу, приходим к проблеме контекста, так как названия сервисов является описанием работы кода, а не описанием бизнес логики.
## Общие слова
В примере много общих слов, которые увеличивают конгнетивную нагрузку на человека, который этот код читает. Я говорю о `record`, `user_exists_check`, `validated_params`, `business_validate`, `input_validate`, `params` без контекста не понятно, что это такое, потому что терминология общая. В качестве примера: прочтите название двух сервисов и попробуйте ответить себе, что каждый из них делает:
*`Excursion::Services::JoinUser`
*`Excursion::Services::EnrollUser`
## Стейт класса и микс параметров и зависимостей
В оригинальном примере можно заметить, что конструктор каждого из классов представляет из себя микс из зависимостей и переменных.
В этом нет проблем до тех пор, пок не придется тестировать код и использовать тяжелособирающиеся зависимости (например rom). Так же, каждый вызов сервиса приведет к новой аллокации объектов, но это не так критично. Намного критичнее завязка локики на внутренний стейт объекта. Во первых, сервис перестает быть чистым(мутация данных приведет к багам), во вторых, поддержка кода становится сложнее, так как аргументы могут подсказать детали реализации. В качестве примера сравните два определения функций:
*`def save(validated_params)`
*`def save(record, validated_params)`
В первом случае первая мысль приходящая в голову - параметры сохраняются в базе и создается новый объект модели. Во втором случае понимаешь, что сохраняются данные для конкретной записи. Проблема в том, что это не выдуманный пример. Если посмотреть на оригинал - `save` действительно использует `record` который является внутренним стейтом объекта. Но на самом деле `record` - это просито инстанс модели `excursion`.
## Изоляция цепочек
Распространный подход в рельсе: цепочки методов в моделе, которые сложно тестировать. Я говорю о случаях `record.users.where(payload).exists?` и подобных ему. Проблема заключается в том, что для такого кода сложно написать юнит тест, поэтому приходиться либо писать сложный мок, либо использовать интеграционное тестирование (на любой тест ходить в базу). В каждом из случаев возникают проблемы: моки сложно поддерживать и понять. А походы в базу увеличивают время прохождения тестов на минуты и даже часы.
# Конечный результат
В итоге, после рефакторинга, основанного на идеях описанных выше, получился код, в котором видно какие бизнес шаги происходят (валидация данных, проверка пользователя на участие в экскурсии, запись пользователя на экскурсию). Данный код не идеальны, но даже сейчас он позволяет отказаться от избыточности сервисов, добавляет бизнес контекст и может быть покрыт юнит тестами без сложной подготовки данных.