Презентеры в Rails

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

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

Рефакторинг вьюх

У нас есть страница, где мы выводим заголовок и статус публикации поста. Заголовок является полем в нашей таблице, а статус методом в модели Post.

%h1= @post.title
%p= @post.publication_status

publication_status выводит дату публикации, если пост уже опубликован. Иначе мы видим слово ‘Draft’.

class Post < ActiveRecord::Base
def publication_status
published_at || 'Draft'
end
end

В принципе, не так плохо. Но если нам нужно изменить формат даты, к примеру, на ‘Х часов назад’? Мы бы рады использовать хелпер-метод time_ago_in_words, но он не доступен в моделях. Простым решением является перенос логики в хелпер модуль.

module PostHelper
def publication_status(post)
if post.published_at
time_ago_in_words(post.published_at)
else
'Draft'
end
end
end

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

Создание первого презентера

Мы может создать класс PostPresenter , который включает логику, которая есть в хелпере.

class PostPresenter < Struct.new(:post, :view)
def publication_status
if post.published_at?
view.time_ago_in_words(post.published_at)
else
'Draft'
end
end
end
presenter = PostPresenter.new(@post, view_context)
presenter.publications_status #=> 'Draft'

view_context представляет инстанс ActionView, который мы можем использовать везде. Теперь нам нужно сделать вызов в контексте вьюхи из контроллера.

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

class BasePresenter < SimpleDelegator
def initialize(model, view)
@model, @view = model, view
super(@model)
end
  def h
@view
end
end

Наследование от встроенного в руби класса SimpleDelegator и вызов superпри инициализации помогает удостовериться, что если мы вызываем любой метод, которого нет в презентере, он вызывается на самом объекте post.

Я также включил метод #h который возвращает контекст вьюхи. После наследования от BasePresenter, наш первоначальный презентер выглядит так:

class PostPresenter < BasePresenter
def publication_status
if @model.published_at?
h.time_ago_in_words(@model.published_at)
else
'Draft'
end
end
end

Мы можем инициализировать презентер объектов в контроллере:

class PostsController < ApplicationController
def show
post = Post.find(params[:id])
@post = PostPresenter.new(post, view_context)
end
end

Удаление презентеров из контроллеров

Чтобы сделать вещи проще, мы должны избегать инициализацию презентера в контроллере и вместо этого добавить хелпер-метод, который позволит нам обернуть ActiveRecord объект внутри презентера.

module ApplicationHelper
def present(model)
klass = "#{model.class}Presenter".constantize
presenter = klass.new(model, self)
yield(presenter) if block_given?
end
end
- present(@post) do |post|
%h2= post.title
%p= post.author

Общие презентеры

Код ниже предполагает, что все презентер классы следуют правилу именования, т.е. ‘Presenter’ + имя модели. Порой я разделяю презентеры на более мелкие части и размещаю их в разных местах. К примеру, у меня есть AdminPostPresenter , который содержив хелпер-методы для отображения постов в админ-панели.
Для этого я поправил метод present для включения второго аргумента.

module ApplicationHelper
def present(model, presenter_class=nil)
klass = presenter_class || "#{model.class}Presenter".constantize
presenter = klass.new(model, self)
yield(presenter) if block_given?
end
end

Это позволяет вызвать present(@post, AdminPostPresenter) во вьюхе где я использую специальные презентеры для админки, в то время когда present(@post) я могу использовать где угодно.

Вывод

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

Дальнейшее чтение

Оригинал статьи

Like what you read? Give Vitaliy a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.