Presenter in Rails
Bài đăng này đã không được cập nhật trong 8 năm
Khi model của bạn bị phình to với rất nhiều methods mà nó chỉ được sử dụng duy nhất trong views, đây có lẽ là khoảng thời gian tốt nhất để refactor chúng. Di chuyển logic đó vào trong helper modules có thể là OK trong một số trường hợp, nhưng sự phát triển trong views của bạn khá phức tạp, có lẽ bạn cần xem xét đến presenter
.
Presenter
cung cấp cho bạn một cách hướng đối tượng (object oriented) để tiếp cận với views helpers. Trong bài viết này tôi sẽ theo hướng làm sao để refactored views của chúng ta bằng cách sử dụng presenter
thay vì các phương thức trong helper hoặc thậm chí là các methods trong model.
Ryan Bates' một chuyên gia tuyệt vời Railscast có một bài viết về presenters from scratch nó là bài hướng dẫn đã dẫn dắt tôi đến refactor này. Nó đi sâu hơn về những gì tôi tôi sẽ viết ở đây, vì thế hãy xem nó sau khi bạn đọc bài viết này.
Refactoring the views
Chúng ta có một HAML
view nơi chúng ta sẽ hiện thị title, và trạng thái public của bài viết (publication status). Title là một thuộc tính ActiveRecord
và trạng thái của bài viết là một method trong Post
model.
%h1= @post.title
%p= @post.publication_status
publication_status
sẽ hiện thị ngày public bài viết nếu nó đã được published. Nếu không sẽ được hiện thị ra một string là Draft
class Post < ActiveRecord::Base
def publication_status
published_at || 'Draft'
end
end
Nó không thực sự là quá tệ. Nhưng điều gì nếu chúng ta muốn show publication date theo dạng X hours ago
thay thế format bình thường? Chúng ta nên sử dụng time_ago_in_words
một helper method của Rails, nhưng nó không available trong model. Một cách đơn giản nhất để tiếp cận đến nó là chúng ta sẽ chuyển method đó sang helper module.
module PostHelper
def publication_status post
if post.published_at
time_ago_in_words post.published_at
else
'Draft'
end
end
end
Nó đã giải quyết được vấn đề của chúng ta, nhưng view helpers có những bất lợi của việc đưa tất cả các methods helper vào bên trong một single namespace
.
Nó cần có một class để chứa tất cả helper methods liên quan.
Creating our first presenter
Chúng ta có thể tạo một class với tên PostPresenter
để implement logic mà chúng ta đã viết trong helper.
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
ở đây chính là một đại diện cho một instance của ActionView
, đó cũng là nơi mà chúng ta có thể dùng được view helpers như hàm time_ago_in_words
.Chính vì PostPresenter
không chứa các helper methods, nên chúng ta cần pass view_context
vào class như một tham số.
ví dụ về time_ago_in_words
ActionView::Base.new.time_ago_in_words Time.now
=> "less than a minute"
Bây giờ chúng ta sẽ giải quyết vấn đề với #title
bởi vì nó cũng cần thiết đối với view. Chúng ta có thể tránh điều này bằng cách gọi trực tiếp method trong post object nếu chúng ta không defined nó trong presenter. Bây giờ chúng ta hãy tạo một base class BasePresenter
nó sẽ được kế thừa bởi tất cả các class presenter của chúng ta.
class BasePresenter < SimpleDelegator
attr_reader :model, :view
def initialize model, view
@model, @view = model, view
super @model
end
end
Do kế thừa từ một class dựng sẵn của Ruby là SimpleDelegator
và đã gọi super
trong phương thức khởi tạo, nên chúng ta đảm bảo rằng nếu chúng ta gọi bất kì method nào mà nó không được định nghĩa trong presenter
thì nó sẽ được gọi ra trong post object.
Sau khi được kế thừa từ BasePresenter
thì presenter của chúng ta sẽ như sau:
class PostPresenter < BasePresenter
def publication_status
if model.published_at?
view.time_ago_in_words model.published_at
else
'Draft'
end
end
end
Chúng ta có thể khởi tạo presenter ngay trong controller của chúng ta.
class PostsController < ApplicationController
def show
post = Post.find(params[:id])
@post = PostPresenter.new(post, view_context)
end
end
Di chuyển presenter ra khỏi controllers
Để đơn giản hóa hơn nữa, chúng ta nên tránh sử dụng presenter object trong controller và thay vào đó chúng ta sẽ thêm vào một helper method, nó sẽ cho phép bạn wrap các đối tượng ActiveRecord
vào trong presenter và đích đến là view.
module ApplicationHelper
def present(model)
klass = "#{model.class}Presenter".constantize
presenter = klass.new(model, self)
yield(presenter) if block_given?
end
end
Ở đây chúng ta đã pass presenter object vào một block, block đó chúng ta cũng sẽ pass vào view, do đó chúng ta có thể viết code như sau:
- present(@post) do |post|
%h2= post.title
%p= post.author
Custom presenters
Những mã code trên cho thấy tất cả các class presenter của chúng ta đều follow theo một convention là thêm Presenter
vào sau tên của model name. Đôi khi tôi muốn chia nhỏ presenter ra thành các class nhỏ hơn để sử dụng được những nơi khác nhau. Ví dụ: có một AdminPostPresenter
nó chứa các method để hiện thị post trên màn hình admin.
Trong trường hợp này, chúng ta sẽ làm cho presenter
chấp nhận một tham số thứ 2 cho phép truyền vào class name:
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
Nó cho phép chúng ta gọi function present(@post, AdminPostPresenter)
ở trong view nơi mà tôi sử dụng một admin presenter đặc biệt, trong khi đó chúng ta vẫn có thể tiếp tục sử dụng presenter(@post)
ở những nơi khác.
Kết luận
Sử dụng presenter là một cách tốt cho chúng ta maintaining views. Họ cũng có thêm nhiều tiện ích dễ dàng cho việc test. Nếu bạn maitaining Rails views của bạn và gặp vấn đề, thì presenter là một cách tốt để clean mọi thứ.
Tài liệu
- Ryan Bates’ Presenters from Scratch Railscast
- Draper gem
- Jay Fields’ article about presenters
- Pull-req của Justin Gordon có nhiều thảo luận hữu ích về presenter.
All rights reserved