Битва за разложение и повторное использование

Прежде чем найти дом с Ruby, я был давним разработчиком Java почти с самого начала языка. Так что это, безусловно, привлекло мое внимание, когда я прочитал первую строку этой статьи Ильи Суздальницкого: C++ и Java, вероятно, являются одними из самых серьезных ошибок информатики. Отчасти кликбейт, отчасти тема выбора любого языка для развлечения читателей (ни один из них не идеален), а отчасти правда.

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

Хорошо, да начнется битва.

Служба — это слишком часто используемый термин, что мы на самом деле имеем в виду?

Не думайте здесь о «микросервисах». Мы могли бы говорить о том, как мы организуем нашу логику в величественном монолите. Сосредоточьтесь на основной концепции услуги, конструкции, по существу аналогичной функции. Учитывая набор входных данных, он последовательно выполняет одно действие и возвращает результат. Услуги в приложении электронной коммерции могут включать «Добавить товар в корзину» или «Оформить заказ». Вы можете надежно вызвать службу для выполнения данной функции, которую она описывает.

Не совсем потрясающие вещи, верно? Оставайтесь со мной на мгновение, чтобы увидеть, насколько актуально это путешествие в прошлое.

Рассмотрим теперь ванильную реализацию Ruby с использованием PORO (обычный старый объект Ruby). Подождите, разве мы уже не объединили два понятия? Служба, реализованная как объект? Ну, все в Ruby является объектом, поэтому у нас нет особого выбора, кроме как начать с этого.

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

Взгляд с точки зрения объекта

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

Рассмотрим использование класса калькулятора, как показано ниже.

calc = Calculator.new
puts calc.total     # total will be 0 (zero)
calc.add(15)
puts calc.total     # total will be 15
                    # different than the first invocation

Но подождите, это то, что общее должно быть в этот момент! Наша программа верна, недетерминизм побеждает! Вы были бы правы в этом контексте.

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

Давайте рассмотрим платежную систему, в которой мы передаем наш объект калькулятора между методами для поддержания текущей суммы платежей. Этот подход, кажется, имеет смысл, и, безусловно, какой-нибудь инженер в команде попробует его. Наш калькулятор передается в операцию оплаты за просрочку, которая добавляет сборы, однако устанавливает десятичную точность равной нулю, потому что бизнес решил, что все сборы равны сумме в долларах. Теперь, если мы передаем тот же объект калькулятора в класс расчета налога, точность будет изменена. Здесь мы хотим и нуждаемся в двухразрядной точности (стандартные доллары и центы).

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

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

Первоначально объекты предназначались для связи через слабосвязанные сообщения, а не для прямого манипулирования состоянием с помощью таких конструкций, как общедоступные сеттеры (вспомните attr_accessor). Инкапсуляция часто плохо понимается или реализуется. Популярные объектно-ориентированные языки, конечно же, не навязывают это. На самом деле, с Java bean-компонентами и атрибутом Ruby attr_accessors все обстоит наоборот.

Организация бизнес-логики таким образом, чтобы разработчики могли ее найти

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

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

Вот где услуги действительно блестят. Они могут аккуратно располагаться поверх нашей объектной модели и реализовывать эти задачи. Мы по-прежнему используем ActiveRecord и его возможности объектно-реляционного сопоставления (ORM). Мы просто ограничиваем логику в классах моделей всем, что, как мы знаем, применимо ко всем вариантам использования, обычно проверками и другими ограниченными выводами или вычислениями.

Распространенным вариантом организации сервисов является создание каталога app/services. Слишком часто мы произвольно чувствуем себя ограниченными структурой каталогов, которую мы получаем от «rails new». Просто выберите стандарт для своей команды и придерживайтесь его.

Пока не выбрасывайте предметы

Если все это все еще кажется несколько академическим, или я еще не убедил вас, потерпите еще немного. Объекты по-прежнему чрезвычайно ценны, и в том, что все в Ruby является объектом, все еще есть что-то волшебное. Любой строковый объект в Ruby имеет множество замечательных встроенных вспомогательных функций. Больше никаких глупых классов StringHelper, которые вы видите в других языках.

Однако помимо этого есть настоящее место для объектов с состоянием: пользовательские интерфейсы. В пользовательском интерфейсе я определенно хочу недетерминированного поведения, потому что я хочу реагировать на то, что пользователь только что щелкнул или ввел. Мне нужно состояние текстового поля или виджета в этот момент времени. Пользователи, безусловно, недетерминированы, и поэтому модель ООП идеально подходит.

Однако давайте рассмотрим бэкэнд. В Rails это начинается с действия контроллера, свободного эквивалента службы, ориентированной на HTTP. Как мы уже отмечали, Rails не дает никаких указаний или структуры относительно того, как реализовать «бизнес-логику». Вы найдете тысячи постов, предупреждающих вас не выбрасывать слишком много логики в действие вашего контроллера.

Лично я думаю, что общего руководства по ограничению большинства методов до приличной длины (видимая страница или около того) достаточно, но более важным моментом является то, как вы структурируете действительно важные вещи, логику вашего приложения. Здесь в игру вступают наши сервисные объекты.

Достаточно теории, давайте применим это на практике

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

Вы можете реализовать шаблон службы как PORO, но лучше использовать один из доступных гемов, помогающих в структуре кода, оркестровке и обработке ошибок. Гем LightService обеспечивает хорошую реализацию, почти не добавляя накладных расходов. Его конструкция очень проста, как показано на схеме ниже. Органайзер используется для определения последовательного рабочего процесса, состоящего из одного или нескольких действий. Ошибка в любом из действий приведет к короткому замыканию всего рабочего процесса.

Чтобы использовать LightService, просто добавьте запись в свой Gemfile.

gem 'light-service'

Я рекомендую вам также настроить ведение журнала, которое по умолчанию отключено. Добавьте следующую строку в файл application.rb.

LightService::Configuration.logger = Logger.new(STDOUT)

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

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

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

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

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

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

Эта статья ранее была опубликована на HackerNoon