Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Ruby on Rails
Code

Антипаттерны: контроллеры Rails

by
Length:LongLanguages:

Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)

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

Темы

  • Толстые контроллеры
  • Не-RESTful контроллеры
  • Вложенные ресурсы

Толстые контроллеры

Ну, «толстые модели, тонкие контроллеры», правильно? Если вы не прочитали предыдущие статьи из серии Антипаттернов, я должен упомянуть, что стремление к контроллерам, которые остаются тонкими, является лучшим ориентиром  для вас. Все, что добавляет лишний жир не подходит для ваших проектов - «все тонкое» имеет гораздо больше смысла. (Может быть, я должен четко указать, что я никоим образом не связан с индустрией моды и не хочу произвести впечатление, что нельзя считать красивым, не приспосабливаясь к определенному типу воображаемого тела.)

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

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

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

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

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

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

Презентеры

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

Презернтеры могут «имитировать» модель, которая представляет состояние, которое в свою очередь требуется вашему представлению, и объединяет атрибуты, которые необходимо перемещать через контроллер. Они конечно могут быть более сложными, но тогда я начинаю чувствовать что они уже залезают на территорию "Декоратора".

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

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

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

Поведение пользователя тоже играет при этом ключевую роль. В приведенном ниже примере мы хотим создать простую миссию, которая имеет has_one agent и один quartermaster. Это хороший пример того, как быстро все может выйти из-под контроля. Контроллеру нужно манипулировать несколькими объектами, которые нужно отобразить во вложенной форме, чтобы связать вещи вместе. Вы скоро увидите, что все это можно вылечить с помощью приятного «объекта формы», который представляет необходимые объекты и объединяет все в одном центральном классе.

app/models/mission.rb

Я упоминаю модели здесь только ради полноты картины, если вы никогда до этого не использовали fields_for  - все просто. Ниже приведена вся суть дела.

Слишком много переменных экземпляра

app/controllers/missions_controller.rb

В целом, легко увидеть, что здесь мы движемся в неправильном направлении. Уже все довольно громоздко, хотя и состоит только из методов new и create. Нехорошо! Приватные методы уже слишком быстро накапливаются. Надеюсь, что agent_params и quartermaster_params в MissionsController не кажутся вам слишком гладкими. Вы считаете, что такое редко случается? Боюсь что нет. «Единственная ответственность» в контроллерах действительно являются золотым ориентиром. И через минуту вы увидите почему.

Даже если вы прищурились, это выглядит черезчур противно. И во время сохранения в действии create сразу с валидацией, если мы не сможем сохранить каждый объект из-за какой-либо ошибки или чего-то еще, мы закончим с потерянными объектами, с которыми никто не хочет иметь дело. Хлоп!

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

Следуя этому пути, в представлении будет прилагаться form_for для @mission и дополнительные fields_for для @agent и @quartermaster.

Грязная форма с несколькими объектами

app/views/missions/new.html.erb

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

Презентер объекта формы

app/views/missions/new.html.erb

Как вы можете легко увидеть, наше отображение стало намного проще - никаких больше вложений. Но есть часть, с которой вы должны быть немного осторожны:

Вам необходимо предоставить form_for вместе с url, чтобы можно было отправить параметры из этой формы в соответствующий контроллер - в данном случае MissionsController. Без этого дополнительного аргумента Rails попытается найти контроллер для нашего объекта-презетнера @mission_presenter с помощью соглашений - в данном случае MissionFormPresentersController - и конечно же сломаться без него.

В общем, мы должны стараться держать наши действия контроллера в основном такими же простыми, как будто мы имеем дело с манипуляцией ресурсами CRUD - это то, что контроллер делает и для чего он лучше всего оборудован, чтобы не смешивать компоненты MVC. Как хороший побочный эффект, уровень сложности в ваших контроллерах при этом также заметно снизится.

app/controllers/missions_controller.rb

Контроллер становится проще прямо на глазах, не так ли? Гораздо чище и почти стандартные действия контроллера. Мы имеем дело с одним объектом, который выполняет одну работу. Мы создаем экземпляр одного объекта, презентера, и наполняем его как обычно параметрами.

Единственное, что меня беспокоило, это отправка этого длинного списка параметров. Я извлек их в метод whitelisted, который просто возвращает массив с полным списком параметров. В противном случае mission_params выглядел бы следующим образом: это было слишком неприятно:

И да, слово об аргументе :mission_form_presenter для params.require. Хотя мы назвали нашу переменную экземпляра для презентера @mission_presenter, когда мы используем его с form_for, Rails ожидает, что ключ хэша params для формы будет называться по имени объекта, созданного экземпляром, а не по имени, указанного в контроллере. Я видел как новички несколько раз спотыкались на этом. То, что Rails предоставляет вам критические ошибки в таком случае, также не помогает. Если вам нужно немного переосмыслить параметры, вот хорошее место для поиска:

В нашей модели Mission мы больше не нуждаемся в accepts_nested_attributes и можем избавиться от этой безобидной, но страшной вещи. Метод validates также не имеет здесь значения, потому что мы добавляем эту ответственность к нашему объекту формы. То же самое можно сказать и о наших проверках на Agent и Quartermaster.

app/models/mission.rb

Инкапсуляция этой логики проверки непосредственно на наш новый объект помогает нам сохранять чистоту и организованность кода. Разумеется, в тех случаях, когда вы также можете создавать эти объекты независимо друг от друга, проверки должны оставаться там, где они нужны в настоящее время. Этот вид дублирования также можно решить, например, используя validates_with с отдельным классом для проверки, который наследуется от ActiveModel::Validator.

Теперь у нас есть тонкий контроллер с единственной ответственностью и простая форма для одновременного создания нескольких объектов. Потрясающе! Как мы достигли всего этого? Ниже представлен презентер, который делает всю работу, не подразумевая, что этот класс выполняет большую работу. Мы хотим иметь какую-то промежуточную модель без базы данных, которая управляет несколькими объектами. Взгляните на этот простой старый ruby объект (PORO).

app/presenters/mission_form_presenter.rb

Я думаю, что будет справедливо сказать, что это выглядит не очень сложно. MissionFormPresenter - это объект формы, который теперь инкапсулирует то, что делало наш контроллер излишне толстым. В качестве бонуса наше отображение теперь стало тонким простым. Здесь мы можем агрегировать всю информацию из нашей формы, а затем последовательно мы создаем все объекты, которые нам нужны последовательно.

Самая важная часть происходит в нашем новом методе save. Сначала мы создаем новый объект Mission. После этого мы можем создать два связанных с ним объекта: Agent и Quartermaster. Через наши ассоциации has_one и belongs_to мы можем использовать метод create_x, который адаптируется к имени связанного объекта.

Например, если мы используем has_one :agent, мы получаем метод create_agent. Легко, правда? (На самом деле мы также получаем метод build_agent.) Я решил использовать версию с ошибкой (!), потому что она вызывает ошибку ActiveRecord::RecordInvalid, если запись недействительна при попытке ее сохранить. Если обернуть эти методы внутрь блока transaction, то мы можем быть спокойны что объект не будет сохранен если не прошла валидация. Блок транзакций откатится, если что-то пойдет не так во время сохранения.

Но вы можете спросить, как это работает с атрибутами? Мы просим Rails немного поработать с помощью include ActiveModel::Model  (API). Это позволяет нам инициализировать объекты с помощью хэша атрибутов - а это именно то, что мы делаем в контроллере. После этого мы можем использовать наши методы attr_accessor для извлечения наших атрибутов для создания объектов, которые нам действительно нужны.

ActiveModel::Model также позволяет нам взаимодействовать с представлениями и контроллерами. Среди других лакомств вы также можете использовать это для валидации в подобных классах. Помещение этих валидаций в такие объекты выделенной формы - хорошая идея для организации вашего кода, и это также помогает вашим моделям выглядеть немного аккуратнее.

Я решил извлечь длинный список параметров в приватные методы, которые заполняют объекты, созданные в save. В таком объекте-презентере я мало беспокоюсь о том, что вокруг есть еще несколько приватных методов. Почему нет? Так гораздо чище!

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

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

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

Не-RESTful контроллеры

Скорее всего будет плохой идеей не придерживаться стандартных действий контроллера. Вы можете легко избежать антипаттерна, когда у вас контроллеры с огромной кучей кастомных методов. Такие методы, как login_user, activate_admin, show_books и другие забавные имена, которые на самом деле стоят за new, create, show и т.д., должны дать вам повод для паузы и усомниться в вашем подходе. Не следование подходу RESTful может легко привести к массивным контроллерам, главным образом, потому что вам придется сражаться с фреймворком или каждый раз изобретать колесо.

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

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

В большинстве случаев все это вам подойдет. FYI, действия new и edit не являются частью REST - они больше похожи на разные версии действия show, помогая вам представить различные этапы жизненного цикла ресурса. В совокупности, в большинстве случаев, эти семь стандартных действий контроллера дают вам все необходимое для управления вашими ресурсами в ваших контроллерах. Еще одно большое преимущество заключается в том, что другие разработчики Rails, работающие с вашим кодом, смогут быстрее перемещаться по вашим контроллерам.

Следуя этой простой логике RESTful также поможет им разобраться как вы называете свои контролеры. Имя ресурса, на котором вы работаете, должно быть отражено в объекте контроллера.

Например, наличие MissionsController, который обрабатывает другие ресурсы, помимо объектов @mission, является признаком того, что что-то здесь не так. Большой размер контроллера часто также является явным признаком того, что REST игнорировался. Если вы столкнулись с большими контроллерами, которые реализуют множество кастомных методов, которые не соблюдают соглашения, то может стать весьма эффективно стратегией, чтобы разделить их на несколько контроллеров, которые имеют целенаправленные обязанности и в основном управляют только одним ресурсом, придерживаясь стиля RESTful. Разделите их, и вам будет легче создавать свои методы Rails.

Вложенные ресурсы

Посмотрите на следующий пример и спросите себя, что здесь не так:

Вложенный AgentsController

app/controllers/agents_controller.rb

Здесь мы проверяем, есть ли у нас вложенный маршрут, который предоставляет нам идентификатор для возможного объекта @mission. Если это так, то мы хотим использовать связанный объект, чтобы получить от него agents. В противном случае мы получим список всех агентов для представления. Выглядит безвредно, особенно потому, что он по-прежнему очень простой, но это начало потенциальной более крупной вложенности.

Вложенные маршруты

Здесь нет ничего плохого во вложенных маршрутах. В общем, нет ничего плохого в этом подходе. Мы должны быть осторожны в том, как контроллер обрабатывает все это дело - и, как следствие, то, как представление должно адаптироваться. Не совсем чисто, как вы можете видеть ниже.

Отображение с ненужными условиями

app/views/agents/index.html.erb

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

Я думаю, это немного красноречиво относится к отдельным обязанностям. Это не слишком сильно нарушает эту идею, хотя у нас есть вторй объект - @mission . Но поскольку мы используем его для получения доступа к определенному набору агентов, это абсолютно нормально.

Разветвление является неотъемлемой частью и, скорее всего, приведет к плохим проектным решениям - как в представлениях, так и в контроллерах. Создание двух версий @agents одним и тем же методом является здесь плохим местом. Короче говоря, все это очень быстро может выйти из-под контроля. Как только вы начнете использовать такие ресурсы, возможно, скоро появятся новые крысы.

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

Отдельные контроллеры

Вместо того, чтобы вкладывать эти ресурсы, мы должны предоставлять каждой версии этого ресурса свой собственный отдельный контроллер-один контроллер для «простых», неназванных агентов и один для агентов, связанных с миссией. Мы можем добиться этого, сделав для одного из них неймспейс в папке /missions.

app/controllers/missions/agents_controller.rb

Обертывая этот контроллер внутри модуля, мы можем избежать того, чтобы AgentsController дважды наследовался от ApplicationController. Без этого мы бы столкнулись бы с такой ошибкой: Unable to autoload constant Missions::AgentsController. Я думаю, что модуль представляет собой небольшую цену, чтобы заплатить за автозагрузку Rails. Второй AgentsController может оставаться в том же файле, что и раньше. Теперь он имеет дело только с одним возможным ресурсом в index - отображение всех агентов без миссий, которые находятся вокруг.

app/controllers/agents_controller.rb

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

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

Новые маршруты

Наш вложенный ресурс для agents теперь правильно перенаправляется на controllers/missions/agents_controller.rb и каждое действие может заботиться о агентах, которые являются частью миссии. Ради полноты, давайте посмотрим также на наши итоговые отображения:

Агенты с миссией

app/views/missions/agents/index.html.erb

Агенты без миссии

app/views/agents/index.html

Так, давайте избавимся от этого небольшого количества дублирования, где мы также перебираем @agents. Я создал партиал для рендеринга список агентов и поместил его в shared каталог под views.

app/views/shared/_agents.html.erb

Здесь ничего нового или удивительного, но наши отображения теперь лучше следуют принципу DRY.

Агенты с миссией

app/views/missions/agents/index.html.erb

Агенты без миссии

app/views/agents/index.html

Готово!

Заключение

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

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

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

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.