Вы должны писать услуги

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

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

Когда службы не работают

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

Невежество — это счастье?

Наивным подходом было бы возвращать nil или false всякий раз, когда происходит сбой, и истинное значение в противном случае. Скрытое предположение здесь заключается в том, что вызывающий код заботится только о возвращаемом значении и может предложить только общее «Что-то пошло не так» в случае сбоя. Это предположение может работать для прототипа приложения, но не для производственного кода.

Обработка ошибок

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

Ошибки как данные

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

Пример сервиса

Например, вот как может выглядеть ShoppingCartService. Позже мы получим API класса ServiceResult на основе использования.

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

Первое, что нужно отметить, это то, как читается метод process. Несколько вещей становятся очевидными:

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

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

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

Мы могли бы изменить реализацию метода check_inventory, чтобы она была более полезной:

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

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

Метод fail можно компоновать, позволяя другим результатам передавать свои сообщения. Если бы InventoryManagementService вызывал другие службы (скажем, HTTP-запрос к системе управления складом), и они не работали, результат отразил бы это, и мы можем обработать этот сбой по своему усмотрению.

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

Как выглядит успех?

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

Еще раз, если обработка завершается неудачей, мы теряем результат. Затем мы вызываем complete!, чтобы сказать, что мы закончили обработку, и возвращаем результат. Это устанавливает состояние результата в success, если ранее он не был неудачным. Другими словами, если результат остается успешным на всех этапах, возвращаемое значение общедоступного метода process_order является успешным результатом, value которого является обработанным порядком. В противном случае это неудачный результат с данными о том, какой именно шаг не удался, и гарантией того, что дальнейшие шаги не были выполнены.

Заворачивать

Как и было обещано, вот важные части API, которые мы рассмотрели для класса ServiceResult. Фактическая реализация оставлена ​​читателю в качестве упражнения (мы настоятельно рекомендуем TDD как способ ее создания)!

class ServiceResult
  # A way to return its current state
  def success?; end
  def failure?; end
  # A method to continue processing that
  # takes a block, and only executes it
  # if the current state is success
  def if_success(&block); end
  # A method to fail the result
  def fail(result_or_data); end
  # A method to finish processing
  def complete!(value); end
  # A way to return its value
  def value; end
end

Надеемся, что это обсуждение помогло проиллюстрировать подход, который может помочь уменьшить цикломатическую сложность и шаблонность, а также повысить удобство сопровождения и надежность процедурной бизнес-логики. Если вам нравится писать такой код, приходите к нам на https://www.beambenefits.com/careers, мы нанимаем!

Раскрытие информации

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