10 项 Ruby on Rails 的最佳实践

2016-11-25 10:32:55来源:作者:开源中国翻译文章人点击

Ruby on Rails 是一款被宽泛使用的 Web 应用程序框架。 Rails 使我们办公更有效率,让我们更专注于手头的任务而不是技术本身。 在初学阶段,坚持 Rails 的最佳实践非常重要 。因此,在这篇文章中,我们将对 Ruby on Rails 中的最佳实践做系列介绍。

毁灭之路

如果你忽略了 Web 应用程序框架的最佳实践重要性,那么你就不算了解框架,甚至有大麻烦。最坏的情况,在此情况下开发的应用程序会出现许多问题,你后期需要不停“擦屁股”。并且,这还会给新功能的开发,项目的维护以及开发者的引入带来困难。因此,坚持最佳的实践能让你保持工作的高能和高效,避免你(或你的团队)在问题出现时焦头烂额,抓耳挠腮。

荣耀之路

正如标题所示:最佳实践。因为某些原因他们被广泛使用着。这里列出一些优点:

1.可维护性

2.可读性

3.优雅

4.快速发展

5.DRY代码

让我们一起来了解下。

Ruby on Rails 社区风格指南:

在每种编程语言中,我们都会看到糟糕的或者精彩的代码。代码风格因人而异,这就导致新开发成员加入时项目的延迟。拥有一个由社区推动的编码风格指南十分重要,因为它一致性风格的实现,在贯穿整个代码库中起着至关重要的作用。项目 的建立通常由小团队换到大团队,开发者们也有不同的编码风格和背景。遵循Ruby社区风格指南是我的第一个最佳实践,这里有一些我想特意强调的风格偏好:

两个空格缩进

这是Ruby社区中最广泛采用和最赞同的风格指南之一。使用2空格缩进代替4空格缩进。让我们看一个例子:

4 个空格缩进

def some_method some_var = true if some_var do_something else do_something_else endend

2 个空格缩进

def some_method some_var = true if some_var do_something else do_something_else endend

后者更加简洁,可读性强。此外,它在大文件里更多层次的缩进将会更加明显。

用 a 来定义判断方法?

在Ruby中我们有一个方法返回true或者false的约定。这些方法就是判断方法,约定是以带有问号(?)的名称结尾。在大多数编程语言中,你会看见定义的方法,或者各种各样对变量名称的定义,如 is_valid或is_paid 等。Ruby不鼓励这种风格,它们鼓励大家使用更人性化的语言,如:object.valid?或orfee.paid?(注意,这里没有is_前缀),这样的风格与Ruby的通用性和可读性保持一致。

迭代: 使用each 而不是 for

几乎所有的Ruby程序员遍历集合时都是使用each来进行迭代,而不是for。因为它更简单易读。

* for *

for i in 1..100 ...end

* each *

(1..100).each do |i| ...end

看到效果了吗?

条件: 使用 unless 而不是!if:

如果你发现你自己使用if语句来进行条件判断 例如:

if !true do_thisend

或者

if name != "sarmad" do_thatend

那么你应该使用Ruby独有的unless 语句, 像这样:

unless true do_thisend

或者

unless name == "sarmad" do_thatend

同样,它也是跟可读性相关。然而,如果你的条件中需要使用一个else语句,那么千万不要使用unless-else.

错误的语句

unless user.save #throw errorelse #return successend

正确的语句

if user.save #return successelse #throw errorend 异常处理**

异常处理是用于在某些条件下提前退出方法的术语,参考如下示例:

if user.gender == "male" && user.age > 17 do_somethingelsif user.gender == "male" && user.age < 17 && user.age > 5 do_something_elseelsif user.age < 5 raise StandardErrorend

在这种情况下,它需要通过判断所有条件的来确定用户是否低于5岁,并抛出异常。首选方法是:

raise StandardError if user.age < 5if user.gender == "male" && user.age > 17 do_somethingelsif user.gender == "male" && user.age < 17 #we saved a redundant check here do_something_elseend

当满足某个条件时尽早的返回能让程序效率更高。

提示:我强烈建议你仔细看看这些编码风格指南 这里(Ruby)和 这里 (Rails)。

编写测试

如果你熟悉 Rails 的话,那么就会了解 Rails 社区对于测试有多么重视了。我曾听人说过,作为一个新手,测试工作使得学习 Rails 成为一件难事儿。不过也有人说过从一开始就这样做对于掌握 Rails 的基础知识(以及在某些普通的网页开发场景中)有帮助。不过这些都不会阻止测试成为软件开发界绝对正确的最佳实践。事实上,我遇到过有人会抱怨,如果把测试工作加进来的话,要完成一个功能就需要花费更多的时间。但是一旦他们在使用 Rails 开发的时候进入到测试环节,并且一开始就承受了编写测试的“麻烦”,那么实际上就能在第一时间将功能构件好。另外,这样做也能使程序涵盖到许多的边缘场景,促使其成为项目的一个更好的设计。一个好的 Ruby 程序员天生就善于做测试。

让我们来列举下测试的一些好处:

测试扮演着一个更能或者应用程序的详细规格说明书这一角色。

测试可以成为其他开发者的文档,帮助他们理解你在程序实现中的意图。

测试有助于你实现就能捕获并且修复一些问题。

测试会在你重构代码或者进行性能增强,而不想影响到程序的任何其它方面时,给你带来信心。

DRY (不要浪费你的生命)

要尽可能的确保你没有在重复做一些相同的事情来浪费自己的生命。让我们来讨论一下 Ruby 的面向对象规则中许多可以帮助你避免重复的方法。

使用抽象类: 假设你有下面这样两个类:

class Mercedes def accelerate "60MPH in 5 seconds" end def apply_brakes "stopped in 4 seconds" end def open_boot "opened" end def turn_headlights_on "turned on" end def turn_headlights_off "turned off" endendclass Audi def accelerate "60MPH in 6.5 seconds" end def apply_brakes "stopped in 3.5 seconds" end def open_boot "opened" end def turn_headlights_on "turned on" end def turn_headlights_off "turned off" endend

这两个类彼此中有三个重复的方法 open_boot,turn_headlights_on, 以及 turn_headlights_off。在本文中我们不会去讨论为什么不去写重复的代码, 关于此你可以读一读 这个。现在讨论只专门针对DRY 这个原则。这里要使用的最佳实践就是使用继承和/或者抽象类。下面我们来重写这个类以解决问题:

class Car # Uncomment the line below if you want this class to be uninstantiable # i.e you can't make an instance of this class. # You can only inherit other classes from it. # self.abstract = true def open_boot "opened" end def turn_headlights_on "turned on" end def turn_headlights_off "turned off" endendclass Mercedes < Car def accelerate "60MPH in 5 seconds" end def apply_brakes "stopped in 4 seconds" endendclass Audi < Car def accelerate "60MPH in 6.5 seconds" end def apply_brakes "stopped in 3.5 seconds" endend

明白吗? 这样好多了!

使用模块

模块,从另外一方面来看,是一种在类之间共享行为的灵活方式。人们应该使用除了继承之外的模块(组件)的原因超出类本文的讨论范围。这里说明一下模块是类和行为之间一种“has-a”关系而继承是一种“is-a”关系就行了:

class Newspaper def headline #code end def sports_news #code end def world_news #code end def price #code endendclass Book def title #code end def read_page(page_number) #code end def price #code end def total_pages #code endend

假设我们需要向两个类都添加一个 print 方法,而不想编写重复的代码,就可以使用模块,像下面这样:

module Printable def print #code endend

修改类的代码,让它们引入这个模块:

class Newspaper #This wil add the module's methods as instance methods to this class include Printable def headline #code end def sports_news #code end def world_news #code end def price #code endendclass Book #This wil add the module's methods as instance methods to this class include Printable def title #code end def read_page(page_number) #code end def price #code end def total_pages #code endend

这是一种非常强大且使用的技术。我们也可以使用extend Printable而不是include Printable 来让模块的方法成为类的方法。

枚举类型的巧妙使用

假如说你有了一个叫做 Book 的模型,这个模型拥有一个列/域,你想要在这个列/域里面存储这本书,而不管是草稿、完成还是发布的状态。你发现自己正要做的会是像下面这样:

if book.status == "draft" do_somethingelsif book.status == "completed" do_somethingelsif book.status == "published" do_somethingend

或者:

if book.status == 0 #draft do_somethingelsif book.status == 1 #completed do_somethingelsif book.status == 2 #published do_somethingend

如果是这种情况的话,你就应该去看看枚举类型了! 你要将这个状态列定义成整型,理想情况下应该不能为空(null:false), 并且还想要这个模型再创建了之后其状态有一个默认值,例如默认为0。现在,你就可以像下面这样定义枚举了:

enum status: { draft: 0, completed: 1, published: 2 }

现在,你可以将代码进行重写了,如下:

if book.draft? do_somethingelsif book.completed? do_somethingelsif book.published? do_somethingend

看起来很棒,是不是? 这样的做法不仅让你得到了对应状态名称的判断方法,还给你提供了可以在你所定义的状态之间进行切换的方法。

book.draft!book.completed!book.published!

这些方法也可以切换状态来匹配。这将会是你的工具库中多么优雅的一个工具啊。

胖的模型,瘦的控制器和关注点

另外一个最佳实践就是坚持不对控制器之外的相关逻辑进行响应。那些你并不想要将其放到一个控制器的代码的例子就是任何业务的逻辑或者持久化/模型变更的逻辑。例如,有些人可能会让他们的模型看起来像下面这个样子:

class BooksController < ApplicationController before_action :set_book, only: [:show, :edit, :update, :destroy, :publish] # code omitted for brevity def publish @book.published = true pub_date = params[:publish_date] if pub_date @book.published_at = pub_date else @book.published_at = Time.zone.now end if @book.save # success response, some redirect with a flash notice else # failure response, some redirect with a flash alert end end # code omitted for brevity private # Use callbacks to share common setup or constraints between actions. def set_book @book = Book.find(params[:id]) end # code omitted for brevityend

现在让我们将这段复杂的逻辑转换成相关的模型:

class Book < ActiveRecord::Base def publish(publish_date) self.published = true if publish_date self.published_at = publish_date else self.published_at = Time.zone.now end save endendclass BooksController < ApplicationController before_action :set_book, only: [:show, :edit, :update, :destroy, :publish] # code omitted for brevity def publish pub_date = params[:publish_date] if @book.publish(pub_date) # success response, some redirect with a flash notice else # failure response, some redirect with a flash alert end end # code omitted for brevity private # Use callbacks to share common setup or constraints between actions. def set_book @book = Book.find(params[:id]) end # code omitted for brevityend

这是一种直观的场景,其中能很明确的认识到这块功能属于模型。而在许多其它的场景中,你就得多花点心思找到一个正确的平衡了,而且要知道什么该怎么放。有时候你从一个控制器那里独立出来的逻辑并不适合放到任何模型的上下文中,这个你就得仔细琢磨一下那里会最适合它了。我会根据我的经验为你设定一些简单的规则,但如果你能想到一些针对一些问题的更好的方法,就在评论中告诉我吧。

控制器应该只是做一些针对模型的简单的查询操作。复杂的查询应该挪到模型中去,并且要在 可复用的范围进行分割。控制器中包含的应该主要是一些请求处理和响应相关的逻辑。

任何代码,只要不是跟请求和响应相关的,但是又直接跟一个模型相关,就应该被挪到模型中去。

任何表示一个数据结构的类都应该作为一个 Non-ActiveRecord 模型(无表类)挪到 app/models目录中去。

当逻辑涉及特定领域(打印,库一起其它诸如此类),而非真正适合一个模型 (ActiveRecord or Non-ActiveRecord)的上下文时,就使用 Ruby 的 PORO (Plain Old Ruby Objects) 类。你可以将这些类放到 app/models/some_directory/ 中去。任何被放到 app/目录中的东西都会被自动在应用启动时加载,因为这个目录已经被包含在 Rails 的自动加载路径中了。PORO 也可以被放到 app/models/concerns以及 app/controllers/concerns目录中去。

如果是独立于应用程序的 PORO, 模块或者类,可以放到 l ib/目录中去,这样也可以被用于其它应用程序。

如果你必须从其它不相关的功能中提取常用的功能,请使用模块。你可以将它们放到 app/* 目录,而如果它们是独立于应用程序的,就放到 lib/目录。

当应用程序代码不断增长,而难以决定将特殊的逻辑放置到哪里时,“服务”层是另外一个支持通用MVC的相当重要的地方。假设你需要有一个在一本书被发布时可以发送 SMS 或者 Email 给订阅者的机制,或者向他们的设备推送通知,就可以在 app/service/ 中创建一个 Notification 服务,并且在你需要该功能时启动这个服务。

国际化/本地化

一开始就要对你的应用进行国际化。不要把这件事情留到最后,否则后面就会变成一个困扰你的大问题。好的网站不会只支持一种语言,它们通常都会有一个更大的目标,目标越大越好。开发时就国际化是最佳的实践之一来考虑。这就是为了 Rails 跟 I18ngem 保持同步, 它表明了对你的应用进行国际化的重要性。关于此你可以看看 这里。

这个gem给你提供了如下开箱即用的功能:

支持对英语以及类似语言开箱即用

简化了针对其它语言的定制和扩展操作

它能让你设置一个默认的区域,并且根据用户的所在区域或者偏好的区域设置来进行变更。

如下是一个简单的示例,展示了如何将一段非国际化的 HTML 转变成国际化的 HTML:

<h1>Books Listing</h1><table> <thead> <th>Name</th> <th>Author</th> </thead> <tbody> <td> Some book </td> <td> Some author </td> </tbody></table

config/locales目录中的文件被用来支持国际化,它们可以被 Rails 自动加载。每一个Rails 应用默认都会有一个 config/locales/en.yml文件。这个文件负责保存英语的翻译。如果你需要添加更多语言的翻译,只要添加名称匹配对应区域且后缀为 .yml 的文件就行了。在本例中我们仍然使用 en.yml,对上面的HTML进行国际化重构:

<h1><%= t('.title') %></h1><table> <thead> <th><%= t('.name') %></th> <th><%= t('.author') %></th> </thead> <tbody> <td> Some book </td> <td> Some author </td> </tbody></table>

现在将 .yml 文件中所示的内容放进去,这样修改后的 HTML 就可以提取翻译了。

# config/en.ymlen: title: "Books Listing" name: "Name" author: "Author" 数据库最佳实践

db/schema.rb文件在其顶部的一段注释中如是说:

强烈建议你将这个文件放到版本控制系统中去。

还有:

如果你需要在另外一个系统上创建应用程序的数据库, 应该使用 db:schema:load, 而不是从头开始将所有的迁移都跑一遍。后者是一种有缺陷并且不可持续的方式 (你做了越多的迁移,运行起来就会越慢,出问题的可能性也越大 )。

强烈建议你将这个文件放到版本控制系统中去,如果没有放进去保持更新, 那么就没法利用 rails 的 db:schema:load 命令。正如上面解释的,如果你需要在另外一台机器上创建应用程序的数据库,就应该使用db:schema:load 而不是 db:migrate。不鼓励重头开始跑所有的迁移,因为这样做会随着时间推移徒生瑕疵。我个人就遇到过这个问题好几次。当迁移出问题时,很难对问题进行跟踪定位, 找出到底是在迁移过程的哪个位置上出问题了。db:schema:load 是这些境况的救星。

注意!db:schema:load 只被用于你需要在一个新系统上创建应用程序数据库的时候。如果只是要添加新的迁移,你应该只要让 db:migrate 来做就行了。如果你在现有的一个已经填充了数据的DB上运行 db:schema:load, 你的数据 (有可能是生产数据) 将会被清除掉。因此只要牢记下面这三条规则,你就是安全的:

在你添加并应用了新的迁移时,总是将对 schema.rb的修改提交到你的版本控制系统中去。

在一个新系统上创建应用程序数据库是使用 db:schema:load。

所有其它在你需要应用新的迁移的情况下都使用 db:migrate。

提示:不要使用迁移来向DB添加数据,而是将 db/seeds.rb用于此目的。

嵌套的资源/路由

如果你拥有一个数据,它属于另外一个资源的子资源, 那么最好对内嵌在父资源的路由中的子资源的路由进行一下定义, 假如你拥有一个 Post资源和一个 Comment资源, 并且有对这些模型的关联进行设置:

Post模型有许多评论

Comment模型属于 Post

而你的 config/routes.rb文件看起来像下面这个样子:

resources :postsresources :comments

这样救护将你的路由定义成下面这个样子:

http://localhost:3000/ posts

http://localhost:3000/ posts/1

http://localhost:3000/ posts/1/edit

http://localhost:3000/ comments

http://localhost:3000/ comments/1

http://localhost:3000/ comments/1/edit

这样还好,但不是一个好的实践。我们应该将 Comment的路由定义内嵌到 Post路由中。像这样:

resources :posts do resources :commentsend

现在这样就会将你的路由定义成下面这个样子:

http://localhost:3000/ posts

http://localhost:3000/ posts/1

http://localhost:3000/ posts/1/edit

http://localhost:3000/ posts/1/ comments

http://localhost:3000/ posts/1/ comments/1

http://localhost:3000/ posts/1/ comments/1/edit

URL 是可读的,并且可以预知评论是属于 ID 为 1 的一个 Post。这样做会有一个小麻烦:你必须将你对 Ruby 的表单和URL辅助器的使用方式变化一下。例如,在评论表单中,你得这样:

<%= form_for(@comment) do |f| %> <!-- form elements removed for brevity --><% end %>

这个需要作出变化。目前它持有 Comment的一个新的实例,我们需要将它修改成下面这样:

<%= form_for([@comment.post, @comment]) do |f| %> <!-- form elements removed for brevity --><% end %>

注意传入到 form_for 辅助器中的参数,它现在是一个数组了, 数组中首先包含了父资源,第二个则是 Comment实例。

另外一个我们要修改的是所有针对 Comments的URL 辅助器:

<%= link_to 'Show', comment %>

我们会做如下修改:

<%= link_to 'Show', [comment.post, comment] %>

这样你的展示链接就可以用了。下面来看看修改链接:

<%= link_to 'Edit', edit_comment_path(comment) %>

这个会做如下修改:

<%= link_to 'Edit', edit_post_comment_path(comment.post, comment) %>

注意! 辅助器的名称 (edit_post_comment_path) 以及参数 (两个参数而不是1个)都被修改了,以使其能运行于内嵌的资源/路由。

使用 Time.now 代替 Time.zone.now

一个最佳实践是 始终 在文件config/application.rb中 定义应用程序的默认时区,如下所示:

config.time_zone = ‘Eastern Time (US & Canada)'`.

Date.today和Time.now始终返回机器所在时区的本地日期和时间。使用Time.zone.now和Time.zone.today来避免开发机器和生产服务器之间的冲突是有意义的。

不要在视图中放置太多逻辑

视图是表示层,不应该包含逻辑。你应该 避免检查到如下情况:

<% if book.published? && book.published_at > 1.weeks.ago %> <span>Recently added</span><% end %>

或者

<% if current_user.roles.collect(&:name).include?("admin") || (user == book.owner && book.draft?) %> <%= link_to 'Delete', book, method: :delete, data: { confirm: 'Are you sure?' } %><% end %>

你可以做的是将这个条件检查移动到帮助模块,他们位于app/helpers中,并且在所有视图中自动可用。例如:

# app/view/helpers/application_helper.rbmodule ApplicationHelper def recently_added?(book) book.published? && book.published_at > 1.weeks.ago end # current_user is defined in application controller, which can be # accessed from helper modules & methods def can_delete?(book) current_user.roles.collect(&:name).include?("admin") || (user == book.owner && book.draft?) endend

将上述视图标记修改为:

<% if recently_added?(book) %> <span>Recently added</span><% end %>

<% if can_delete?(book) %> <%= link_to 'Delete', book, method: :delete, data: { confirm: 'Are you sure?' } %><% end %>

还有很多其他地方会用到can_delete?的方法,此处只是将逻辑和视图分开的示例。

总结

像我在文章开头讨论过的,如果项目按框架和社区定义的正确方式书写, 它能给我们带来便利。框架的最佳实践是由有经验的人开发的,他们克服实践中遇到的重重困难,然后总结开发出解决问题的这些实践。我们很高兴 社区能有这样一群人存在,并得以从他们的经验中获益。很幸运,Rails如此受欢迎,它有一个伟大的社区,这使的它越来越受欢迎。

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台