Gem Draper 使用帮助 (即 draper README 翻译)

zamia · 2016年02月28日 · 最后由 liamsmith 回复于 2024年01月29日 · 4528 次阅读

本来想写一篇小文章来解释 decorator 的使用,发现 draper 这个 gem 的 readme 写的很赞,不只是说明了 gem 的使用,还清晰的解释了为什么要使用 decorator,什么情况下使用 decorator,所以花了几个小时翻译了一下,方便喜欢看中文的同学~

rails 本身的 helper 的主要问题是全局命名空间以及调用不方便,draper 使用了 decorator 设计模式,通过面向对象的方法来对原有的 model 进行封装。这个 gem 也非常强大,如果是一个生命期较长的 rails 项目的话,强烈推荐使用 draper 来封装 model 的 view 逻辑来代替 helper。

发现翻译文章比写一篇还累啊~~

============== 我是分割线 ============

Draper: View Models for Rails

Draper 使用『面向对象』的方式,给 Rails 添加了独立的视图层。

在不使用 Draper 的情况下,类似功能只能通过添加一个 helper 或者给 model 添加一堆逻辑来实现,而使用 Draper 之后,通过对视图逻辑进行封装,使代码组织更清晰,也更易于测试。

为什么使用装饰器?

比如你的应用中有一个 Article 模型,使用 Draper 之后,可以创建一个对应的 ArticleDecorator,这个 decorator 封装了 model 对象,并且只封装了视图相关的逻辑。在 controller 中,在传递给 view 层之前,我们可以先装饰一下 article 模型:

# app/controllers/articles_controller.rb
def show
  @article = Article.find(params[:id]).decorate
end

这样在 view 层使用装饰过的 model 和直接使用 model 基本上没有任何区别。但是,任何时候,如果你开始在 view 中写一堆逻辑、或者你想写一个 helper 方法的时候,就可以通过在 decorator 中实现一个方法来代替。

下面的例子演示了如何把一个 helper 方法转成 decorator 方法。假如目前有一个 helper 方法长这样:

# app/helpers/articles_helper.rb
def publication_status(article)
  if article.published?
    "Published at #{article.published_at.strftime('%A, %B %e')}"
  else
    "Unpublished"
  end
end

这样写完就会觉得很别扭,publication_status 的命名空间是全局的,它在所有的 controllers 和 views 中均可以调用。然后,过了一段时间,当你想实现一个 Book 对象的 publication status 的时候,并且要求 book 的日期格式跟 article 不一样。怎么整?

两个办法。要么通过传入参数的类型来判断对象的类型(Ruby 并不是静态类型的语言,所以需要在函数体来判断),然后实现不同的逻辑;要么把这个方法拆成两个方法,book_publication_statusarticle_publication_status。随着项目不断变大,需要持续添加方法到全局的命名空间,调用的时候也必须记住所有的函数名。额,ugly……

这时候,需要使用面向对象的思维。假如你不知道 rails 有个东西叫 helper,你可能想着能这样调用就好了:

<%= @article.publication_status %>

假如没有 decorator,那么就得在 Article 模型中实现这个 publication_status 这个方法,但是这个方法呢,本身又属于视图逻辑,并不属于模型层的逻辑。

所以,更好的方法呢,是实现一个 decorator:

# app/decorators/article_decorator.rb
class ArticleDecorator < Draper::Decorator
  delegate_all

  def publication_status
    if published?
      "Published at #{published_at}"
    else
      "Unpublished"
    end
  end

  def published_at
    object.published_at.strftime("%A, %B %e")
  end
end

publication_status 方法内,我们使用了 published? 方法,这个方法从哪来的?其实它是在 Article 中定义的。得益于 delegate_all 调用,Article 中的方法可以无缝的在这个 decorator 中使用。

decorator 有一些别名,比如 "presenter","exhibit","view model",也有直接叫 "view" (这样的命名约定下,Rails 中的 views 应该叫 templates)的。不管叫什么,使用面向对象编程来代替 helper 这种面向过程编程,都是非常棒的方法!

综合来说,遇到下面这些情况时,Decorators 尤为合适:

  • 格式化复杂的数据显示给用户;
  • 实现模型对象中常用的展示逻辑时,比如由 first_name 和 last_name 合并生成 name 这种情况;
  • 封装一些对象的属性,比如把 url 属性变成一个超链接。

安装(Installation)

添加 Draper 到 Gemfile:

gem 'draper', '~> 1.3'

然后在应用的根目录下执行 bundle install 即可。

如果是从 0.x 的版本中升级而来,主要的变更在 wiki 中有详细说明。

实现装饰器(Writing Decorators)

Decorators 继承自 Draper::Decorator,一般放入 app/decorators 目录,并且命名保持和相应的模型一致:

# app/decorators/article_decorator.rb
class ArticleDecorator < Draper::Decorator
# ...
end

生成器

引入 draper gem 之后,当使用 rails 生成一个 controller 时:

rails generate resource Article

也会自动生成一个 decorator,非常方便!!

如果 Article 模型已经存在的话,也可以直接执行:

rails generate decorator Article

来创建一个 ArticleDecorator

调用 Helper 方法

一般的 rails helper 方法还是不可避免的要使用,不管是 rails 内置的 helper,还是 app 内自定义的 helper,通过 h 对象即可调用:

class ArticleDecorator < Draper::Decorator
  def emphatic
    h.content_tag(:strong, "Awesome")
  end
end

嗯,如果觉得总是写一个 h. 很麻烦,可以在 Decorator 类中添加:

include Draper::LazyHelpers

那么会自动 mixin 很多 helper 方法,也就不再需要写 h. 了。

(注意:capture 方法必须通过 h 或者 helpers 来调用)

调用 model 中的方法

当 decorator 中需要调用 model 中的方法时,除了 下面 会提到的 delegation 的方式,任何时候都可以通过 object 对象(或者 model 对象)来调用。

class ArticleDecorator < Draper::Decorator
  def published_at
    object.published_at.strftime("%A, %B %e")
  end
end

使用装饰器(Decorating Objects)

单个对象的装饰

好,现在写完一个装饰器了,如何使用呢?最简单的办法是调用 model 的decorate 方法:

@article = Article.first.decorate

这种方式通过命名约定来自动推断应该使用哪个装饰器类。假如需要更灵活的使用,比如 使用 ProductDecorator 装饰了一个模型 Widget,可以直接调动装饰器类:

@widget = ProductDecorator.new(Widget.first)
# or, equivalently
@widget = ProductDecorator.decorate(Widget.first)

集合的装饰

装饰集合中的所有对象

如果要装饰一个对象的集合,可以一次性装饰所有的对象:

@articles = ArticleDecorator.decorate_collection(Article.all)

假如集合是一个 ActiveRecord 查询,也可以直接这样使用:

@articles = Article.popular.decorate

注意: 在 Rails 3 中,.all方法返回的是一个数据,所以 不能 使用 Article.all.decorate 这种方法。但是,Rails 4 中,.all 方法返回的是一个 query 对象,所以可以用这种方法。

装饰集合本身

如果想给一个集合本身添加一些方法(比如,用于分页),那么可以继承 Draper::CollectionDecorator

# app/decorators/articles_decorator.rb
class ArticlesDecorator < Draper::CollectionDecorator
  def page_number
    42
  end
end

# elsewhere...
@articles = ArticlesDecorator.new(Article.all)
# or, equivalently
@articles = ArticlesDecorator.decorate(Article.all)

Draper 使用 decorate 方法来装饰每个对象,你也可以通过覆盖集合装饰器的 decorator_class 方法来改名,或者传递一个 :with 参数给构造器。

使用分页

有些分页的 gem 会添加一些方法到 ActiveRecord::Relation,比如 Kaminaripaginate 方法需要集合实现 current_page, total_pages, and limit_value 这些方法。为了导出这些方法给一个集合类的装饰器,可以把这些方法 delegate 到 object 对象:

class PaginatingDecorator < Draper::CollectionDecorator
  delegate :current_page, :total_pages, :limit_value, :entry_name, :total_count, :offset_value, :last_page?
end

这里的 delegateActive Support 中的 delegate 方法是相同的,除了 参数 :to 是可选的,不传递时默认 delegate 到 :object 对象。

will_paginate 需要下面这些方法被 delegate :

delegate :current_page, :per_page, :offset, :total_entries, :total_pages

装饰相关联的对象

当主模型被装饰时,可以自动装饰相关联的对象。比如,Article 模型有一个相关的对象 Author 时:

class ArticleDecorator < Draper::Decorator
  decorates_association :author
end

ArticleDecorator 装饰一个 Article 时,draper 会自动使用 AuthorDecorator 来装饰 Author 对象。

装饰 Finders

还可以在 decorator 中调用 decorate_finders 方法:

class ArticleDecorator < Draper::Decorator
  decorates_finders
end

这样,当在 decorator 调用 finder 类方法时,可以直接返回一个装饰过的对象:

@article = ArticleDecorator.find(params[:id])

什么时候使用装饰器?

理论上,装饰器是和它所装饰的对象行为上是很接近的,所以,看起来在 controller 的 action 方法一开始就装饰这个对象,然后一直使用这个装饰器对象就行了。

别这么干!

因为,装饰器本质上就是为了给 view 层使用的,所以只应该在 view 层使用装饰器。先准备好 model,然后在最后一刻开始装饰它们,然后紧接着在 view 中使用它们。这样的话,就避免了很多尝试修改装饰器对象而导致的诸多隐患。

为了让装饰器对象只读,draper 也提供了 decorates_assigned 方法给 controller。它添加了一个 helper 方法,会自动返回一个装饰过后的对象:

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  decorates_assigned :article

  def show
    @article = Article.find(params[:id])
  end
end

decorates_assigned :article这个语句基本上等同于:

def article
  @decorated_article ||= @article.decorate
end
helper_method :article

也就是说,只需要在 views 中调用 article 方法(取代常用的 @article 对象),就可以直接得到一个装饰后的 @article 对象。而在 controller 中,可以继续使用 @article 变量来进行各种逻辑操作,比如 @article.comments.build 可以创建一个 comment。

测试 (Testing)

Draper 支持 Rspec,MiniTest::Rails 以及 Test::Unit,并且生成 decorator 的时候会自动创建一些测试用例。

Rspec

spec 测试文件一般放在 spec/decorators 中。如果放在另外一个目录,那么需要使用 type: :decorator来标记它们。

在 controller 测试中,可能会想判断一个实例变量是否被正确的装饰了。可以使用下面这些 matchers 来判断:

assigns(:article).should be_decorated

# 或者,下面这样可以显式的指定装饰器类
assigns(:article).should be_decorated_with ArticleDecorator

另外,model.decorate == model,所以,添加 decorator 之后,已经写好的 specs 应该仍然可以通过测试。

Spork 用户

spec_helper.rb 文件中的 Spork.prefork 段,需要添加下面的代码:

require 'draper/test/rspec_integration'

独立测试装饰器

在测试中,Draper 会创建一个 view 的上下文环境来存取 helper 方法。默认情况下,Draper 会创建一个 ApplicationController,然后在 view 的上下文中使用它。假如你想通过测试每一个组件来加入测试,那么可以通过添加下面的代码到 spec_helper 或者类似文件中来删除这种依赖:

Draper::ViewContext.test_strategy :fast

这样的话,装饰器就不再能够存在应用的 helper 方法,如果需要有选择的引入一些 herlper 方法,可以传递一个 block 给这个方法:

Draper::ViewContext.test_strategy :fast do
  include ApplicationHelper
end

Stub 路由相关的 helper

假如需要写一些依赖于 routes helper 的装饰器方法,那么可以 stub 这些路由,而无需引入 Rails。

假如你在使用 Rspec,minitest-rails 或者 minitest 中 Test::Unit 语法,那么可以直接使用 helpers 对象在你的测试中,因为这些测试是继承自 Draper::TestCase。假如你在使用 minitest 的 spec 语法,并且没有使用 minitest-rails,可以显式的引入 Draper 的 helpers

describe YourDecorator do
  include Draper::ViewHelpers
end

然后,就可以使用你熟悉的方式来 stub 一些路由的 helper 方法了(下面的例子使用了 Rspec 的 stub 方法):

helpers.stub(users_path: '/users')

高级使用技巧(Advanced Usage)

共享装饰器方法

可能会遇到多个装饰器都有类似方法的情况,因为装饰器就是一个 Ruby 对象,所以完全可以使用常用的 Ruby 的技巧来共享相关功能。

比如,Rails 控制器中,一般的 Controller 都是继承自 ApplicationController,可以在装饰器的实现中也使用类似技巧:

# app/decorators/application_decorator.rb
class ApplicationDecorator < Draper::Decorator
# ...
end

然后,让所有的装饰器都继承自这个 ApplicationDecorator,不再直接继承自Draper::Decorator

class ArticleDecorator < ApplicationDecorator
  # decorator methods
end

只 delegate 指定的方法

当装饰器调用 delegate_all 的时候,所有被调用的方法如果没有在装饰器中定义,那么都会委托至原有的 model 对象。这样有点过度了~

所以如果想严格控制哪些方法可以在 view 中被调用,那么可以只 delegate 部分方法到 model 中:

class ArticleDecorator < Draper::Decorator
  delegate :title, :body
end

我们省略了参数 :to ,这样的话,默认委托至 object 对象。

也可以选择委托至其他对象:

class ArticleDecorator < Draper::Decorator
  delegate :title, :body
  delegate :name, :title, to: :author, prefix: true
end

这样,在 view 的模板中,假如 @article 已经被装饰过,那么可以像下面这样使用:

@article.title # 返回 the article's `.title`
@article.body  # 返回 the article's `.body`
@article.author_name  # 返回 the article's `author.name`
@article.author_title # 返回 the article's `author.title`

传递上下文

假如需要传递额外的数据给装饰器,可以在创建装饰器的时候,使用一个 context 参数来传递数据。比如:

Article.first.decorate(context: {role: :admin})

传递给 :context 参数的数据,在装饰器中可以通过 context 方法来获取。

假如使用了 decorates_association,那么主模型的上下文数据会传递给相关的对象。也可以覆盖这个 :context 参数:

class ArticleDecorator < Draper::Decorator
  decorates_association :author, context: {foo: "bar"}
end

或者,如果希望修改主模型的上线文数据,可以使用 lambda 表达式:

class ArticleDecorator < Draper::Decorator
  decorates_association :author,
    context: ->(parent_context){ parent_context.merge(foo: "bar") }
end

指定装饰器类

当使用 decorates_association时,Draper 使用 decorate 方法来装饰每一个关联对象,假如想使用一个不同的类来装饰,可以使用 with参数:

class ArticleDecorator < Draper::Decorator
  decorates_association :author, with: FancyPersonDecorator
end

如果是一个集合的关联(比如 has_many),可以传递一个 CollectionDecorator 的子类,这样的话会装饰整个集合;或者传递一个 Decorator 的子类,这样的话会装饰每一个集合内的对象。

限定 association 的范围

如果期望被装饰的关联对象被排序、限定个数或者其他限定,可以传递 :scope 参数给 decorates_association,这样的话,这个方法会在对象装饰 之前 被调用:

class ArticleDecorator < Draper::Decorator
  decorates_association :comments, scope: :recent
end

代理类方法

如果想代理类方法给模型类,包括使用 decorates_finders 的时候,Draper 必须要知道具体的模型类是什么。默认情况下,Draper 默认你的装饰器被命名为 SomeModelDecorator,然后会代理所有的未知方法给 SomeModel

如果,命名不符合约定,Draper 无法推导出相应的模型类,则需要显式的调用 decorates 方法:

class MySpecialArticleDecorator < Draper::Decorator
  decorates :article
end

这仅仅当需要代理类方法的时候才需要。

使模型可以被装饰

模型对象通过 mixin Draper::Decoratable 模块来获得 decorate 方法,这个行为默认在 ActiveRecord::BaseMongoid::Document 中被引入。

所以如果你 使用了其他 ORM(包括 3.0 版本之前的 Mongoid),或者想装饰普通的 Ruby 对象,那么需要显式的 include 这个模块。

贡献者

Draper was conceived by Jeff Casimir and heavily refined by Steve Klabnik and a great community of open source contributors.

核心开发者

使用 concern 不是更简便?

#2 楼 @ruby_sky concern 主要用来在 model 之间共享逻辑,跟这个要解决的是两个问题。另外,concern 本身也是不推荐大量使用的,concern 采用了 mixin 的方式,一旦代码量增大,也容易导致不少问题。

#4 楼 @zamia 不要这么想。concern 其实就是一个 module 而已,当成正常的业务来分拆来用。

#5 楼 @ruby_sky 当然,如果当做 module 的话,按正常的 ruby way 使用用就行了;rails 的 concern 一般 用于 model 间的业务共享;而且用 module 并不能解决 view-model 的问题,完全两码事~

简单理解:一个 helper 方法被要求只能在 view 中使用并且是设定的对象才能调用该方法,并非全局。

解决 view-model 感觉 presenter 看起来更纯粹 Draper 很好用,也选择了用它,不过也推荐看一下 http://thepugautomatic.com/2014/03/draper/

很有用,点赞~ 👍

仍然不懂 decorate_collection 与 decorate 使用上的具体差别,有无类似举例?谢谢!

ktr 回复

decorate_collection 用在 集合 上,decorate 用在实例上。

  • decorate_collection -> Article.all 或者 Article.where(condition) 或者 Article.where(condition).filter(condition) -> 返回一个集合
  • decorate -> Article.first 或者 Article.find(condition) 或者 Article.find_by(condition) 或者 Article.where(condition).filter(condition).detect(condition) -> 返回一个实例

盲猜:

# 以下等价
ArticleDecorator.decorate_collection(Article.all)
Article.all.each(&:decorate)
# 以下等价
Article.first.decorate
ArticleDecorator.decorate_collection([Article.first])

如果没有装饰器,那么就必须在 Article 模型中实现 publication_status 方法,但是这个方法本身又属于视图逻辑,而不属于模型层的逻辑。mapquest driving directions

需要 登录 后方可回复, 如果你还没有账号请 注册新账号