Привет! Я Олег Федоткин, Head of PaaS в СберМаркете. Хочу поговорить про Service Objects: что с ними не так, почему от них стоит избавляться и как это сделать.  В этой статье я поделюсь своей болью о Service Objects, разберу их ключевые проблемы на примере из опенсорса и предложу собственное решение с чек-листом по его внедрению. Эти архитектурные решения нашли своё применение и в СберМаркете. Этот текст написан нашими прекрасными редакторами на основе моего выступления на Ruby on Rails Meetup. Если вам больше нравится смотреть видео, то оно здесь. В далёком 2011 году я начал писать на Ruby и прочитал книгу, которую, уверен, многие из вас читали — «Agile Web Development with Rails». Там я вместе с автором прошёл путь по созданию своего блога на RoR. Под капотом этого блога был View-Model-Controller. Со временем я утащил модель MVC в свои первые рабочие проекты.  Когда на работе продакт хотел новые фичи, я как инженер вставал перед дилеммой, куда их класть, потому что MVC мне про это ничего не объяснил. Наученный коллегами и Stack Overflow, я клал весь этот код в Контроллеры. Так продолжалось какое-то время, а потом я начал осознавать, что делаю что-то не так.  Контроллеры стали просто неподдерживаемыми. Тем временем продакт продолжал хотеть новые фичи. Я подумал: «Подтекает логика из контроллеров — заклею её жирными моделями». Какое-то время и это работало, но проекты росли, и в итоге я пришёл к тому, что в моделе было 2-3 тысячи строк и из неё тоже нужно было что-то выносить. Тогда всё, что не влезло в MVC, я сложил в общую кучу и назвал её «Сервисом» или Service Object.  Была изобретена прекрасная эвристика. Если это не модель, не представление и не контроллер, это — сервис. Вроде просто и приятно использовать, Но к чему это приводит? Ну сложили и сложили всё в одну кучу, а что плохого-то в этом? Покажу на примере проекта Discourse с Github. У него в районе 30 000 звёзд, его часто используют как сервис для комментариев и у него есть директория services, в которую мы сейчас погрузимся. Вот что нас встречает, когда мы заходим в Services у Discourse: Query Objects Validator Interactor Specification Logger и многое другое Всё это лежит в одной директории и как-то (абсолютно непрозрачно) между собой взаимодействует.  Провалимся в первый же файл — anonymous_shadow_creator. У него есть набор методов: initiallize, get_master и другие. Но не очень понятно, как его вызвать и им  воспользоваться. Допустим, здесь это get_master или get. Но рядом в директории лежит файл destroy_task и в нём уже совсем другие методы. Проверим три лежащих рядом файла — везде разный вызов. Вот nil — это успешное завершение или нет? А пустой массив? А false? И как понять, что что-то пошло не так? Мы никогда не поймём, пока не заглянем внутрь и не прочитаем код от начала до конца. Эти три проблемы лежат на поверхности, и ещё давным-давно я начал искать ответы на них в книгах по разработке ПО. Но вот беда, Service Object не описан ни в одной книге.  Cловосочетание Service Object фигурирует в блоге Мартина Флауэра, но относится совсем не к тому, о чём мы говорим. Можно считать, что Service Object — это паттерн, который выдумали, но в книги добавить забыли (статьи на Медиуме, конечно же, не считаются книгами).  А раз нет описания в профильной литературе, то непонятно, как их фиксить. Значит пора от них избавляться! Есть прекрасная замена Service Objects, называется Interactor. Interactor описывает кодом конкретный сценарий использования (use case). После он собирает данные в структуру и возвращает в UI. Use Case — это то, что вам приносят продакты. Например: пользователь зашёл на страницу, щёлкнул на кнопку и произошла транзакция. Здесь главное, это эти кейсы вам приносит бизнес. Первый вопрос, с которым мы сталкиваемся: «Что выносить в интеракторы»? Ответ: «Выносим в интерактор бизнес-логику». Проверяем его с помощью инварианта: если оба условия верны, то с вашим интераткором всё хорошо.  Интерактор содержит бизнес-логику Бизнес-логика содержится только в интеракторе Если этот инвариант соблюдается, то вы постепенно приходите к Screaming Architecture, когда структура проекта сама говорит о его содержании, ещё до момента, когда мы открыли файлы с кодом. При написании интерактора возникает два ключевых вопроса: Как Service Objects вызывать? Какие параметры в него передавать?  С вызовом всё просто и сложно одновременно. Вам придётся прийти к единой точке вызова. Я на практике видел варианты call, perform, process. Все варианты хороши, ни один не является превалирующим. Главное договориться, что у вас теперь вот так.  Вторая часть — это контракт. Контракт в интеракторе проверяет входные значения определённых типов на соответствие некоторому шаблону (например, user_id, age, reason). Плюс есть правила (например, age>18). Далее следует описательная часть. Если мы опишем контрактом поведение нашего интерактора, то уже на стадии чтения контракта, поймём, что от нас ожидают и как его вызывать.  Если интерактор — это чёрный ящик, то контракт — это наклейка на этом чёрном ящике. Когда она правильно написана и соответствует действительности, то в чёрный ящик можно и не заглядывать. У вас уже есть всё, что нужно, чтобы этим чёрным ящиком воспользоваться. Итого, плюсы контрактов: Понятный интерфейс вызова Очищает код от проверок Строгая типизация исключает часть ошибок Рефакторинг станет проще (можно менятть код, не меняя контракт и никто даже не заметит) Модели станут чище Здесь нам помогут монады из прекрасной библиотеки DRY-RB. Монада — это объект, который может принимать два противоположных значения (success и failure). Сам ответ детерминирован, а содержание монады вы можете менять. Это значит: Ответ интерактора всегда детерминирован: мы всегда знаем, что вернулось (success или failure). Ответ интерактора всегда легко проверить, потому что DRY-RD предоставляет методы  sucess и failure. Больше не нужно гадать, что значит nil или пустое множество. Как итог код становится чище. Итак, мы решили три главные проблемы, которые возникли у нас с Service Objects, но кроме них у нас появилось ещё и две бонусные плюшки. DI (Dependency Injection) — это внедрение зависимостей. Например, нужно доставить какую-то нотификацию к Firebase или SMS. Мы можем использовать эти зависимости прямо из кода, но тогда их сложнее менять и тестировать. А можем передать их на этапе создания объекта через конструктор.  Руками это делать долго. Но DRY-RB помогает писать такие опшены, которые поддерживают дефолтовые значения. Кто-то может подумать, что это всё ради изменяемости кода, но, на самом деле, это очень помогает с тестами. А тесты  в свою очередь приводит к более читаемому и поддерживаемому коду.  Ещё одна приятная вещь из коробки DRY-RB — это yield. Оно помогает убрать одну боль, которая есть в монадах.  В стандартном виде вы получаете монаду, проверяете, это success или failure. Если success, вы идёте дальше, если failure, вы выбрасываете return.  Yield помогает избавиться от многословности. Если success, его значение сразу передаётся в input, если failure, значение самостоятельно делает return и вы не пишете лишних 3-4 строчки кода. Итак, вернёмся к нашему примеру из опенсорса.  В директиве Services у Discourse есть файл user_silencer. Он мьютит определённых пользователей и прячет их посты (на самом деле смысл этого функционала вы поймёте только прочитав весь код).  До этого момента не понятно: Как его вызвать? Возвращает true или false Что такое by_user (сначала я подумал, что это булевое значение, но нет) Что передавать в opts? Не понятно, насколько все параметры обязательные Какие сценарии завершения? Что нужно для тестов? Я переписал этот код c учётом всего, что рассказал раньше. Код стал гораздо более читаемым и оптимизированным для тестов. Благодаря такой организации кода мы можем углубляться в него постепенно.  Этап 0: смотрим на директории на уровне screaming architecture Этап 1: проваливаемся в файл, смотрим завсимости Этап 2: читаем контракт Этап 3: читаем метод #call Этап 4: читаем приватные методы Если вам нужно использовать интерактор просто как чёрную коробку, вам чаще всего хватит прочитать контракт. А это значительно экономит трудочасы команды. Подводим итог. Что такое хороший интерактор? Содержит только бизнес-логику Имеет унифицированный интерфейс для вызова Входные параметры описаны контрактом Возвращает детерминированный результат А ещё: Избегайте хранение состояний Не используйте монады, если приватный метод возвращает только Sucess Тщательно обдумайте зону ответственности интерактора и не пишите God Object-ы.  Минимизируйте код внутри #call Не называйте интеракторы с окончанием на -er и -or. Не «UserSilencer», а «SilenceUser». Так понятнее :) Надеюсь эта статья была полезна! Если тебя интересуют управленческие практики в it, заглядывай на мой Telegram-канал «Инженер и менеджер». Там я часто советую книги об управлении и стратегии и делится инсайтами о работе в роли EM.  Tech-команда СберМаркета завела соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и YouTube.