+5

Presenter in Rails

http://nithinbekal.com/posts/rails-presenters/

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 agothay 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


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí