Создавайте тупые, рефакторинг умные: как убрать проблемы из кода Ruby on Rails

Опубликовано: 2022-03-11
Дэниел Льюис был профессиональным разработчиком Ruby on Rails более 4 лет, работая над дюжиной веб-приложений с высоким трафиком, многие из них через Toptal.

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

Иногда нам не нравится запрос функции, потому что самый простой способ решить его — написать плохой код, а у нас нет элегантного решения в голове. Это отправит многих из нас, разработчиков Rails, на бесплодные поиски в RubyToolbox, GitHub, блогах разработчиков и StackOverflow в поисках драгоценного камня, плагина или примера кода, который заставит нас чувствовать себя лучше.

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

Рефакторинг кода Ruby on Rails

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

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

Этот процесс рефакторинга Ruby on Rails является тем, которому я следую для решения проблем Rails.

В качестве альтернативы, вот журнал коммитов Git для функции, которая была рефакторингом шаг за шагом:

Этот журнал демонстрирует пошаговый рефакторинг Rails.

А вот еще одна интересная статья о масштабном рефакторинге от коллеги по сети Toptal.

Давайте посмотрим, как это делается.

Виды

Шаг 1. Начните с представлений

Скажем, мы начинаем тикет для новой функции. Клиент говорит нам: «Посетители должны иметь возможность просматривать список активных проектов на странице приветствия».

Этот билет требует видимого изменения, поэтому разумным местом для начала работы будет Views. Проблема проста, и нас всех учили решать несколько раз. Я собираюсь решить ее неправильным путем и продемонстрирую, как реорганизовать мое решение в соответствующих областях. Решение проблемы неправильным способом может помочь нам преодолеть трудности, связанные с незнанием правильного решения.

Для начала предположим, что у нас есть модель Project с логическим атрибутом active . Мы хотим получить список всех Projects , для которых значение active равно true , поэтому мы можем использовать Project.where(active: true) и перебирать его в цикле с each блоком.

 app/views/pages/welcome.haml: %ul.projects - Project.where(active: true).each do |project| %li.project= link_to project_path(project), project.name

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

  1. Вы можете получить это изменение на этапе подготовки менее чем за 15 минут.
  2. Если оставить этот блок, его легко кэшировать.
  3. Исправить эту проблему с Rails несложно (можно поручить младшему разработчику).

Шаг 2. Частицы

После того, как я сделал это The Wrong Way , я чувствую себя плохо и хочу изолировать свой плохой код. Если бы это изменение касалось только слоя просмотра, я мог бы преобразовать свой позор в частичный.

 app/views/pages/welcome.haml: = render :partial => 'shared/projects_list' app/views/shared/projects_list.haml: %ul.projects - Project.where(active: true).each do |project| %li.project= link_to project_path(project), project.name

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

Исправить что-то явно глупое всегда проще, чем исправить плохо реализованную, глючную абстракцию.
Твитнуть

Шаг 3. Помощники

Помощники в Rails — это способ создания DSL (предметно-ориентированного языка) для раздела ваших представлений. Вы должны переписать свой код, используя content_tag вместо haml или HTML, но вы получаете возможность манипулировать структурами данных без того, чтобы другие разработчики смотрели на вас из-за 15 строк непечатаемого кода View.

Если бы я использовал помощников здесь, я бы, вероятно, реорганизовал тег li .

 app/views/shared/projects_list.haml: %ul.projects - Project.where(active: true).each do |project| = project_list_item(project) app/helpers/projects_helper.rb: def project_list_item(project) content_tag(:li, :class => 'project') do link_to project_path(project), project.name end end

Контроллеры

Шаг 4. Переместите его в контроллеры

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

 app/views/shared/projects_list.haml: %ul.projects - @projects_list.each do |project| = project_list_item(project) app/controllers/pages_controller.rb: def welcome @projects = Project.where(active: true) end

Шаг 5. Фильтры контроллера

Наиболее очевидная причина для перемещения кода в контроллер before_filter или after_filter — это код, который вы продублировали в нескольких действиях контроллера. Вы также можете переместить код в фильтр контроллера, если хотите отделить цель действия контроллера от требований ваших представлений.

 app/controllers/pages_controller.rb: before_filter :projects_list def welcome end def projects_list @projects = Project.where(active:true) end

Шаг 6. Контроллер приложений

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

 app/controllers/pages_controller.rb: def welcome end app/views/layouts/application.haml: %ul.projects - projects_list.each do |project| = project_list_item(project) app/controllers/application_controller.rb: before_filter :projects_list def projects_list @projects = Project.where(active: true) end

Модели

Шаг 7. Рефакторинг модели

Как гласит девиз MVC: толстая модель, тощие контроллеры и тупые представления . Ожидается, что мы рефакторим все, что можем, в модель, и это правда, что самая сложная функциональность в конечном итоге станет ассоциациями модели и методами модели. Мы всегда должны избегать форматирования/просмотра вещей в модели, но допустимо преобразование данных в другие типы данных. В этом случае лучше всего рефакторить в Модель предложение where(active: true) , которое мы можем превратить в область действия. Использование области полезно не только потому, что она делает вызов более красивым, но если мы когда-нибудь решим добавить атрибут, такой как delete или outdated , мы можем изменить эту область вместо того, чтобы искать все наши предложения where .

 app/controllers/application_controller.rb: before_filter :projects_list def projects_list @projects = Project.active end app/models/project.rb: scope :active, where(active: true)

Шаг 8. Фильтры модели

В этом случае у нас нет конкретного применения для фильтров before_save или after_save filters модели, но следующим шагом, который я обычно делаю, является перенос функциональности из контроллеров и методов модели в фильтры модели.

Скажем, у нас есть еще один атрибут, num_views . Если num_views > 50 , проект становится неактивным. Мы могли бы решить эту проблему в представлении, но вносить изменения в базу данных в представлении неуместно. Мы могли бы решить это в контроллере, но наши контроллеры должны быть как можно тоньше! Мы можем легко решить это в модели.

 app/models/project.rb: before_save :deactivate_if_over_num_views def deactivate_if_over_num_views if num_views > 50 self.active = false fi end

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

Шаг 9. Библиотеки

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

Файлы библиотеки можно хранить в каталоге lib/ , но по мере их роста вы можете перенести их в настоящий RubyGem! Основное преимущество переноса кода в библиотеку заключается в том, что вы можете тестировать библиотеку отдельно от своей модели.

В любом случае, в случае со списком проектов мы могли бы оправдать перенос вызова scope :active из модели Project в файл библиотеки и вернуть его в Ruby:

 app/models/project.rb: class Project < ActiveRecord::Base include Activeable before_filter :deactivate_if_over_num_views end lib/activeable.rb: module Activeable def self.included(k) k.scope :active, k.where(active: true) end def deactivate_if_over_num_views if num_views > 50 self.active = false end end end

Примечание: метод self.included вызывается при загрузке класса модели Rails и передается в область видимости класса как переменная k .

Заключение

В этом руководстве по рефакторингу Ruby on Rails мы потратили менее 15 минут, реализовали решение и поставили его на промежуточный этап для пользовательского тестирования, готового к принятию в набор функций или удалению. К концу процесса рефакторинга у нас есть фрагмент кода, который излагает структуру для реализации элементов с возможностью списка и активации в нескольких моделях, которые прошли бы даже самый строгий процесс проверки.

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

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

Связанный: Усечение метки времени: история Ruby on Rails ActiveRecord