Статьи

Основы AntiPatterns: контроллеры Rails

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

  • Жировые контроллеры
  • Контроллеры без RESTful
  • Ресурсы Гнезда Крысы

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

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

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

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

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

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

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

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

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

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

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

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

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

приложение / модели / mission.rb

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Mission < ActiveRecord::Base
  has_one :agent
  has_one :quartermaster
  accepts_nested_attributes_for :agent, :quartermaster, allow_destroy: true
 
  validates :mission_name, presence: true
  …
 
end
 
class Agent < ActiveRecord::Base
  belongs_to :mission
  validates :name, presence: true
  …
 
end
 
class Quartermaster < ActiveRecord::Base
  belongs_to :mission
  validates :name, presence: true
  …
 
end

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

приложение / контроллеры / missions_controller.rb

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class MissionsController < ApplicationController
  def new
    @mission = Mission.new
    @agent = Agent.new
    @quartermaster = Quartermaster.new
  end
 
  def create
    @mission = Mission.new(mission_params)
    @agent = Agent.new(agent_params)
    @quartermaster = Quartermaster.new(quartermaster_params)
 
    @mission.agent = @agent
    @mission.quartermaster = @quartermaster
 
    if @account.save and @agent.save and @quartermaster.save
      flash[:notice] = ‘Mission accepted’
      redirect_to missions_path
    else
      flash[:alert] = ‘Mission not accepted’
      render :new
    end
  end
 
  private
 
  def mission_params
    params.require(:mission).permit(:mission_name, :objective, :enemy)
  end
 
  def agent_params
    params.require(:agent).permit(:name, :number, :licence_to_kill)
  end
 
  def quartermaster_params
    params.require(:quartermaster).permit(:name, :number, :hacker, :expertise, :humor)
  end
 
end

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

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

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

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

приложение / просмотров / миссии / new.html.erb

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<%= form_for(@mission) do |mission|
 
  <h3>Mission</h3>
    <%= mission.label :mission_name %>
    <%= mission.text_field :mission_name %>
 
    <%= mission.label :objective %>
    <%= mission.text_field :objective %>
 
    <%= mission.label :enemy %>
    <%= mission.text_field :enemy %>
 
  <h3>Agent</h3>
  <%= fields_for @agent do |agent|
      <%= agent.label :name %>
      <%= agent.text_field :name %>
 
      <%= agent.label :number %>
      <%= agent.text_field :number %>
 
      <%= agent.label :licence_to_kill %>
      <%= agent.check_box :licence_to_kill %>
  <% end %>
 
  <h3>Quartermaster</h3>
  <%= fields_for @quartermaster do |quartermaster|
      <%= quartermaster.label :name %>
      <%= quartermaster.text_field :name %>
 
      <%= quartermaster.label :number %>
      <%= quartermaster.text_field :number %>
 
      <%= quartermaster.label :hacker %>
      <%= quartermaster.check_box :hacker %>
 
      <%= quartermaster.label :expertise %>
      <%= quartermaster.text_field :expertise %>
 
      <%= quartermaster.label :humor %>
      <%= quartermaster.check_box :humor %>
  <% end %>
 
  <%= mission.submit %>
<% end %>

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

приложение / просмотров / миссии / new.html.erb

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<%= form_for @mission_presenter, url: missions_path do |mission|
  <h3>Mission</h3>
    <%= mission.label :mission_name %>
    <%= mission.text_field :mission_name %>
 
    <%= mission.label :objective %>
    <%= mission.text_field :objective %>
 
    <%= mission.label :enemy %>
    <%= mission.text_field :enemy %>
 
  <h3>Agent</h3>
    <%= mission.label :agent_name %>
    <%= mission.text_field :agent_name %>
 
    <%= mission.label :agent_number %>
    <%= mission.text_field :agent_number %>
 
    <%= mission.label :licence_to_kill %>
    <%= mission.check_box :licence_to_kill %>
 
  <h3>Quartermaster</h3>
    <%= mission.label :quartermaster_name %>
    <%= mission.text_field :quartermaster_name %>
 
    <%= mission.label :quartermaster_number %>
    <%= mission.text_field :quartermaster_number %>
 
    <%= mission.label :hacker %>
    <%= mission.check_box :hacker %>
 
    <%= mission.label :expertise %>
    <%= mission.text_field :expertise %>
 
    <%= mission.label :humor %>
    <%= mission.check_box :humor %>
 
  <%= mission.submit %>
<% end %>

Как вы можете легко видеть, наше представление стало намного проще — никаких вложений, и эта квартира намного проще. Часть, в которой вы должны быть немного осторожны, такова:

1
<%= form_for @mission_presenter, url: missions_path do |mission|

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

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

приложение / контроллеры / missions_controller.rb

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MissionsController < ApplicationController
 
  def new
    @mission_presenter = MissionFormPresenter.new
  end
 
  def create
    @mission_presenter = MissionFormPresenter.new(mission_params)
    if
      @mission_presenter.save
      flash[:notice] = ‘Mission accepted’
      redirect_to missions_path
    else
      flash[:alert] = ‘Mission not accepted’
      render :new
    end
  end
 
  private
 
  def mission_params
    params.require(:mission_form_presenter).permit(whitelisted)
  end
 
  def whitelisted
    [:mission_name, :objective, :enemy, :agent_name, :agent_number, :licence_to_kill, :quartermaster_name, :quartermaster_number, :hacker, :expertise, :humor]
  end
end

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

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

01
02
03
04
05
06
07
08
09
10
11
12
def mission_params
  params.require(:mission_form_presenter).permit(:mission_name,
                                                 :objective, :enemy,
                                                 :agent_name,
                                                 :agent_number,
                                                 :licence_to_kill,
                                                 :quartermaster_name,
                                                 :quartermaster_number,
                                                 :hacker,
                                                 :expertise,
                                                 :humor)
end

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

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

приложение / модели / mission.rb

1
2
3
4
5
6
7
8
9
class Mission < ActiveRecord::Base
  has_one :agent
  has_one :quartermaster
  #accepts_nested_attributes_for :agent, :quartermaster, allow_destroy: true
 
  #validates :mission_name, presence: true
  …
 
end

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

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

приложение / Ведущие / mission_form_presenter.rb

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class MissionFormPresenter
  include ActiveModel::Model
 
  attr_accessor :mission_name, :objective, :enemy, :agent_name,
                 :agent_number, :licence_to_kill, :quartermaster_name,
                 :quartermaster_number, :hacker, :expertise, :humor
 
  validates :mission_name, :agent_name, :quartermaster_name, presence: true
 
  def save
    ActiveRecord::Base.transaction do
      @mission = Mission.create!(mission_attributes)
      @mission.create_agent!(agent_attributes)
      @mission.create_quartermaster!(quartermaster_attributes)
    end
  end
 
  private
 
  def mission_attributes
    { mission_name: mission_name, objective: objective, enemy: enemy }
  end
 
  def agent_attributes
    { name: agent_name, number: agent_number, licence_to_kill: licence_to_kill }
  end
 
  def quartermaster_attributes
    { name: quartermaster_name, number: quartermaster_number, hacker: hacker, expertise: expertise, humor: humor }
  end
end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

приложение / контроллеры / agents_controller.rb

01
02
03
04
05
06
07
08
09
10
class AgentsController < ApplicationController
  def index
    if params[:mission_id]
      @mission = Mission.find(params[:mission_id])
      @agents = @mission.agents
    else
      @agents = Agent.all
    end
  end
end

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

1
2
3
4
resources :agents
resources :missions do
  resources :agents
end

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

приложение / просмотров / агентов / index.html.erb

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<% if @mission %>
  <h2>Mission</h2>
  <div><%= @mission.name %></div>
  <div><%= @mission.objective %></div>
  <div><%= @mission.enemy %></div>
<% end %>
 
<h2>Agents</h2>
<ul>
  <% @agents.each do |agent|
    <li class=’agent’>
      <div>Name: <%= agent.name %></div>
      <div>Number: <%= agent.number %></div>
      <div>Licence to kill: <%= agent.licence_to_kill %></div>
      <div>Status: <%= agent.status %></div>
    </li>
  <% end %>
</ul>

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

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

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

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

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

приложение / контроллеры / миссии / agents_controller.rb

1
2
3
4
5
6
7
8
9
module Missions
  class AgentsController < ApplicationController
   
    def index
      @mission = Mission.find(params[:mission_id])
      @agents = @mission.agents
    end
  end
end

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

приложение / контроллеры / agents_controller.rb

1
2
3
4
5
6
class AgentsController < ApplicationController
 
  def index
    @agents = Agent.all
  end
end

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

1
2
3
4
resources :agents
resources :missions do
  resources :agents, controller: ‘missions/agents’
end

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
Prefix Verb URI Pattern Controller#Action
              root GET / agents#index
            agents GET /agents(.:format) agents#index
                   POST /agents(.:format) agents#create
         new_agent GET /agents/new(.:format) agents#new
        edit_agent GET /agents/:id/edit(.:format) agents#edit
             agent GET /agents/:id(.:format) agents#show
                   PATCH /agents/:id(.:format) agents#update
                   PUT /agents/:id(.:format) agents#update
                   DELETE /agents/:id(.:format) agents#destroy
    mission_agents GET /missions/:mission_id/agents(.:format) missions/agents#index
                   POST /missions/:mission_id/agents(.:format) missions/agents#create
 new_mission_agent GET /missions/:mission_id/agents/new(.:format) missions/agents#new
edit_mission_agent GET /missions/:mission_id/agents/:id/edit(.:format) missions/agents#edit
     mission_agent GET /missions/:mission_id/agents/:id(.:format) missions/agents#show
                   PATCH /missions/:mission_id/agents/:id(.:format) missions/agents#update
                   PUT /missions/:mission_id/agents/:id(.:format) missions/agents#update
                   DELETE /missions/:mission_id/agents/:id(.:format) missions/agents#destroy

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

приложение / просмотров / миссии / агенты / index.html.erb

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<h2>Mission</h2>
<div><%= @mission.mission_name %></div>
<div><%= @mission.objective %></div>
<div><%= @mission.enemy %></div>
 
<h2>Agents</h2>
<ul>
  <% @agents.each do |agent|
    <li class=’agent’>
      <div>Name: <%= agent.name %></div>
      <div>Number: <%= agent.number %></div>
      <div>Licence to kill: <%= agent.licence_to_kill %></div>
    </li>
  <% end %>
</ul>

приложение / просмотров / агентов / index.html

01
02
03
04
05
06
07
08
09
10
<h2>Agents</h2>
<ul>
  <% @agents.each do |agent|
    <li class=’agent’>
      <div>Name: <%= agent.name %></div>
      <div>Number: <%= agent.number %></div>
      <div>Licence to kill: <%= agent.licence_to_kill %></div>
    </li>
  <% end %>
</ul>

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

приложение / просмотров / общий / _agents.html.erb

01
02
03
04
05
06
07
08
09
10
<h2>Agents</h2>
<ul>
  <% @agents.each do |agent|
    <li class=’agent’>
      <div>Name: <%= agent.name %></div>
      <div>Number: <%= agent.number %></div>
      <div>Licence to kill: <%= agent.licence_to_kill %></div>
    </li>
  <% end %>
</ul>

Ничего нового или удивительного здесь нет, но наши взгляды теперь более СУХИЕ.

приложение / просмотров / миссии / агенты / index.html.erb

1
2
3
4
5
6
<h2>Mission</h2>
<div><%= @mission.mission_name %></div>
<div><%= @mission.objective %></div>
<div><%= @mission.enemy %></div>
 
<%= render «shared/agents», collection: @agents %>

приложение / просмотров / агентов / index.html

1
<%= render «shared/agents», collection: @agents %>

Dope!

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

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

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