翻译 Slimming-Controllers

MaiZardAyumi · 2017年04月11日 · 最后由 torvaldsdb 回复于 2017年04月12日 · 6587 次阅读

看到一篇文章想看看大家的意见,试着翻译过来分享一下~ , 原文地址

Slimming-Controllers

控制器是很笨的,他们仅仅只是一个模型层和视图层的连接器,用来处理关于请求的信息,然后把他们发送到他们应该去的地方。

Pushing Responsibility Down the Stack (不知道怎么翻译。。)

事实上,大多数的控制器做的不仅仅是简单地处理请求。它们看管数据,响应许多失败条件,还展示业务逻辑,这是不好的。

为什么这是不好的呢?因为控制器会使逻辑掉入陷阱。它们比模型更加难以测试,并且这些逻辑几乎是不可能修复的。直接把业务逻辑放在控制器里面违背了 Single Responsibility Principle.

那让我们来看看咱们减少控制器的复杂度。其中一个方法是领域逻辑放到模型层里面。

限制逻辑判断

许多控制器变得糟糕的原因是因为参数判断。“如果这个参数存在,干什么,否则,查找另一个参数,如果那个参数存在,干什么”等等。

Rails 把 REST 构造成七个方法,但是不意味着你的控制器就局限于此。如果你发现一个动作有超过 8 行代码的话,把他提取出来放到一个另外 的控制器方法中。

仅仅是这样还是不够的,这是一个你能够把逻辑放到 model 里面的标志。没有一个合适的 model?创建一个!可能它是一个门面,可能是一个工人。 不管怎样,把这些逻辑放到模型层里面,然后在控制器里面使用。

然后去掉你的定制的方法。就想我们说的,Rails 并没有把你限制在这七个 RESTful 方法里。但我们朝这个方向努力是非常好的。

看看 index

在这个index的实现中,我们把所有的逻辑放到了模型里面。在这个例子中,我们可能想要进一步的重构模型,这可能太复杂了,但是这很好的说明了 我们正在谈论的步骤。

在这个控制器里面,我们有:

def index
    @articles, @tag = Article.search_by_tag_name(params[:tag])
 end

我们的目标是根据提供的tag找到articles。我们在 model 里面有如下定义:

def self.search_by_tag_name(tag_name)
  if tag_name.blank?
    [Article.scoped, nil]
  else
    tag = Tag.find_by_name(tag_name)
    tag ? [tag.articles, tag] : [[], nil]
  end
end

如果传入的 tag 的名称是nil或者空字符串,这个方法会返回 Article.scoped,这是Article.all的一个懒查询版本。如果提供了一个名称, 就会找到这个名称的 tag 然后返回所有的这个 tag 的 articles 和这个 tag,如果没有找到这个名称,会返回一个空数组和 nil。

尽管这不是最复杂的逻辑,但是在控制器里面测试它依然需要很多工作。打开article_spec.rb文件看看这些单元测试是多么简单。

接口

好的面向对象设计依赖于低耦合 - 拥有只有尽可能少的一部分联结在一起的明确的角色和职责。

根据 MVC 架构,Rails 里面的控制器和视图是独立的职责。他们应该是分开的部分,仅仅只通过一些设计良好的连接点来进行交流。

但 Rails 不是这么做的。

你怎么在视图里面获取控制器的数据呢?实例变量。看看下面这个例子, ArticlesController

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

  #...
end

当 Rails 为show动作渲染视图时,复制了控制器里面的实例变量到渲染的视图中,这两个部分很显然耦合了。

OPINION: 无论何时你在 Ruby 你使用实例变量,问问你自己:“是否有更好的方法?”答案总是“是”

限制实例变量

一个常规的控制器动作有一个实例变量。许多动作有两个或三个实例变量,但是如果你有更多的话,这是一个你缺少领域抽象的信号。

这些对象之间必要的“联系”是什么?为什么它们都属于同一个页面?无论原因是什么,这可能是一个领域对象。看看 Facade pattern

我们频繁地使用 accessor 方法。我们怎么为控制器写 accessors?这是非常建的的,让我们再来看看这个控制器和动作

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

  #...
end

视图模板用法

在视图模板中,我们像这样访问这个对象:

<h1><%= @article.title %></h1>

如果我们不使用实例变量,我们怎么做呢?这样怎么样:

<h1><%= article.title %></h1>

我们怎样使它生效呢?Rails 将有一个叫做 article 的 helper 方法。没问题。

实现一个 Helper Method

控制器。我们能够添加一个article方法:

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

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

  #...
end

我们没有必要传递params到我们的新方法,因为现在仍然在控制器上下文里面,所以能够访问常规的params,就像任何其他的动作一样。

加载视图,没有起作用。仅仅在控制器里面定义方法是不够的,我们必须把它作为一个 helper 暴露给视图。

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

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

  helper_method :article

  #...
end

现在视图生效了。但是show动作怎么办呢?如果视图现在是访问article helper,让我们去掉这些实例变量:

class ArticlesController < ApplicationController
  def show
  end

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

  helper_method :article

  #...
end

如果你喜欢,你甚至可以删掉show 方法。

The new Action

show 动作是简单的,那么new呢?

def new
    @article = Article.new
end

在视图模板里面我们希望有一个叫做@article的新的,空的对象。如果我们用这个存在的 helper,它会试图做一个机遇params[:id]的查找。

没问题,在这个 helper 方法里添加一些对params[:id]的响应逻辑:

class ArticlesController < ApplicationController
  def new
  end

  def article
    if params[:id]
      Article.find(params[:id])
    else
      Article.new
    end
  end

  helper_method :article

  #...
end

我们去掉了new方法里面的代码,然后改变了模板视图来使用 helper 而不是实例变量。

性能考虑

哇哦,你注意到当查找show视图时的控制台日志了吗,如果你没有,现在看一看。好像我们进行了一大推查询,每次我们调用 hepler 时。

不是问题:我们将使用一个实例变量在第一次请求后来记住这个对象

class ArticlesController < ApplicationController
  def article
    @cached_article ||= if params[:id]
      Article.find(params[:id])
    else
      Article.new
    end
  end

  # ...
end

为什么这个实例变量叫做@cached_article?我不想用@article,否决有些人又会继续开始在视图里面使用实例变量,如果你想访问 article,你应该使用我已经定义的访问方法,我强烈鼓励你这么做。

可能你甚至想使代码更简洁,你看了看逻辑并且想把它放到模型层里面。好主意!你可能会创建像这样的东西:

class Article < ActiveRecord::Base

  def find_or_build(input_id = nil)
    if input_id.present?
      Article.find(params[:id])
    else
      Article.new
    end
  end
end

但这是没有必要的,你能使用在ActiveRecord里面内置的find_or_initialize_by方法:

class ArticlesController < ApplicationController
  def article
    @cached_article ||= Article.find_or_initialize_by_id(params[:id])
  end

  # ...
end

如果找到了,find_or_initialize_by方法会返回该记录,否则会返回这个类的一个新的实例,在这个例子中是Article

不知道大家怎么看这篇文章的观点?我现在看到过的代码都是创建实例变量,而这个文章说不要使用实例变量。

他需要的是

attr_reader :article
helper_method :article

因为他的需求只是不想在模板里写那个@

lolychee 回复

他说的是为了减低 controller 和 view 的耦合吧,而不用实例变量吧。。

lolychee 回复

不过我还没见过这么写的

十行不到的代码也能 yy 出这么多 pattern。。

无聊的硬♂了

不知道性能到底是提高了还是降低了。。。 rails 坚持约定优于配置,拥有自己的 CURD,如果一定要用helper辅助方法,个人感觉很蹩脚。 对了,从你这篇文章中,也学到了很多东西。谢谢

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