#286 Draper
- Download:
- source codeProject Files in Zip (125 KB)
- mp4Full Size H.264 Video (27.2 MB)
- m4vSmaller H.264 Video (14.6 MB)
- webmFull Size VP8 Video (16.7 MB)
- ogvFull Size Theora Video (32.1 MB)
В этом эпизоде мы рассмотрим Draper, gem, который позволяет добавлять декораторы к вьюхам Rails-приложения, очень похоже на паттерн presenter. Если у вас много сложной логики отображения в ваших шаблонах и хелперах, то Draper может помочь очистить этот код с использованием более объектно-ориентированного подхода. В этом эпизоде мы покажем, как это работает.
Приложение, с которым будем работать показано ниже. В нем есть страница профиля пользователя, которая показывает различные части информации о данном пользователе включая аватар, полное имя, имя пользователя, короткую биографию с использованием разметки и ссылки на вебсайт и ленту Twitter. Если пользователь задал вебсайт, то аватар и полное имя будут ссылаться на этот сайт.
Страница кажется достаточно простой, но мы также должны учитывать пользователей, которые не ввели так много данных как “MrMystery”.
Этот пользователь ввел только имя пользователя, поэтому выводим его вместо его полного имени, показываем аватар по умолчанию и некоторую замену для текста в остальных полях. Это делает шаблон для этой страницы более сложным, с необходимостью множества выражений if для обработки пользователей с разным количеством информации. Мы могли бы сделать этот шаблон значительно чище, если бы могли переместить часть этой логики куда-нибудь еще.
<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
в этом шаблоне для отображения аватара. Давайте взглянем на него.
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
.
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
. Этот экшен сейчас извлекает пользователя как обычно.
class UsersController < ApplicationController def index @users = User.all end def show @user = User.find(params[:id]) end end
Мы должны обернуть этого пользователя в наш декоратор, для чего мы заменяем User.find
на UserDecorator.find
.
def show @user = UserDecorator.find(params[:id]) end
Этот код теперь вернет объект UserDecorator
, который оборачивает объект User
и по умолчанию делегирует все методы ему (об этом – позже). Экшен будет работать точно так же, как и до этого, даже когда мы работаем с UserDecorator
вместо User
. Теперь мы можем начать чистить наши вьюхи и начнем с кода, который отображает аватар пользователя.
<%= link_to_if @user.url.present?, image_tag("avatars/#{avatar_name(@user)}", class: "avatar"), @user.url %>
Мы заменим это во вьюхе следующим кодом:
<%= @user.avatar %>
Этот код обращается к методу avatar в UserDecorator
, который мы сейчас напишем. Есть несколько вещей, которые стоит учитывать во время написания этого метода. Всякий раз, когда вызывается метод хелпера из декоратора, такой как наш link_to_if
, мы должны вызывать его через метод h
(что означает “helpers”). Когда мы хотим сослаться на модель, мы вызываем вместо нее метод model
, в данном случае вместо @user
.
Код, который мы скопировали из вьюхи в avatar
, вызывает метод хелпера avatar_name
. Так как мы вызываем avatar_name
из нашего декоратора, мы переместим его туда из класса UsersHelper
. Теперь, имея метод в том же классе, нам не нужно передавать ему объект User
, и мы можем заменить все обращения к пользователю на model.
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
Далее мы приведем в порядок код, который отображает имя пользователя. Мы заменим этот код во вьюхе:
<h1><%= link_to_if @user.url.present?, (@user.full_name.present? ? @user.full_name : @user.username), @user.url %></h1>
на это:
<h1><%= @user.linked_name %></h1>
Нам потребуется написать метод linked_name
в UserDecorator
. Есть сходства между кодом, который мы взяли из шаблона и методом avatar
, который мы написали ранее. Они оба отображают ссылку, содержимое которой зависит от наличия url
пользователя. Так как мы используем класс, то достаточно просто устранить это дублирование.
Чтобы обеспечить создание ссылки, мы создадим новый private метод site_link
, который принимает содержимое как параметр. Мы можем использовать этот метод потом как в avatar
, так и в linked_name
методах, чтобы привести их в порядок. Как раньше мы заменили любые обращения к @user
в linked_name
на model
. Сделав всё это, наш декоратор теперь выглядит так:
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
Если мы перезагрузим страницу профиля пользователя, то она должна выглядеть точно так же, как и до изменений.
Наш шаблон уже выглядит намного чище, но есть еще много чего мы можем сделать. Далее мы рефакторим большой кусок кода вьюхи - код, который выводит ссылку на вебсайт пользователя.
<dt>Website:</dt> <dd> <% if @user.url.present? %> <%= link_to @user.url, @user.url %> <% else %> <span class="none">None given</span> <% end %> </dd>
Мы заменим на это:
<dt>Website:</dt> <dd><%= @user.website %></dd>
Так же как и до этого, мы создадим метод в классе декоратора. Мы можем видеть из кода, который мы убрали из вьюхи, что если у пользователя нет url
, то выводится некоторый HTML. Можно просто возвратить это как строку, но мы не хотим размещать голый HTML в строке Ruby. Другим решением могло бы быть переместить код в partial и вывести его, но так как нам нужно только лишь вывести единственный элемент HTML, больше смысла будет использовать метод хелпера content_tag
.
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 и биографию пользователя. Здесь не будут показаны детали, но в результате изменений код нашей вьюхи выглядит значительно чище.
<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
выглядят следующим образом:
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
.
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
, который будет выводить любой текст, который мы передадим в него.
class ApplicationDecorator < Draper::Base def markdown(text) Redcarpet.new(text, :hard_wrap, :filter_html, :autolink).to_html.html_safe end end
Теперь в UserDecorator
мы можем изменить метод bio
так, чтобы он вызывал markdown
.
def bio handle_none model.bio do markdown(model.bio) end end
Изменение модели
Теперь у нас есть декоратор, поэтому будет правильно просмотреть модель на предмет кода, относящегося ко вьюхам, который мы можем переместить в соответствующий декоратор. Например, в нашей модели User
есть метод member_since
, который форматирует времяcreated_at
пользователя. Этот код может считаться относящимся ко вьюхам, так как все что он делает - возвращает форматированную строку, поэтому мы переместим его в декоратор.
class User < ActiveRecord::Base def member_since created_at.strftime("%B %e, %Y") end end
Все что нам нужно сделать - переместить метод в декоратор и приписать model
перед created_at
.
def member_since model.created_at.strftime("%B %e, %Y") end
Ограничение доступа к модели с помощью метода allows
Пока мы изменяем UserDecorator
, есть еще одна возможность Draper, которую мы продемонстрируем: метод allows
. В его нынешнем виде UserDecorator
делегирует все свои методы объекту User
, но мы можем выбирать, какие методы передаются модели User
, используя метод allows
и передавая ему имя методов, которые мы хотим делегировать.
class UserDecorator < ApplicationDecorator decorates :user allows :username # Other methods omitted end
Мы разрешим для делегирования только username
, и, таким образом, только метод username
будет передан в модель User. Это единственный метод, который нам надо делегировать, так как это - единственный метод, вызываемый во вьюхе, который не исходит из декоратора. Это дает нам больше контроля над интерфейсом декоратора.
Теперь мы закончили с рефакторингом с помощью декоратора и попробуем загрузить страницу профиля пользователя снова, чтобы убедиться, что до сих пор всё выглядит так же.
Так и есть. Мы даже можем проверить другого пользователя и он выглядит так же, однако наш код значительно чище.
Используя декоратор, наш шаблон show был уменьшен с 1050 байтов в 34 строк до 382 байтов в 16 строк, уменьшение в размере - практически на две трети. Так всё выглядит значительно чище и мы упростили себе задачу редактирования, если бы понадобилось изменять вёрстку страницы.