Создавайте компоненты Sleek Rails с помощью простых старых объектов Ruby

Опубликовано: 2022-03-11

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

Без проблем. Сегодня мы обсудим, как использовать лучшие практики ООП, чтобы сделать ваш код чище, более изолированным и несвязанным.

Стоит ли рефакторинг вашего приложения?

Давайте начнем с рассмотрения того, как вы должны решить, является ли ваше приложение хорошим кандидатом на рефакторинг.

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

  • Медленные модульные тесты. Модульные тесты PORO обычно выполняются быстро с хорошо изолированным кодом, поэтому медленные тесты часто могут быть индикатором плохого дизайна и чрезмерно связанных обязанностей.
  • Модели или контроллеры FAT. Модель или контроллер с более чем 200 строками кода (LOC), как правило, являются хорошими кандидатами на рефакторинг.
  • Чрезмерно большая кодовая база. Если у вас есть ERB/HTML/HAML с более чем 30 000 LOC или исходный код Ruby (без GEM) с более чем 50 000 LOC, есть большая вероятность, что вам следует провести рефакторинг.

Попробуйте использовать что-то вроде этого, чтобы узнать, сколько строк исходного кода Ruby у вас есть:

find app -iname "*.rb" -type f -exec cat {} \;| wc -l

Эта команда будет искать все файлы с расширением .rb (файлы ruby) в папке /app и распечатывать количество строк. Обратите внимание, что это число является приблизительным, поскольку строки комментариев будут включены в эти итоги.

Еще один более точный и информативный вариант — использовать stats задач рейка Rails, которая выводит краткую сводку строк кода, количества классов, количества методов, соотношения методов к классам и соотношения строк кода на метод:

 bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
  • Могу ли я извлечь повторяющиеся шаблоны из моей кодовой базы?

Развязка в действии

Начнем с реального примера.

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

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

Нам нужна страница отчета, отображающая среднюю скорость и расстояние за неделю.

Если средняя скорость для записи выше, чем общая средняя скорость, мы уведомим пользователя с помощью SMS (в этом примере мы будем использовать Nexmo RESTful API для отправки SMS).

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

У нас также есть страница statistics , которая представляет собой еженедельный отчет, включающий среднюю скорость и пройденное расстояние за неделю.

  • Ознакомиться с онлайн-образцом можно здесь.

Код

Структура каталога app выглядит примерно так:

 ⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb

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

Что касается модели Entry , то она содержит бизнес-логику нашего приложения.

Каждая Entry принадлежит User .

Мы проверяем наличие time_period distance date_time и status для каждой записи.

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

  • Образец Gist

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

В entries_controller.rb есть основные действия CRUD (но без обновления). EntriesController#index получает записи для текущего пользователя и упорядочивает записи по дате создания, а EntriesController#create создает новую запись. Не нужно обсуждать очевидное и обязанности EntriesController#destroy :

  • Образец Gist

В то время как statistics_controller.rb отвечает за расчет еженедельного отчета, StatisticsController#index получает записи для вошедшего в систему пользователя и группирует их по неделям, используя метод #group_by , содержащийся в классе Enumerable в Rails. Затем он пытается украсить результаты, используя некоторые частные методы.

  • Образец Gist

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

Ниже представлен список записей для вошедшего в систему пользователя ( index.html.erb ). Это шаблон, который будет использоваться для отображения результатов действия индекса (метода) в контроллере записей:

  • Образец Gist

Обратите внимание, что мы используем частичный render @entries для извлечения общего кода в частичный шаблон _entry.html.erb , чтобы мы могли сохранить наш код СУХИМ и пригодным для повторного использования:

  • Образец Gist

То же самое касается _form . Вместо того, чтобы использовать один и тот же код с действиями (new и edit), мы создаем повторно используемую частичную форму:

  • Образец Gist

Что касается просмотра страницы еженедельного отчета, statistics/index.html.erb показывает некоторую статистику и сообщает о недельной производительности пользователя, группируя некоторые записи:

  • Образец Gist

И, наконец, хелпер для записей, entries_helper.rb , включает в себя два хелпера readable_time_period и readable_speed , которые должны сделать атрибуты более удобочитаемыми:

  • Образец Gist

Пока ничего особенного.

Большинство из вас возразит, что рефакторинг противоречит принципу KISS и сделает систему более сложной.

Так действительно ли это приложение нуждается в рефакторинге?

Абсолютно нет , но мы рассмотрим его только в демонстрационных целях.

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

Жизненный цикл

Итак, давайте начнем с объяснения структуры шаблона Rails MVC.

Обычно он начинается с того, что браузер делает запрос, например https://www.toptal.com/jogging/show/1 .

Веб-сервер получает запрос и использует routes , чтобы узнать, какой controller использовать.

Контроллеры выполняют работу по анализу пользовательских запросов, отправке данных, файлов cookie, сеансов и т. д., а затем запрашивают у model получение данных.

models — это классы Ruby, которые взаимодействуют с базой данных, хранят и проверяют данные, выполняют бизнес-логику и выполняют прочую тяжелую работу. Представления — это то, что видит пользователь: HTML, CSS, XML, Javascript, JSON.

Если мы хотим показать последовательность жизненного цикла запроса Rails, это будет выглядеть примерно так:

Rails разделяет жизненный цикл MVC

Чего я хочу добиться, так это добавить больше абстракции, используя простые старые рубиновые объекты (PORO) и сделать шаблон примерно следующим для действий create/update :

Форма создания диаграммы Rails

И что-то вроде следующего для действий list/show :

Запрос списка диаграмм Rails

Добавляя абстракции PORO, мы обеспечиваем полное разделение ответственности между SRP, в чем Rails не очень хорош.

Методические рекомендации

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

  • Модели ActiveRecord могут содержать ассоциации и константы, но не более того. Таким образом, это означает отсутствие обратных вызовов (используйте объекты службы и добавьте туда обратные вызовы) и никаких проверок (используйте объекты формы, чтобы включить именование и проверки для модели).
  • Сохраняйте контроллеры тонкими слоями и всегда вызывайте сервисные объекты. Некоторые из вас могут спросить, зачем вообще использовать контроллеры, если мы хотим продолжать вызывать сервисные объекты, чтобы содержать логику? Что ж, контроллеры — это хорошее место для маршрутизации HTTP, анализа параметров, аутентификации, согласования контента, вызова правильной службы или объекта редактора, перехвата исключений, форматирования ответа и возврата правильного кода состояния HTTP.
  • Службы должны вызывать объекты Query и не должны хранить состояние. Используйте методы экземпляра, а не методы класса. В соответствии с SRP должно быть очень мало общедоступных методов.
  • Запросы должны выполняться в объектах запросов. Методы объекта запроса должны возвращать объект, хэш или массив, а не ассоциацию ActiveRecord.
  • Избегайте использования помощников и вместо этого используйте декораторы. Почему? Обычная ошибка хелперов Rails заключается в том, что они могут превратиться в большую кучу не объектно-ориентированных функций, имеющих общее пространство имен и наступающих друг на друга. Но гораздо хуже то, что нет отличного способа использовать любой вид полиморфизма с хелперами Rails — предоставление разных реализаций для разных контекстов или типов, переопределение или подклассирование хелперов. Я думаю, что вспомогательные классы Rails обычно следует использовать для служебных методов, а не для конкретных случаев использования, таких как форматирование атрибутов модели для любой логики представления. Держите их легкими и свежими.
  • Избегайте использования проблем и вместо этого используйте декораторы/делегаторы. Почему? В конце концов, проблемы, кажется, являются основной частью Rails и могут СУХАТЬ код, когда они используются несколькими моделями. Тем не менее, основная проблема заключается в том, что проблемы не делают объект модели более связным. Код просто лучше организован. Другими словами, никаких реальных изменений в API модели нет.
  • Попробуйте извлечь объекты-значения из моделей , чтобы сделать ваш код чище и сгруппировать связанные атрибуты.
  • Всегда передавайте одну переменную экземпляра на представление.

Рефакторинг

Прежде чем мы начнем, я хочу обсудить еще одну вещь. Когда вы начинаете рефакторинг, обычно вы спрашиваете себя: «Это действительно хороший рефакторинг?»

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

Я не буду обсуждать такие вещи, как перемещение логики из контроллеров в модели, так как я предполагаю, что вы уже делаете это, и вам удобно использовать Rails (обычно Skinny Controller и модель FAT).

Ради краткости этой статьи я не буду обсуждать здесь тестирование, но это не значит, что вы не должны тестировать.

Напротив, вы всегда должны начинать с теста , чтобы убедиться, что все в порядке, прежде чем двигаться дальше. Это обязательно, особенно при рефакторинге.

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

Извлечение объектов-значений

Во-первых, что такое объект-значение?

Мартин Фаулер объясняет:

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

Иногда вы можете столкнуться с ситуацией, когда понятие заслуживает собственной абстракции и чье равенство основано не на ценности, а на тождестве. Примеры включают дату Ruby, URI и путь. Извлечение в объект-значение (или модель предметной области) очень удобно.

Зачем беспокоиться?

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

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

Когда это полезно?

Нет единого, универсального ответа. Делайте то, что лучше для вас и что имеет смысл в той или иной ситуации.

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

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

Как это делается?

Объекты-значения должны подчиняться некоторым основным правилам:

  • Объекты значений должны иметь несколько атрибутов.
  • Атрибуты должны быть неизменными на протяжении всего жизненного цикла объекта.
  • Равенство определяется атрибутами объекта.

В нашем примере я создам объект значения EntryStatus , чтобы абстрагировать Entry#status_weather и Entry#status_landform для их собственного класса, который выглядит примерно так:

  • Образец Gist

Примечание. Это просто обычный объект Ruby (PORO), который не наследуется от ActiveRecord::Base . Мы определили методы чтения для наших атрибутов и присваиваем их при инициализации. Мы также использовали сопоставимый миксин для сравнения объектов методом (<=>).

Мы можем изменить модель Entry , чтобы использовать созданный нами объект значения:

  • Образец Gist

Мы также можем изменить метод EntryController#create , чтобы использовать новый объект значения соответствующим образом:

  • Образец Gist

Извлечение сервисных объектов

Так что же такое сервисный объект?

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

Почему? Каковы преимущества?

  • Развязка. Сервисные объекты помогают добиться большей изоляции между объектами.
  • Видимость. Сервисные объекты (если они правильно названы) показывают, что делает приложение. Я могу просто просмотреть каталог сервисов, чтобы увидеть, какие возможности предоставляет приложение.
  • Уборочные модели и контроллеры. Контроллеры превращают запрос (параметры, сеанс, файлы cookie) в аргументы, передают их службе и перенаправляют или обрабатывают в соответствии с ответом службы. В то время как модели имеют дело только с ассоциациями и настойчивостью. Извлечение кода из контроллеров/моделей в сервисные объекты будет поддерживать SRP и сделает код более несвязанным. Тогда ответственность модели будет заключаться только в том, чтобы иметь дело с ассоциациями и сохранением/удалением записей, в то время как объект службы будет нести единственную ответственность (SRP). Это приводит к лучшему дизайну и лучшим модульным тестам.
  • СУШИТЕ и принимайте изменения. Я делаю сервисные объекты настолько простыми и маленькими, насколько это возможно. Я компоную сервисные объекты с другими сервисными объектами и повторно использую их.
  • Очистите и ускорьте набор тестов. Сервисы легко и быстро тестировать, поскольку они представляют собой небольшие объекты Ruby с одной точкой входа (метод вызова). Сложные сервисы состоят из других сервисов, поэтому вы можете легко разделить свои тесты. Кроме того, использование служебных объектов упрощает имитацию/заглушку связанных объектов без необходимости загрузки всей среды rails.
  • Возможность вызова из любого места. Сервисные объекты, скорее всего, будут вызываться из контроллеров, а также другие сервисные объекты, DelayedJob/Rescue/Sidekiq Jobs, задачи Rake, консоль и т. д.

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

Когда следует извлекать служебные объекты?

Здесь также нет жесткого правила.

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

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

Вот некоторые индикаторы того, когда следует использовать объекты службы:

  • Действие сложное.
  • Действие распространяется на несколько моделей.
  • Действие взаимодействует с внешней службой.
  • Действие не является основной задачей базовой модели.
  • Существует несколько способов выполнения действия.

Как следует проектировать сервисные объекты?

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

Я обычно использую следующие рекомендации и соглашения для разработки объекта службы:

  • Не сохранять состояние объекта.
  • Используйте методы экземпляра, а не методы класса.
  • Общедоступных методов должно быть очень мало (желательно один для поддержки SRP.
  • Методы должны возвращать объекты расширенного результата, а не логические значения.
  • Сервисы находятся в каталоге app/services . Я рекомендую вам использовать подкаталоги для доменов с большим объемом бизнес-логики. Например, файл app/services/report/generate_weekly.rb будет определять Report::GenerateWeekly , а файл app/services app/services/report/publish_monthly.rbReport::PublishMonthly .
  • Сервисы начинаются с глагола (и не заканчиваются на Service): ApproveTransaction , SendTestNewsletter , ImportUsersFromCsv .
  • Службы реагируют на метод вызова. Я обнаружил, что использование другого глагола делает его немного избыточным: ApproveTransaction.approve() плохо читается. Кроме того, метод call является методом де-факто для лямбда-выражений, процедур и объектов-методов.

Если вы посмотрите на StatisticsController#index , вы заметите группу методов ( weeks_to_date_from , weeks_to_date_to , avg_distance и т. д.), связанных с контроллером. Это не очень хорошо. Подумайте о разветвлениях, если вы хотите генерировать еженедельный отчет вне statistics_controller .

В нашем случае создадим Report::GenerateWeekly и извлечем логику отчета из StatisticsController :

  • Образец Gist

Итак StatisticsController#index теперь выглядит чище:

  • Образец Gist

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

Домашнее задание: рассмотрите возможность использования объекта Value для WeeklyReport вместо Struct .

Извлечение объектов запроса из контроллеров

Что такое объект запроса?

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

Вам следует выделить сложные запросы SQL/NoSQL в отдельный класс.

Каждый объект Query отвечает за возврат набора результатов на основе критериев / бизнес-правил.

В этом примере у нас нет сложных запросов, поэтому использование объекта Query не будет эффективным. Однако для демонстрации давайте извлечем запрос в Report::GenerateWeekly#call и создадим generate_entries_query.rb :

  • Образец Gist

А в Report::GenerateWeekly#call заменим:

 def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end

с участием:

 def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end

Шаблон объекта запроса помогает сохранить логику вашей модели строго связанной с поведением класса, а также сохранить ваши контроллеры тонкими. Поскольку они представляют собой не что иное, как простые старые классы Ruby, объекты запросов не должны наследоваться от ActiveRecord::Base и не должны отвечать ни за что, кроме выполнения запросов.

Извлечь Создать запись в сервисный объект

Теперь давайте извлечем логику создания новой записи в новый объект службы. Давайте воспользуемся соглашением и создадим CreateEntry :

  • Образец Gist

И теперь наш EntriesController#create выглядит следующим образом:

 def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end

Переместить проверки в объект формы

Теперь здесь все становится интереснее.

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

Объект формы — это обычный объект Ruby (PORO). Он берет на себя управление объектом контроллера/службы везде, где ему нужно общаться с базой данных.

Зачем использовать объекты формы?

При рефакторинге приложения всегда полезно помнить о принципе единой ответственности (SRP).

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

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

Вот где появляются объекты формы.

Объект Form отвечает за представление формы в вашем приложении. Таким образом, каждое поле ввода можно рассматривать как атрибут в классе. Он может проверить, соответствуют ли эти атрибуты некоторым правилам проверки, и может передать «чистые» данные туда, куда им нужно (например, в модели вашей базы данных или, возможно, в ваш построитель поисковых запросов).

Когда следует использовать объект формы?

  • Если вы хотите извлечь проверки из моделей Rails.
  • Когда несколько моделей могут быть обновлены одной отправкой формы, вы можете захотеть создать объект формы.

Это позволяет вам поместить всю логику формы (соглашения об именах, проверки и т. д.) в одном месте.

Как создать объект формы?

  • Создайте простой класс Ruby.
  • Включите ActiveModel::Model (в Rails 3 вместо этого вы должны включить Naming, Conversion и Validations)
  • Начните использовать свой новый класс формы, как если бы это была обычная модель ActiveRecord, самая большая разница в том, что вы не можете сохранять данные, хранящиеся в этом объекте.

Обратите внимание, что вы можете использовать гем реформы, но придерживаясь PORO, мы создадим entry_form.rb , который выглядит следующим образом:

  • Образец Gist

И мы CreateEntry , чтобы начать использовать объект формы EntryForm :

 class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end

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

Переместить обратные вызовы в сервисный объект

Как мы договорились ранее, мы не хотим, чтобы наши модели содержали проверки и обратные вызовы. Мы извлекли проверки, используя объекты формы. Но мы все еще используем некоторые обратные вызовы ( after_create в Entry модели compare_speed_and_notify_user ).

Почему мы хотим удалить обратные вызовы из моделей?

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

обратные вызовы after_* в основном используются для сохранения или сохранения объекта.

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

В нашем случае мы отправляем SMS пользователю после сохранения записи, которая на самом деле не связана с доменом Entry.

Простой способ решить проблему — переместить обратный вызов на соответствующий объект службы. В конце концов, отправка SMS для конечного пользователя связана с сервисным объектом CreateEntry , а не с самой моделью Entry.

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

Итак, теперь наша CreateEntry выглядит примерно так:

  • Образец Gist

Используйте декораторы вместо помощников

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

Мне нужен класс, который будет вызывать методы декорированного объекта.

Я могу использовать method_missing для реализации этого, но я буду использовать стандартную библиотеку SimpleDelegator .

Следующий код показывает, как использовать SimpleDelegator для реализации нашего базового декоратора:

 % app/decorators/base_decorator.rb require 'delegate' class BaseDecorator < SimpleDelegator def initialize(base, view_context) super(base) @object = base @view_context = view_context end private def self.decorates(name) define_method(name) do @object end end def _h @view_context end end

Итак, почему метод _h ?

Этот метод действует как прокси для контекста представления. По умолчанию контекст представления является экземпляром класса представления, классом представления по умолчанию является ActionView::Base . Вы можете получить доступ к помощникам представления следующим образом:

 _h.content_tag :div, 'my-div', class: 'my-class'

Чтобы было удобнее, добавим в ApplicationHelper метод decorate :

 module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= "#{object.class}Decorator".constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end

Теперь мы можем переместить хелперы EntriesHelper в декораторы:

 # app/decorators/entry_decorator.rb class EntryDecorator < BaseDecorator decorates :entry def readable_time_period mins = entry.time_period return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60 Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe end def readable_speed "#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe end end

И мы можем использовать readable_time_period и readable_speed так:

 # app/views/entries/_entry.html.erb - <td><%= readable_speed(entry) %> </td> + <td><%= decorate(entry).readable_speed %> </td>
 - <td><%= readable_time_period(entry) %></td> + <td><%= decorate(entry).readable_time_period %></td>

Структура после рефакторинга

В итоге мы получили больше файлов, но это не обязательно плохо (и помните, что с самого начала мы признали, что этот пример был только для демонстрационных целей и не обязательно был хорошим вариантом использования для рефакторинга):

 app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb

Заключение

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

При использовании MVC в качестве архитектуры веб-приложений все остается связанным и замедляет работу, поскольку большинство изменений влияют на другие части приложения. Кроме того, это заставляет вас думать, куда поместить некоторую бизнес-логику — в модель, контроллер или представление?

Используя простые PORO, мы переместили бизнес-логику в модели или службы, которые не наследуются от ActiveRecord , что уже является большой победой, не говоря уже о том, что у нас есть более чистый код, который поддерживает SRP и более быстрые модульные тесты.

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

Надеюсь, я продемонстрировал, как использование Plain Old Ruby Objects и других абстракций разделяет проблемы, упрощает тестирование и помогает создавать чистый, удобный для сопровождения код.

Связанный: Каковы преимущества Ruby on Rails? После двух десятилетий программирования я использую Rails