看到一篇文章想看看大家的意见,试着翻译过来分享一下~ , 原文地址
控制器是很笨的,他们仅仅只是一个模型层和视图层的连接器,用来处理关于请求的信息,然后把他们发送到他们应该去的地方。
事实上,大多数的控制器做的不仅仅是简单地处理请求。它们看管数据,响应许多失败条件,还展示业务逻辑,这是不好的。
为什么这是不好的呢?因为控制器会使逻辑掉入陷阱。它们比模型更加难以测试,并且这些逻辑几乎是不可能修复的。直接把业务逻辑放在控制器里面违背了 Single Responsibility Principle.
那让我们来看看咱们减少控制器的复杂度。其中一个方法是领域逻辑放到模型层里面。
许多控制器变得糟糕的原因是因为参数判断。“如果这个参数存在,干什么,否则,查找另一个参数,如果那个参数存在,干什么”等等。
Rails 把 REST 构造成七个方法,但是不意味着你的控制器就局限于此。如果你发现一个动作有超过 8 行代码的话,把他提取出来放到一个另外 的控制器方法中。
仅仅是这样还是不够的,这是一个你能够把逻辑放到 model 里面的标志。没有一个合适的 model?创建一个!可能它是一个门面,可能是一个工人。 不管怎样,把这些逻辑放到模型层里面,然后在控制器里面使用。
然后去掉你的定制的方法。就想我们说的,Rails 并没有把你限制在这七个 RESTful 方法里。但我们朝这个方向努力是非常好的。
在这个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 方法。没问题。
控制器。我们能够添加一个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
方法。
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
。
不知道大家怎么看这篇文章的观点?我现在看到过的代码都是创建实例变量,而这个文章说不要使用实例变量。