Rails中的Presenter

什么是Presenter

当你使用的models里面有很多view相关的逻辑的时候, 会觉得这块逻辑写在这个地方不太好。 很多人可能会把这些代码移到helper方法里面去,确实是可以的。但是当你view的逻辑复杂后, 可能你需要看看Presenter了。

Presenter是面向对象的方式实现view hepler。我就介绍下如何使用presenter重构helper 和model里面的一些方法。

重构views

例如我们需要显示一个博客文章的状态, 如果文章已经发表的话就显示发表的时间,否则显示为"Draft",

1
2
3
4
5
class Post < ActiveRecord::Base
  def publication_status
    published_at || 'Draft'
  end
end

这时候看起来没有什么问题,如果需求改成希望把时间显示成xxx hours ago的话,这时候如果使用 helper里面的time_ago_in_words方法再方便不过了。但model里面不支持这个方法,所以我们移到 helper里面去。

1
2
3
4
5
6
7
8
9
module PostHelper
  def publication_status(post)
    if post.published_at
      time_ago_in_words(post.published_at)
    else
      'Draft'
    end
  end
end

这个方法确实解决了这个问题,但是helper有个缺点,把所有的helper方法都放到一个namespace下面了。 如果post有一个这样的方法,那该多方便。

创建第一个Presenter

我们来创建一个PostPresenter(app/presenter目录下面)类来实现上面的需求。

1
2
3
4
5
6
7
8
9
10
11
12
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

view_context是ActionView的一个实例,这样的话我们才能使用time_ago_in_words方法。 但是当我们想获取post的title的时候,这个persenter将无法获得。所以我们需要创建一个 BasePresenter来解决这个问题,所有的Presenter将会继承这个类。

1
2
3
4
5
6
7
8
9
10
class BasePresenter < SimpleDelegator
  def initialize(model, view)
    @model, @view = model, view
    super(@model)
  end

  def h
    @view
  end
end

通过继承SimpleDelegator类并且调用了super(@model)初始化方法,这个保证了如果我们调用的 方法在presenter里面不存在的话,就会调用model对应的方法。

并且定义了一个h方法来代替view。上面的ProjectPresenter将会重构成这样:

1
2
3
4
5
6
7
8
9
class PostPresenter < BasePresenter
  def publication_status
    if @model.published_at?
      h.time_ago_in_words(@model.published_at)
    else
      'Draft'
    end
  end
end

我们可以在controller里面初始化:

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

将presenter移出controller

上面的presenter在controller里面每次初始化感觉比较麻烦,现在我们通过定义一个helper方法, 来简化这个初始化。

1
2
3
4
5
6
7
module ApplicationHelper
  def present(model)
    klass = "#{model.class}Presenter".constantize
    presenter = klass.new(model, self)
    yield(presenter) if block_given?
  end
end

上面通过传的model来实例化其对应的Presenter,然后通过block的方式实现你需要的调用。

1
2
3
- present(@post) do |post|
  %h2= post.title
  %p= post.author

定制的Presenter

上面的presenter都是默认认为是modle的名字加上Presenter,如果post还有一个定制的 AdminPostPresenter该如何实现呢?其实很简单,present再加一个参数就搞定了,代码如下。

1
2
3
4
5
6
7
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)这样就可以了。

总结

使用Presenter极大的分离了model里面view相关的逻辑,也给测试带来了方便。也有类似的gem 例如draper实现了差不多的功能。如果你的view和model有相关的代码,可以考虑使用Presenter 来重构一下吧。

参考原文

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

评论