文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

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

毁灭之路

如果你忽略了一个Web应用程序框架的最佳实践,你就有游离框架之外运行的风险,而你甚至都没有察觉到。在最坏的情况下,您的应用程序会成为一个大麻烦,供你来处理,并且,会使新功能的开发,项目的维护以及开发者的引入变得非常困难。相信我,坚持最佳的实践能保持工作的高能和高效,避免你(或你的团队)在问题出现时焦头烂额,抓耳挠腮。

第 1 段(可获 1.83 积分)

荣耀之路

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

    1.可维护性
    2.可读性
    3.优雅
    4.快速发展
    5.DRY代码

让我们一起来了解下。

Ruby on Rails 社区风格指南:

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

第 2 段(可获 1.75 积分)

两个空格缩进

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

4 个空格缩进

def some_method
    some_var = true
    if some_var
        do_something
    else
        do_something_else
    end
end

2  个空格缩进

def some_method
  some_var = true
  if some_var
    do_something
  else
    do_something_else
  end
end

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

第 3 段(可获 0.78 积分)

用 a 来定义判断方法?

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

迭代: 使用each 而不是 for

第 4 段(可获 1.04 积分)

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

* for *

for i in 1..100
  ...
end

* each *

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

看到效果了吗?

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

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

if !true
  do_this
end

或者

if name != "sarmad"
  do_that
end

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

unless true
  do_this
end

或者

unless name == "sarmad"
  do_that
end

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

第 5 段(可获 0.83 积分)

错误的语句

unless user.save
  #throw error
else
  #return success
end

正确的语句

if user.save
  #return success
else
  #throw error
end

异常处理**

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

if user.gender == "male" && user.age > 17
  do_something
elsif user.gender == "male" && user.age < 17 && user.age > 5
  do_something_else
elsif user.age < 5
  raise StandardError
end

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

第 6 段(可获 0.64 积分)
raise StandardError if user.age < 5
if user.gender == "male" && user.age > 17
  do_something
elsif user.gender == "male" && user.age < 17 #we saved a redundant check here
  do_something_else
end

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

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

编写测试

如果你对Rails熟悉,那么你就会知道Rails社区对于测试有多么重视了。我曾经听人说过,做为一个新手,测试工作让Rails的学习变的困难。不过我也听有些人说,从一开始就这么做对于掌握Rails的基础知识(或者在一些场景下的web开发)有意义 。但是这些不会阻止测试成为软件开发中绝对最好的实践。事实上,我听到到有人抱怨,当你把测试加入到日常工作中,这将需要花费更多的时间来完成一个功能。但是,一旦他们进入到Rails的测试环节,并且从一开始就忍受编写测试程序带来的“麻烦”,那么他们实际上在第一时间就开始构建功能。 而且,这样做也覆盖了如此多的边缘场景,这些会促使我们项目中可以架构出更好的设计。一个优秀的Ruby开发者在测试上具有天生的优势。

 

第 7 段(可获 2.35 积分)

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

  • 测试如同一个功能或者一个应用程序的详细规范
  • 测试如同其他开发者的文档说明, 帮助他们理解你在程序实现中的意图
  • 测试帮助你事先捕获并修复bugs。
  • 测试给予你信心,当你在重构代码或者提高性能时不会中断任何程序。

DRY (不要做重复的事情)

做任何事情的时候确保你没有在做重复的事情, 避免你在做重复的事情。 让我们来讨论两种方式,Ruby面向对象的原则可以帮助你避免做重复的事情。
 
使用抽象类: 假如你有下面两个类:

第 8 段(可获 1.39 积分)
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"
  end
end

class 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"
  end
end

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

第 9 段(可获 0.71 积分)

理解了吗? 这样好多了!

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

class Newspaper
  def headline
    #code
  end

  def sports_news
    #code
  end

  def world_news
    #code
  end

  def price
    #code
  end
end

class Book
  def title
    #code
  end

  def read_page(page_number)
    #code
  end

  def price
    #code
  end

  def total_pages
    #code
  end
end
第 10 段(可获 0.7 积分)

我们需要给类增加一个 print 打印的方法,但又不想重复代码,那么可以用模块来实现:

module Printable
  def print
    #code
  end
end

接着修改类使它包含这个模块:

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
  end
end

class 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
  end
end
第 11 段(可获 0.38 积分)

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

枚举类型的巧妙使用

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

if book.status == "draft"
  do_something
elsif book.status == "completed"
  do_something
elsif book.status == "published"
  do_something
end

或者

if book.status == 0 #draft
  do_something
elsif book.status == 1 #completed
  do_something
elsif book.status == 2 #published
  do_something
end
第 12 段(可获 0.78 积分)

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

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

现在你可以将你的代码重新成如下:

if book.draft?
  do_something
elsif book.completed?
  do_something
elsif book.published?
  do_something
end

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

第 13 段(可获 0.9 积分)
  • 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 brevity
end
第 14 段(可获 0.81 积分)

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

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
  end
end

class 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 brevity
end
第 15 段(可获 0.14 积分)

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

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

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

  • 任何表示一个数据结构的类都应该作为一个 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,模块,或者类放到 lib/的 目录中去

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

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

第 16 段(可获 4.71 积分)

国际化/本地化

从一开始就要对你的应用程序国际化。不要把这件事留到以后再做,不然以后只会成为你的困扰。好的网站不会只支持一种语言,它们常常有更大的语言目标.。目标越大越好.最好的实践之一就是将国际化的思想贯穿整个开发过程。 这就是为什么Rails要坚持国际化理念。这表明国际化你的应用程序非常重要。更多有关的信息可以 看这里.

这里给你提供开箱即用的建议:

  • 对英语和其他类似语言的开箱即用的支持
  • 简化对其他语言的自定义和任何扩展
第 17 段(可获 1.35 积分)

它允许你设置一个默认的语言环境,也可以根据用户所属区域或偏好来进行更改。

这里有一个简短的示例,展示了如何将本地的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 应用程序默认都有一个 onfig/locales/en.yml 文件。这个文件负责保存对英语的翻译 。 如果你需要添加更多语言的翻译,你要添加与之匹配的本地文件,该文件后缀为.yml。在这个示例中我们仍然使用en.yml文件,让我们对上面的HTML代码进行国际化重构:

第 18 段(可获 1.33 积分)
<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.yml
en:
  title: "Books Listing"
  name: "Name"
  author: "Author"

数据库最佳实践

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

强烈建议将对该文件的检查加入到你的版本控制系统

第 19 段(可获 0.45 积分)

还有

如果你需要在另一个系统创建应用程序数据库,你应该使用 db:schema:load,而不是运行所有迁移数据。或者是一种有缺陷且不稳定的方法(越多迁移数据的积累,将运行的越慢,并且出现问题的可能性越大)

强烈的建议你将该文件的检查加入到你的版本控制系统。如果这个文件没有被检查到且没有更新,那么你将无法使用 rails db:schema:load 。 正如上文的解析,如果你需要在另一太机器上创建应用程序的数据库 ,那么你应该使用 db:schema:load 而不是 db:migrate.。运行所有迁移数据会使你变的气馁,因为它发展的趋势是随着时间推移会产生缺陷。我个人有过多次处理这种问题的经验。当迁移数据出现错误时,很难追踪出现问题的地方,在数据迁移的那里出现问题。 db:schema:load 是这中情况下的救星。

第 20 段(可获 1.34 积分)

注意!db:schema:load 仅用于你需要在一个新的系统上创建你的应用程序数据库的时候. 如果你添加一个新的数据迁移,你应该简化到只要让 db:migrate 来做这项工作。如果你在一台现有的且存储了数据的数据库上运行db:schema:load ,你的数据(可能是生产数据 )将会被清除。所以谨记下面三条简单的规则,你才能万事无忧。

  1. 当你添加或应用任何新的数据迁移的时候,在你的版本控制系统中要一直检查 schema.rb文件
  2. 当在一个新系统上创建应用程序数据库时使用 db:schema:load。
  3. 当你需要应用新迁移的数据时,在任何情况下都使用db:migrate。
第 21 段(可获 1.3 积分)

提示: 不要使用数据迁移来想数据库中添加数据。而使用 db/seeds.rb 来完成该目的.

嵌套的资源/路由

如果你有一个资源归属于另外一个资源,那么最好的办法是定义嵌套在父资源路由中的子资源的路由,例如,如果你拥有一个 Post 资源和一个 Comment 资源,且对模块的关联设置如下:

  • Post model has many comments
  • Comment model belongs to Post

而且你的config/routes.rb文件看起来像这样:

resources :posts
resources :comments

那么将会像这样来定义你的路由:

第 22 段(可获 1.23 积分)
  • 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 :comments
end

重新定义路由如下:

  • 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
第 23 段(可获 1.19 积分)

这URL是可读的且指明了Comments属于 Post ,post的ID是1。 这样做会有一个小问题:你必须在Ruby表单的用法和URL助手做出一些改变: . 举个例子,在comments表单,你是这样:

<%= 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实例。

第 24 段(可获 1.19 积分)

我们需要作出改变的另外的事情是针对 Comments 所有的URL助手:

  <%= link_to 'Show', comment %>

将会被改成这样:

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

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

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

 这里将会被改成这样:

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

注意! 两个助手的名字 (edit_post_comment_path) 和参数 (1个参数被取代为两个参数)都被修改了,目的是让其可以运行于嵌套的资源/路由

使用 Time.zone.now  来取代Time.now

第 25 段(可获 0.85 积分)

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

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

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

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

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

<% if book.published? && book.published_at > 1.weeks.ago %>
  <span>Recently added</span>
<% end %>
第 26 段(可获 0.98 积分)

或者

<% 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 %>

你能做的就是将这个条件检查移到一个 helper 模块中,这个模块位于 app/helpers ,它默认对所有视图有效。下面是具体的例子:

# app/view/helpers/application_helper.rb
module 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?)
  end
end
第 27 段(可获 0.4 积分)

修改上述视图标记为:

<% 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如此受欢迎,它有一个伟大的社区,这些会让我们爱上我们所做的事情。

我确信,很多其他方面的实践你认为非常重要,请在评论中让我们知道你的偏好。

第 28 段(可获 2.1 积分)

文章评论