Во многих проектах, особенно крупных, часто встречается множество повторящихся частей. В идеале, следуя принципу DRY, нужно стараться минимизировать количество повторяющегося кода – проще поддерживать и тестировать одну копию кода, а не несколько.
Когда дело касается моделей или контроллеров, вопрос решается просто – выносим повторяющийся функционал в модули и подключаем его там где нужно. Однако, с шаблонами (views) все обстоит несколько сложнее – они чаще всего не являются ruby-кодом, поэтому не поддерживают расширение модулями. Конечно, есть библиотеки, реализующие разметку с помощью ruby-кода, такие как, например Markaby или Erector, но они используются довольно редко. Гораздо чаще используются ERB и HAML. Именно для таких языков разметки в Rails реализован механизм хелперов (helpers).
Хелперы – это модули, которые подключаются в обработчике шаблонов и автоматически становятся доступными внутри шаблона. Наиболее часто используемые хелперы вам хорошо знакомы – form_for, link_to, content_tag, stylesheet_link_tag, javascript_include_tag и так далее. По сути, каждый хелпер – это ruby-метод, который реализует часть логики генерации ответа. Хелпер может непосредственно генерировать часть ответа или же просто выполнять, например, сложную проверку, которая часто повторяется в нескольких шаблонах.
Например, в коде приложения может встретиться вот такой код, проверяющий наличие у пользователя прав на просмотр части страницы:
1 2 3 4 5 6 7 8 9 | <% if user = current_user and user.active? and user.roles.include?(:admin) %>
This is the secret message for admins
<% end %>
Some content
<% if user = current_user and user.active? and user.roles.include?(:admin) %>
More information for admins only
<% end %>
|
Если этот код часто повторяется, то его можно вынести в хелпер. Например, вот таким образом:
1 2 3 4 5 6 7 | module AuthHelper
def if_admin
if user = current_user and user.active? and user.roles.include?(:admin)
yield
end
end
end
|
И затем несколько переработать шаблон, используя хелпер:
1 2 3 4 5 6 7 8 9 | <% if_admin do %>
This is the secret message for admins
<% end %>
Some content
<% if_admin do %>
More information for admins only
<% end %>
|
Это довольно простой пример того, как можно избавиться от повторяющегося кода выносом его в хелперы.
Однако, в некоторых случаях не получится обойтись простым хелпером. Например, в том случае, когда генерируемый код достаточно сложен. Давайте рассмотрим вот такой пример: в приложении необходимо сгенерировать блок сообщения о результате действия. Блок должен содержать основное сообщение (текст), одну или несколько ссылок для выполнения дальнейших действий, дополнительную информацию (текст, таблицы, и т.д.). Кроме того, блок может быть просто отображен на странице или загружен через AJAX, и в обоих случаях должен происходить вызов события JavaScript-обработчика.
Если мы не используем хелперы, то наш код должен выглядеть примерно таким образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <div id="result_for_action" class="result">
<div class="message">
<%= message_text.html_safe %>
</div>
<div class="buttons">
<%= link_to("Some Action", some_action_path) %>
<%= link_to("Other Action", other_action_path) %>
</div>
<% if additional_info %>
<div class="additional" style="display:none;">
<%= additional_info.html_safe %>
</div>
<%= link_to_function('Additonal Info...',
'$("#result_for_action").find(".additional, a.show").toggle()',
:class => :show
) %>
<% end %>
</div>
<% javascript_tag do %>
<% if request.xhr? %>
$(document).trigger('result.ajax');
<% else %>
$(document).trigger('result.inline');
<% end %>
<% end %>
|
На первый взгляд, довольно тривиально, ничего особо страшного. Однако, давайте представим что такой код должен генерироваться в половине случаев ответа на запрос пользователя. Этот код будет из шаблона в шаблон повторяться, обрекая программиста на муки при любой попытке изменить общую логику формирования ответа или даже просто HTML-разметку. Например, при необходимости перенести
Если подобный код встречается в проекте повсеместно, пусть даже с небольшими вариациями, то вполне логично было бы вынести его в хелпер. Например, вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | module ResultHelper
def result_for(name, options = {})
message = content_tag(:div, options[:message], :class => :message)
buttons = content_tag(:div, options[:buttons].join(' '),
:class => :buttons
)
if options[:additional]
additional = content_tag(:div, options[:additional],
:class => :additional,
:style => "display:none"
)
additional << link_to_function('Additonal Info...',
"$('#result_for_#{ name }').find('.additional, a.show').toggle()",
:class => :show
)
else
additional = ''
end
javascript = javascript_tag(
"$(document).trigger('result.#{ request.xhr? ? :ajax : :inline }');"
)
content_tag(:div,
[message, buttons, additional, javascript].join(" ").html_safe,
:class => :result,
:id => "result_for_#{ name }"
)
end
end
|
А затем использовать вот таким образом:
1 2 3 4 5 6 7 8 | <%= result_for(:action,
:message => message_text,
:buttons => [
link_to("Some Action", some_action_path),
link_to("Other Action", other_action_path)
],
:additional => additional_info
) %>
|
Однако, этот подход накладывает определенные ограничения. Например, если генерация набора параметров будет содержать какую-то логику, то код станет плохо читаемым. Или, например, если для каждого из блоков потребуется передать дополнительный CSS-класс. Вот во что может выродиться использование такого хелпера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <%= result_for(:action,
:message => message_text,
:message_class => :success,
:buttons => if current_user
[
link_to("Some Action", some_action_path),
link_to("Other Action", other_action_path)
]
else
[
link_to("Login", login_path)
]
end,
:buttons_class => (:unauthorize unless current_user),
:additional => additional_info + (some_text || ""),
:additional_link_text => "Additional Info and Instructions..."
) %>
|
На мой взгляд, более логично было бы исспользовать подход, близкий к тому, который используется для формирования разметки форм. А именно – создание специального класса, builder-а, который будет предоставлять дополнительный DSL для формирования параметров, которые будет использовать хелпер. Этот класс может предоставлять методы для задания параметров конкретных блоков и их содержания, а так же отвечать непосредственно за формирование ответа.
В нашем случае у блока с результатом действия есть 3 подблока, каждый из которых имеет собственные параметры:
- Сообщение – принимает CSS-класс, причем чаще всего это классы
success,failureиnotice - Кнопки – принимает CSS-класс
- Дополнительная информация – принимает CSS-класс и текст для ссылки, включающей отображение блока
Для упрощения работы мы должны создать хелпер, который будет формировать экземпляр класса, передавать ему входные параметры и вставлять результат работы класса в тело ответа:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | module ResultHelper
def result_for(action_name, &block)
builder = Builder.new(self, action_name)
concat(builder.html(&block))
end
class Builder
def initialize(template, action_name)
@template = template
@action_name = action_name
end
def html(&block)
"This is HTML code"
end
end
end
|
Далее, нам необходимо добавить методы для каждого из блоков, которые будут принимать параметры и разметку для содержимого блока:
1 2 3 4 5 6 7 8 9 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 | module ResultHelper
def result_for(action_name, &block)
concat(
Builder.new(self, action_name).html(&block)
)
end
class Builder
attr_reader :template
delegate :capture, :to => :template
def initialize(template, action_name)
@template = template
@action_name = action_name
end
def html(&block)
"This is HTML code"
end
def message(css_class = nil, &block)
@message = capture(&block)
@message_class = css_class
end
def buttons(css_class = nil, &block)
@buttons = capture(&block)
@buttons_class = css_class
end
def additional(css_class = nil, link_text = "Additional Info...", &block)
@additional = capture(&block)
@additional_class = css_class
@additional_link_Text = link_text
end
end
end
|
И наконец, мы должны добавить логику формирования ответа, учитывающую переданные данные:
1 2 3 4 5 6 7 8 9 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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | module ResultHelper
def result_for(action_name, &block)
concat(
Builder.new(self, action_name).html(&block)
)
end
class Builder
attr_reader :template
delegate :capture, :content_tag, :link_to, :javascript_tag, :request, :to => :template
def initialize(template, action_name)
@template = template
@action_name = action_name
end
def html(&block)
yield(self)
result = [
content_tag(:div, @message, :class => "message #{@message_class}"),
content_tag(:div, @buttons, :class => "buttons #{@buttons_class}")
]
if @additional
result << content_tag(:div, @additional,
:class => "additional #{@additional_class}",
:style => "display: none"
)
result << link_to(@additional_link_text,
"$('#{block_id}').find('.additional, a.show').toggle()",
:class => :show
)
end
result << javascript_tag(
"$(document).trigger('result.#{ request.xhr? ? :ajax : :inline }');"
)
content_tag(:div, result.join(" ").html_safe,
:class => "result",
:id => block_id
)
end
def block_id
"result_for_#{@action_name}"
end
def message(css_class = nil, &block)
@message = capture(&block)
@message_class = css_class
end
def success(&block)
message(:success, &block)
end
def failure(&block)
message(:failure, &block)
end
def notice(&block)
message(:notice, &block)
end
def buttons(css_class = nil, &block)
@buttons = capture(&block)
@buttons_class = css_class
end
def additional(css_class = nil, link_text = "Additional Info...", &block)
@additional = capture(&block)
@additional_class = css_class
@additional_link_text = link_text
end
end
end
|
Стоит обратить внимание на несколько моментов в предложенном коде. Во-первых, это делегирование методов, относящихся к работе с шаблоном, непосредственно объекту template, который и отвечает за обработку шаблона. Этот прием позволяет сделать код более читаемым. Во-вторых, это методы success, failure и notice – они упрощают вывод сообщений с часто используемыми параметрами.
И наконец, давайте посмотрим каким образом мы можем использовать новый хелпер:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <% result_for :action do |r| %>
<% r.success do %>
<%= message_text %>
<% end %>
<% r.buttons do %>
<% if current_user %>
<%= link_to("Some Action", some_action_path) %>
<%= link_to("Other Action", other_action_path) %>
<% else %>
<%= link_to("Login", login_path) %>
<% end %>
<% end %>
<% r.additional(current_user ? nil : :unauthorize, "Additional Info and Instructions...") do %>
additional
<% end %>
<% end %>
|
На мой взгляд, все стало гораздо лаконичнее, понятнее, код гораздо легче сканировать визуально. В качестве дополнительного бонуса можно назвать тот факт, что порядок вывода информации никак не зависит от порядка вызова методов класса.
Итог
Для улучшения читаемости шаблонов и устранения дублирующегося кода можно (и зачастую даже нужно) использовать хелперы. А для создания хелперов со сложной логикой, несколькими блоками разметки и/или большим количеством передаваемых параметров можно создавать специальные классы, которые будут принимать входные параметры и формировать ответ.