homeASCIIcasts

286: Draper 

(view original Railscast)

Other translations: En Ja

Other formats:

Written by Alexander Ulitin

В этом эпизоде мы рассмотрим Draper, gem, который позволяет добавлять декораторы к вьюхам Rails-приложения, очень похоже на паттерн presenter. Если у вас много сложной логики отображения в ваших шаблонах и хелперах, то Draper может помочь очистить этот код с использованием более объектно-ориентированного подхода. В этом эпизоде мы покажем, как это работает.

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

Страница профиля для пользователя, который ввел все свои данные.

Страница кажется достаточно простой, но мы также должны учитывать пользователей, которые не ввели так много данных как “MrMystery”.

Страница профиля для пользователя, который ввел лишь часть данных.

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

/app/views/users/show.html.erb

<div id="profile">
  <%= link_to_if @user.url.present?, ↵
  image_tag("avatars/#{avatar_name(@user)}", class: "avatar"), ↵
  @user.url %>
  <h1><%= link_to_if @user.url.present?, ↵
    (@user.full_name.present? ? @user.full_name : ↵
    @user.username), @user.url %></h1>
  <dl>
    <dt>Username:</dt>
    <dd><%= @user.username %></dd>
    <dt>Member Since:</dt>
    <dd><%= @user.member_since %></dd>
    <dt>Website:</dt>
    <dd>
    <% if @user.url.present? %>
      <%= link_to @user.url, @user.url %>
    <% else %>
      <span class="none">None given</span>
    <% end %>
    </dd>
    <dt>Twitter:</dt>
    <dd>
    <% if @user.twitter_name.present? %>
      <%= link_to @user.twitter_name, ↵
  "http://twitter.com/#{@user.twitter_name}" %>
    <% else %>
      <span class="none">None given</span>
    <% end %>
    </dd>
    <dt>Bio:</dt>
    <dd>
    <% if @user.bio.present? %>
      <%=raw Redcarpet.new(@user.bio, :hard_wrap, :filter_html, ↵
        :autolink).to_html %>
    <% else %>
      <span class="none">None given</span>
    <% end %>
    </dd>
  </dl>
</div>

Так как эта логика относится к вьюхам, мы не можем выделить ее в модель. Одним из решений было бы использования методов хелперов. Мы уже используем один, обозначаемый image_tag в этом шаблоне для отображения аватара. Давайте взглянем на него.

/app/helpers/users_helper.rb

module UsersHelper
  def avatar_name(user)
    if user.avatar_image_name.present?
      user.avatar_image_name
    else
      "default.png"
    end
  end
end

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

Установка Draper

Этот сценарий - отличная возможность для использования presenter или декоратора. Так как Draper относится к ним, давайте добавим его в наше приложение. Gem Draper устанавливается как обычно, добавлением его в Gemfile с последующим запуском bundle.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.1.0'
gem 'sqlite3'


# Gems used only for assets and not required
# in production environments by default.
group :assets do
  gem 'sass-rails', "  ~> 3.1.0"
  gem 'coffee-rails', "~> 3.1.0"
  gem 'uglifier'
end

gem 'jquery-rails'
gem 'redcarpet'

gem 'draper'

Когда Draper установлен, мы создадим декоратор для нашей модели User, запустив генератор draper:decorator.

$ rails g draper:decorator user
      create  app/decorators
      create  app/decorators/application_decorator.rb
      create  app/decorators/user_decorator.rb

Раз это наш первый декоратор, то также будет сгенерирован application_decorator. Любые декораторы, которые мы генерируем, наследуются от ApplicationDecorator, поэтому мы можем разместить там любую функциональность, которую хотим разделить между декораторами.

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

Приведение в порядок страницы профиля

Для использования Draper на нашей странице профиля для начала нужно сделать изменения в экшене show контроллера UsersController. Этот экшен сейчас извлекает пользователя как обычно.

/app/controllers/users_controller.rb

class UsersController < ApplicationController
  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
  end
end

Мы должны обернуть этого пользователя в наш декоратор, для чего мы заменяем User.find на UserDecorator.find.

/app/controllers/users_controller.rb

def show
  @user = UserDecorator.find(params[:id])
end

Этот код теперь вернет объект UserDecorator, который оборачивает объект User и по умолчанию делегирует все методы ему (об этом – позже). Экшен будет работать точно так же, как и до этого, даже когда мы работаем с UserDecorator вместо User. Теперь мы можем начать чистить наши вьюхи и начнем с кода, который отображает аватар пользователя.

/app/views/users/show.html.erb

<%= link_to_if @user.url.present?, image_tag( ↵
  "avatars/#{avatar_name(@user)}", class: "avatar"), @user.url %>

Мы заменим это во вьюхе следующим кодом:

/app/views/users/show.html.erb

<%= @user.avatar %>

Этот код обращается к методу avatar в UserDecorator, который мы сейчас напишем. Есть несколько вещей, которые стоит учитывать во время написания этого метода. Всякий раз, когда вызывается метод хелпера из декоратора, такой как наш link_to_if, мы должны вызывать его через метод h (что означает “helpers”). Когда мы хотим сослаться на модель, мы вызываем вместо нее метод model, в данном случае вместо @user.

Код, который мы скопировали из вьюхи в avatar, вызывает метод хелпера avatar_name. Так как мы вызываем avatar_name из нашего декоратора, мы переместим его туда из класса UsersHelper. Теперь, имея метод в том же классе, нам не нужно передавать ему объект User, и мы можем заменить все обращения к пользователю на model.

/app/decorators/user_decorator.rb

class UserDecorator < ApplicationDecorator
  decorates :user

  def avatar
    h.link_to_if model.url.present?, h.image_tag("avatars/#{avatar_name}", class: "avatar"), model.url
  end

  private
  def avatar_name
    if model.avatar_image_name.present?
      model.avatar_image_name
    else
      "default.png"
    end
  end
end

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

/app/views/users/show.html.erb

<h1><%= link_to_if @user.url.present?, (@user.full_name.present? ? @user.full_name : @user.username), @user.url %></h1>

на это:

/app/views/users/show.html.erb

<h1><%= @user.linked_name %></h1>

Нам потребуется написать метод linked_name в UserDecorator. Есть сходства между кодом, который мы взяли из шаблона и методом avatar, который мы написали ранее. Они оба отображают ссылку, содержимое которой зависит от наличия url пользователя. Так как мы используем класс, то достаточно просто устранить это дублирование.

Чтобы обеспечить создание ссылки, мы создадим новый private метод site_link, который принимает содержимое как параметр. Мы можем использовать этот метод потом как в avatar, так и в linked_name методах, чтобы привести их в порядок. Как раньше мы заменили любые обращения к @user в linked_name на model. Сделав всё это, наш декоратор теперь выглядит так:

app/decorators/user_decorator.rb

class UserDecorator < ApplicationDecorator
  decorates :user

  def avatar
    site_link h.image_tag("avatars/#{avatar_name}", ↵
      class: "avatar")
  end

  def linked_name
    site_link(model.full_name.present? ? model.full_name : ↵
      model.username)
  end

  private
  def site_link(content)
    h.link_to_if model.url.present?, content, model.url
  end

  def avatar_name
    if model.avatar_image_name.present?
      model.avatar_image_name
    else
      "default.png"
    end
  end
end

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

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

/app/views/user/show.html.erb

<dt>Website:</dt>
<dd>
  <% if @user.url.present? %>
    <%= link_to @user.url, @user.url %>
  <% else %>
    <span class="none">None given</span>
  <% end %>
</dd>

Мы заменим на это:

/app/views/user/show.html.erb

<dt>Website:</dt>
<dd><%= @user.website %></dd>

Так же как и до этого, мы создадим метод в классе декоратора. Мы можем видеть из кода, который мы убрали из вьюхи, что если у пользователя нет url, то выводится некоторый HTML. Можно просто возвратить это как строку, но мы не хотим размещать голый HTML в строке Ruby. Другим решением могло бы быть переместить код в partial и вывести его, но так как нам нужно только лишь вывести единственный элемент HTML, больше смысла будет использовать метод хелпера content_tag.

/app/decorators/user_decorator.rb

def website
  if model.url.present?
    h.link_to model.url, model.url
  else
    h.content_tag :span, "None given", class: "none"
  end
end

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

/app/views/users/show.html.erb

<div id="profile">
  <%= @user.avatar %>
  <h1><%= @user.linked_name %></h1>
  <dl>
    <dt>Username:</dt>
    <dd><%= @user.username %></dd>
    <dt>Member Since:</dt>
    <dd><%= @user.member_since %></dd>
    <dt>Website:</dt>
    <dd><%= @user.website %></dd>
    <dt>Twitter:</dt>
    <dd><%= @user.twitter %></dd>
    <dt>Bio:</dt>
    <dd><%= @user.bio %></dd>
  </dl>
</div>

Новые методы в декораторе – twitter и bio выглядят следующим образом:

/app/decorators/user_decorator.rb

def website
  if model.url.present?
    h.link_to model.url, model.url
  else
    h.content_tag :span, "None given", class: "none"
  end
end

def twitter
  if model.twitter_name.present?
    h.link_to model.twitter_name, ↵
      "http://twitter.com/#{model.twitter_name}"
  else
    h.content_tag :span, "None given", class: "none"
  end
end

def bio
  if model.bio.present?
    Redcarpet.new(model.bio, :hard_wrap, :filter_html, ↵
 		:autolink).to_html.html_safe
  else
    h.content_tag :span, "None given", class: "none"
  end
end

Два новых метода очень похожи друг на друга и на метод website, написанный ранее. Есть небольшое дублирование между тремя методами, особенно в каждом выражении else, поэтому было бы правильно выделить эту часть в отдельный метод.

Можно для этого использовать блок. Мы выделим код из else в отдельный метод, назовем его handle_none. Передадим значение, наличие которого мы хотим проверить в этот метод, а так же блок. Если значение существует, код в блоке будет выполнен, а иначе будет выведен тег span. Тогда мы можем использовать этот handle_none, чтобы очистить методы website, twitter и bio.

/app/decorators/user_decorator.rb

def website
  handle_none model.url do
    h.link_to model.url, model.url
  end
end

def twitter
  handle_none model.twitter_name do
    h.link_to model.twitter_name, ↵
      "http://twitter.com/#{model.twitter_name}"
  end
end

def bio
  handle_none model.bio do
    Redcarpet.new(model.bio, :hard_wrap, :filter_html, ↵
      :autolink).to_html.html_safe
  end
end

private
def handle_none(value)
  if value.present?
    yield
  else
    h.content_tag :span, "None given", class: "none"
  end
end

Еще одно изменение, которое мы можем сделать - выделить вывод разметки в ApplicationDecorator так, чтобы можно было бы вызывать его из любого другого декоратора, который мы могли бы сделать. Мы можем создать новый метод markdown, который будет выводить любой текст, который мы передадим в него.

/app/decorators/application_decorator.rb

class ApplicationDecorator < Draper::Base
  def markdown(text)
    Redcarpet.new(text, :hard_wrap, :filter_html, ↵
      :autolink).to_html.html_safe
  end
end

Теперь в UserDecorator мы можем изменить метод bio так, чтобы он вызывал markdown.

/app/decorators/user_decorator.rb

def bio
  handle_none model.bio do
    markdown(model.bio)
  end
end

Изменение модели

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

/app/models/user.rb

class User < ActiveRecord::Base
  def member_since
    created_at.strftime("%B %e, %Y")
  end
end

Все что нам нужно сделать - переместить метод в декоратор и приписать model перед created_at.

/app/decorators/user_decorator.rb

def member_since
  model.created_at.strftime("%B %e, %Y")
end

Ограничение доступа к модели с помощью метода allows

Пока мы изменяем UserDecorator, есть еще одна возможность Draper, которую мы продемонстрируем: метод allows. В его нынешнем виде UserDecorator делегирует все свои методы объекту User, но мы можем выбирать, какие методы передаются модели User, используя метод allows и передавая ему имя методов, которые мы хотим делегировать.

/app/decorators/user_decorator.rb

class UserDecorator < ApplicationDecorator
  decorates :user
  allows :username

  # Other methods omitted
end

Мы разрешим для делегирования только username, и, таким образом, только метод username будет передан в модель User. Это единственный метод, который нам надо делегировать, так как это - единственный метод, вызываемый во вьюхе, который не исходит из декоратора. Это дает нам больше контроля над интерфейсом декоратора.

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

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

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

Страница профиля для MrMystery также не изменилась.

Используя декоратор, наш шаблон show был уменьшен с 1050 байтов в 34 строк до 382 байтов в 16 строк, уменьшение в размере - практически на две трети. Так всё выглядит значительно чище и мы упростили себе задачу редактирования, если бы понадобилось изменять вёрстку страницы.