├── CONTRIBUTING.md
├── README-zhCN.md
├── README-zhTW.md
└── README.md
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | 感謝你考慮貢獻,有幾點需要注意一下,謝謝:
2 |
3 | ## 發現新增或缺少的內容?
4 |
5 | 請從[英文原文](https://raw.github.com/bbatsov/rails-style-guide/master/README.md)拷貝至中文文件,再開始進行翻譯。
6 |
7 | 這樣省去了 markdown 格式及排版等問題。
8 |
9 | ## __在中文文章中,請在英數字前後加上一半形空格。__
10 |
11 | No:
12 |
13 | 有1個美女愛上2個男人。
14 |
15 | Yes:
16 |
17 | 有 1 個美女愛上 2 個男人。
18 |
19 | ## 中文請用中文的標點符號。
20 |
21 | `.`、`,` 不是中文的句號跟逗號。
22 |
23 | ## 請不要更改原文或添加原文裡沒有的內容。
24 |
25 | 欲添加新內容請至[上游](https://github.com/bbatsov/rails-style-guide)提交。
26 |
27 | ## 請先預覽修改後的內容,確定沒有忘記包起來的代碼塊。
28 |
29 | ## markdown 行內代碼請與中文分開。
30 |
31 | No:
32 |
33 | Ruby2.0的`prepend`方法。
34 |
35 | Yes:
36 |
37 | Ruby 2.0 的 `prepend` 方法。
38 |
39 | ## 跟代碼有關的都要用行內代碼包起來
40 |
41 | No:
42 |
43 | 'x'
44 |
45 | Yes:
46 |
47 | `'x'`
48 |
49 | ## 避免一個 Pull Request 裡面包含多個提交。
50 |
51 | 參考:https://github.com/JuanitoFatas/ruby-style-guide/pull/13
52 |
53 | 多個提交我要手動 Merge,很難 review。
54 |
55 | 謝謝,
56 |
57 | _Juanito Fatas_
58 |
--------------------------------------------------------------------------------
/README-zhCN.md:
--------------------------------------------------------------------------------
1 | # 序幕
2 |
3 | > 榜样很重要。
4 | > -- 《机械战警》 Alex J. Murphy 警官
5 |
6 | 这份指南旨在提供一系列 Ruby on Rails 4 开发的最佳实践和风格惯例。本指南与社区驱动并制定的 [Ruby 编码风格指南](https://github.com/bbatsov/ruby-style-guide)可以互为补充。
7 |
8 | 本文中的一些建议只适用于 Rails 4.0+ 版本。
9 |
10 | 你可以使用 [Transmuter](https://github.com/TechnoGate/transmuter) 来生成本文的 PDF 或 HTML 版本。
11 |
12 | 本指南同时有以下语言的翻译版:
13 |
14 | * [英文原版](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README.md)
15 | * [繁體中文](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md)
16 | * [日语](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md)
17 | * [俄语](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md)
18 | * [土耳其语](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md)
19 |
20 | # Rails 风格指南
21 |
22 | 这份 Rails 风格指南推荐的是 Rails 的最佳实践,现实世界中的 Rails 程序员据此可以写出可维护的高质量代码。我们只说实际使用中的用法。指南再好,但里面说的过于理想化结果大家拒绝使用或者可能根本没人用,又有何意义。
23 |
24 | 本指南分为几个小节,每一小节由几条相关的规则构成。我尽力在每条规则后面说明理由(如果省略了说明,那是因为其理由显而易见)。
25 |
26 | 这些规则不是我凭空想象出来的——它们中的绝大部分来自我多年以来作为职业软件工程师的经验,来自 Rails 社区成员的反馈和建议,以及许多备受推崇的 Rails 编程资源。
27 |
28 | ## 目录
29 |
30 | * [配置](#配置)
31 | * [路由](#路由)
32 | * [控制器](#控制器)
33 | * [模型](#模型)
34 | * [ActiveRecord](#activerecord)
35 | * [ActiveRecord 查询](#activerecord-查询)
36 | * [迁移](#迁移)
37 | * [视图](#视图)
38 | * [国际化](#国际化)
39 | * [Assets](#assets)
40 | * [Mailers](#mailers)
41 | * [Time](#time)
42 | * [Bundler](#bundler)
43 | * [有缺陷的 Gem](#有缺陷的-gem)
44 | * [进程管理](#进程管理)
45 |
46 | ## 配置
47 |
48 | *
49 | 自定义的初始化代码应放在 `config/initializers` 目录下。 Initializers 目录中的代码在应用启动时被执行。
50 | [[link](#config-initializers)]
51 |
52 | *
53 | 每个 gem 的初始化代码应放在单独的文件中,并且文件名应与 gem 的名称相同。例如: `carrierwave.rb`, `active_admin.rb`。
54 | [[link](#gem-initializers)]
55 |
56 | *
57 | 相应地调整开发环境、测试环境及生产环境的配置(修改 `config/environments/` 目录下对应的文件)
58 | [[link](#dev-test-prod-configs)]
59 |
60 | * 添加需要预编译的额外静态资源文件(如果有的话):
61 |
62 | ```Ruby
63 | # config/environments/production.rb
64 | # 预编译额外的静态资源文件(application.js, application.css, 以及所有已经被加入的非 JS 或 CSS 的文件)
65 | config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js )
66 | ```
67 |
68 | *
69 | 将所有环境下都通用的配置放在 `config/application.rb` 文件中。
70 | [[link](#app-config)]
71 |
72 | *
73 | 创建一个与生产环境高度相似的 `staging` 环境。
74 | [[link](#staging-like-prod)]
75 |
76 | *
77 | 其它配置应保存在 YAML 文件中,存放在 `config/` 目录下。
78 | [[link](#yaml-config)]
79 |
80 | 从 Rails 4.2 开始,可以通过 `config_for` 这个新方法轻松地加载 YAML 配置文件:
81 |
82 | ```Ruby
83 | Rails::Application.config_for(:yaml_file)
84 | ```
85 |
86 | ## 路由
87 |
88 | *
89 | 当需要为一个 RESTful 资源添加动作时(你真的需要吗?),应使用 `member` 路由和 `collection` 路由。
90 | [[link](#member-collection-routes)]
91 |
92 | ```Ruby
93 | # 差
94 | get 'subscriptions/:id/unsubscribe'
95 | resources :subscriptions
96 |
97 | # 好
98 | resources :subscriptions do
99 | get 'unsubscribe', on: :member
100 | end
101 |
102 | # 差
103 | get 'photos/search'
104 | resources :photos
105 |
106 | # 好
107 | resources :photos do
108 | get 'search', on: :collection
109 | end
110 | ```
111 |
112 | *
113 | 当需要定义多个 `member/collection` 路由时,应使用块结构。
114 | [[link](#many-member-collection-routes)]
115 |
116 | ```Ruby
117 | resources :subscriptions do
118 | member do
119 | get 'unsubscribe'
120 | # 更多路由
121 | end
122 | end
123 |
124 | resources :photos do
125 | collection do
126 | get 'search'
127 | # 更多路由
128 | end
129 | end
130 | ```
131 |
132 | *
133 | 使用嵌套路由(nested routes),它可以更有效地表现 ActiveRecord 模型之间的关系。
134 | [[link](#nested-routes)]
135 |
136 | ```Ruby
137 | class Post < ActiveRecord::Base
138 | has_many :comments
139 | end
140 |
141 | class Comments < ActiveRecord::Base
142 | belongs_to :post
143 | end
144 |
145 | # routes.rb
146 | resources :posts do
147 | resources :comments
148 | end
149 | ```
150 |
151 | *
152 | 使用命名空间路由来归类相关的动作。
153 | [[link](#namespaced-routes)]
154 |
155 | ```Ruby
156 | namespace :admin do
157 | # 将请求 /admin/products/* 交由 Admin::ProductsController 处理
158 | # (app/controllers/admin/products_controller.rb)
159 | resources :products
160 | end
161 | ```
162 |
163 | *
164 | 不要使用旧式的控制器路由。这种路由会让控制器的所有动作都通过 GET 请求调用。
165 | [[link](#no-wild-routes)]
166 |
167 | ```Ruby
168 | # 非常差
169 | match ':controller(/:action(/:id(.:format)))'
170 | ```
171 |
172 | *
173 | 不要使用 `match` 来定义任何路由,除非确实需要将多种请求映射到某个动作,这时可以通过 `via` 选项来指定请求类型,如 `[:get, :post, :patch, :put, :delete]`。
174 | [[link](#no-match-routes)]
175 |
176 | ## 控制器
177 |
178 | *
179 | 控制器应该保持苗条 ― 它们应该只为视图层提供数据,不应包含任何业务逻辑(所有业务逻辑都应当放在模型里)。
180 | [[link](#skinny-controllers)]
181 |
182 | *
183 | 每个控制器的动作(理论上)应当只调用一个除了初始的 find 或 new 之外的方法。
184 | [[link](#one-method)]
185 |
186 | *
187 | 控制器与视图之间共享不超过两个实例变量。
188 | [[link](#shared-instance-variables)]
189 |
190 | ## 模型
191 |
192 | *
193 | 自由地引入不是 ActiveRecord 的模型类。
194 | [[link](#model-classes)]
195 |
196 | *
197 | 模型的命名应有意义(但简短)且不含缩写。
198 | [[link](#meaningful-model-names)]
199 |
200 | *
201 | 如果需要模型类有与 ActiveRecord 类似的行为(如验证),但又不想有 ActiveRecord 的数据库功能,应使用 [ActiveAttr](https://github.com/cgriego/active_attr) 这个 gem。
202 | [[link](#activeattr-gem)]
203 |
204 | ```Ruby
205 | class Message
206 | include ActiveAttr::Model
207 |
208 | attribute :name
209 | attribute :email
210 | attribute :content
211 | attribute :priority
212 |
213 | attr_accessible :name, :email, :content
214 |
215 | validates_presence_of :name
216 | validates_format_of :email, :with => /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i
217 | validates_length_of :content, :maximum => 500
218 | end
219 | ```
220 |
221 | 更完整的示例请参考 [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr)。
222 |
223 | ### ActiveRecord
224 |
225 | *
226 | 避免改动缺省的 ActiveRecord 惯例(表的名字、主键等),除非你有一个充分的理由(比如,不受你控制的数据库)。
227 | [[link](#keep-ar-defaults)]
228 |
229 | ```Ruby
230 | # 差 - 如果你能更改数据库的 schema,那就不要这样写
231 | class Transaction < ActiveRecord::Base
232 | self.table_name = 'order'
233 | ...
234 | end
235 | ```
236 |
237 | *
238 | 把宏风格的方法调用(`has_many`, `validates` 等)放在类定义语句的最前面。
239 | [[link](#macro-style-methods)]
240 |
241 | ```Ruby
242 | class User < ActiveRecord::Base
243 | # 默认的 scope 放在最前(如果有的话)
244 | default_scope { where(active: true) }
245 |
246 | # 接下来是常量初始化
247 | COLORS = %w(red green blue)
248 |
249 | # 然后是 attr 相关的宏
250 | attr_accessor :formatted_date_of_birth
251 |
252 | attr_accessible :login, :first_name, :last_name, :email, :password
253 |
254 | # 紧接着是与关联有关的宏
255 | belongs_to :country
256 |
257 | has_many :authentications, dependent: :destroy
258 |
259 | # 以及与验证有关的宏
260 | validates :email, presence: true
261 | validates :username, presence: true
262 | validates :username, uniqueness: { case_sensitive: false }
263 | validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ }
264 | validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true}
265 |
266 | # 下面是回调方法
267 | before_save :cook
268 | before_save :update_username_lower
269 |
270 | # 其它的宏(如 devise)应放在回调方法之后
271 |
272 | ...
273 | end
274 | ```
275 |
276 | *
277 | `has_many :through` 优于 `has_and_belongs_to_many`。 使用 `has_many :through` 允许 join 模型有附加的属性及验证。
278 | [[link](#has-many-through)]
279 |
280 | ```Ruby
281 | # 不太好 - 使用 has_and_belongs_to_many
282 | class User < ActiveRecord::Base
283 | has_and_belongs_to_many :groups
284 | end
285 |
286 | class Group < ActiveRecord::Base
287 | has_and_belongs_to_many :users
288 | end
289 |
290 | # 更好 - 使用 has_many :through
291 | class User < ActiveRecord::Base
292 | has_many :memberships
293 | has_many :groups, through: :memberships
294 | end
295 |
296 | class Membership < ActiveRecord::Base
297 | belongs_to :user
298 | belongs_to :group
299 | end
300 |
301 | class Group < ActiveRecord::Base
302 | has_many :memberships
303 | has_many :users, through: :memberships
304 | end
305 | ```
306 |
307 | *
308 | `self[:attribute]` 比 `read_attribute(:attribute)` 更好。
309 | [[link](#read-attribute)]
310 |
311 | ```Ruby
312 | # 差
313 | def amount
314 | read_attribute(:amount) * 100
315 | end
316 |
317 | # 好
318 | def amount
319 | self[:amount] * 100
320 | end
321 | ```
322 |
323 | *
324 | `self[:attribute] = value` 优于 `write_attribute(:attribute, value)`。
325 | [[link](#write-attribute)]
326 |
327 | ```Ruby
328 | # 差
329 | def amount
330 | write_attribute(:amount, 100)
331 | end
332 |
333 | # 好
334 | def amount
335 | self[:amount] = 100
336 | end
337 | ```
338 |
339 | *
340 | 总是使用新式的 ["sexy"
341 | 验证](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/)。
342 | [[link](#sexy-validations)]
343 |
344 | ```Ruby
345 | # 差
346 | validates_presence_of :email
347 | validates_length_of :email, maximum: 100
348 |
349 | # 好
350 | validates :email, presence: true, length: { maximum: 100 }
351 | ```
352 |
353 | *
354 | 当一个自定义的验证规则使用次数超过一次时,或该验证规则是基于正则表达式时,应该创建一个自定义的验证规则文件。
355 | [[link](#custom-validator-file)]
356 |
357 | ```Ruby
358 | # 差
359 | class Person
360 | validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
361 | end
362 |
363 | # 好
364 | class EmailValidator < ActiveModel::EachValidator
365 | def validate_each(record, attribute, value)
366 | record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
367 | end
368 | end
369 |
370 | class Person
371 | validates :email, email: true
372 | end
373 | ```
374 |
375 | *
376 | 自定义验证规则应放在 `app/validators` 目录下。
377 | [[link](#app-validators)]
378 |
379 | *
380 | 如果你在维护数个相关的应用,或验证规则本身足够通用,可以考虑将自定义的验证规则抽象为一个共用的 gem。
381 | [[link](#custom-validators-gem)]
382 |
383 | *
384 | 自由地使用命名 scope。
385 | [[link](#named-scopes)]
386 |
387 | ```Ruby
388 | class User < ActiveRecord::Base
389 | scope :active, -> { where(active: true) }
390 | scope :inactive, -> { where(active: false) }
391 |
392 | scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }
393 | end
394 | ```
395 |
396 | *
397 | 当一个由 lambda 和参数定义的命名 scope 太过复杂时,
398 | 更好的方式是创建一个具有同样用途并返回 `ActiveRecord::Relation` 对象的类方法。这很可能让 scope 更加精简。
399 | [[link](#named-scope-class)]
400 |
401 | ```Ruby
402 | class User < ActiveRecord::Base
403 | def self.with_orders
404 | joins(:orders).select('distinct(users.id)')
405 | end
406 | end
407 | ```
408 |
409 | 注意这种方式不允许命名 scope 那样的链式调用。例如:
410 |
411 | ```Ruby
412 | # 不能链式调用
413 | class User < ActiveRecord::Base
414 | def User.old
415 | where('age > ?', 80)
416 | end
417 |
418 | def User.heavy
419 | where('weight > ?', 200)
420 | end
421 | end
422 | ```
423 |
424 | 这种方式下 `old` 和 `heavy` 可以单独工作,但不能执行 `User.old.heavy`。
425 | 若要链式调用,请使用下面的代码:
426 |
427 | ```Ruby
428 | # 可以链式调用
429 | class User < ActiveRecord::Base
430 | scope :old, -> { where('age > 60') }
431 | scope :heavy, -> { where('weight > 200') }
432 | end
433 | ```
434 |
435 | *
436 | 注意
437 | [`update_attribute`](http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute)
438 | 方法的行为。它不运行模型验证(与 `update_attributes` 不同),因此可能弄乱模型的状态。
439 | [[link](#beware-update-attribute)]
440 |
441 | *
442 | 应使用对用户友好的 URL。URL 中应显示模型的一些具有描述性的属性,而不是仅仅显示 `id`。有多种方法可以达到这个目的:
443 | [[link](#user-friendly-urls)]
444 |
445 | * 重写模型的 `to_param` 方法。Rails 使用该方法为对象创建 URL。该方法默认会以字符串形式返回记录的 `id` 项。
446 | 可以重写该方法以包含其它可读性强的属性。
447 |
448 | ```Ruby
449 | class Person
450 | def to_param
451 | "#{id} #{name}".parameterize
452 | end
453 | end
454 | ```
455 |
456 | 为了将结果转换为一个 URL 友好的值,字符串应该调用 `parameterize` 方法。
457 | 对象的 `id` 属性值需要位于 URL 的开头,以便使用 ActiveRecord 的 `find` 方法查找对象。
458 |
459 | * 使用 `friendly_id` 这个 gem。它允许使用对象的一些描述性属性而非 `id` 来创建可读性强的 URL。
460 |
461 | ```Ruby
462 | class Person
463 | extend FriendlyId
464 | friendly_id :name, use: :slugged
465 | end
466 | ```
467 |
468 | 查看 [gem documentation](https://github.com/norman/friendly_id) 以获得更多 `friendly_id` 的使用信息。
469 |
470 | *
471 | 应使用 `find_each` 来迭代一系列 ActiveRecord 对象。用循环来处理数据库中的记录集(如 `all` 方法)是非常低效率的,因为循环试图一次性得到所有对象。而批处理方法允许一批批地处理记录,因此需要占用的内存大幅减少。
472 | [[link](#find-each)]
473 |
474 | ```Ruby
475 | # 差
476 | Person.all.each do |person|
477 | person.do_awesome_stuff
478 | end
479 |
480 | Person.where('age > 21').each do |person|
481 | person.party_all_night!
482 | end
483 |
484 | # 好
485 | Person.find_each do |person|
486 | person.do_awesome_stuff
487 | end
488 |
489 | Person.where('age > 21').find_each do |person|
490 | person.party_all_night!
491 | end
492 | ```
493 |
494 | *
495 | 因为 [Rails 为有依赖关系的关联添加了回调方法](https://github.com/rails/rails/issues/3458),应总是调用
496 | `before_destroy` 回调方法,调用该方法并启用 `prepend: true` 选项会执行验证。
497 | [[link](#before_destroy)]
498 |
499 | ```Ruby
500 | # 差——即使 super_admin 返回 true,roles 也会自动删除
501 | has_many :roles, dependent: :destroy
502 |
503 | before_destroy :ensure_deletable
504 |
505 | def ensure_deletable
506 | fail "Cannot delete super admin." if super_admin?
507 | end
508 |
509 | # 好
510 | has_many :roles, dependent: :destroy
511 |
512 | before_destroy :ensure_deletable, prepend: true
513 |
514 | def ensure_deletable
515 | fail "Cannot delete super admin." if super_admin?
516 | end
517 | ```
518 |
519 | ### ActiveRecord 查询
520 |
521 | *
522 | 不要在查询中使用字符串插值,它会使你的代码有被 SQL 注入攻击的风险。
523 | [[link](#avoid-interpolation)]
524 |
525 | ```Ruby
526 | # 差——插值的参数不会被转义
527 | Client.where("orders_count = #{params[:orders]}")
528 |
529 | # 好——参数会被适当转义
530 | Client.where('orders_count = ?', params[:orders])
531 | ```
532 |
533 | *
534 | 当查询中有超过 1 个占位符时,应考虑使用名称占位符,而非位置占位符。
535 | [[link](#named-placeholder)]
536 |
537 | ```Ruby
538 | # 一般般
539 | Client.where(
540 | 'created_at >= ? AND created_at <= ?',
541 | params[:start_date], params[:end_date]
542 | )
543 |
544 | # 好
545 | Client.where(
546 | 'created_at >= :start_date AND created_at <= :end_date',
547 | start_date: params[:start_date], end_date: params[:end_date]
548 | )
549 | ```
550 |
551 | *
552 | 当只需要通过 id 查询单个记录时,优先使用 `find` 而不是 `where`。
553 | [[link](#find)]
554 |
555 | ```Ruby
556 | # 差
557 | User.where(id: id).take
558 |
559 | # 好
560 | User.find(id)
561 | ```
562 |
563 | *
564 | 当只需要通过属性查询单个记录时,优先使用 `find_by` 而不是 `where`。
565 | [[link](#find_by)]
566 |
567 | ```Ruby
568 | # 差
569 | User.where(first_name: 'Bruce', last_name: 'Wayne').first
570 |
571 | # 好
572 | User.find_by(first_name: 'Bruce', last_name: 'Wayne')
573 | ```
574 |
575 | *
576 | 当需要处理多条记录时,应使用 `find_each`。
577 | [[link](#find_each)]
578 |
579 | ```Ruby
580 | # 差——一次性加载所有记录
581 | # 当 users 表有成千上万条记录时,非常低效
582 | User.all.each do |user|
583 | NewsMailer.weekly(user).deliver_now
584 | end
585 |
586 | # 好——分批检索记录
587 | User.find_each do |user|
588 | NewsMailer.weekly(user).deliver_now
589 | end
590 | ```
591 |
592 | *
593 | `where.not` 比书写 SQL 更好。
594 | [[link](#where-not)]
595 |
596 | ```Ruby
597 | # 差
598 | User.where("id != ?", id)
599 |
600 | # 好
601 | User.where.not(id: id)
602 | ```
603 |
604 | ## 迁移
605 |
606 | *
607 | 应使用版本控制工具记录 `schema.rb` (或 `structure.sql` )的变化。
608 | [[link](#schema-version)]
609 |
610 | *
611 | 应使用 `rake db:scheme:load` 而不是 `rake db:migrate` 来初始化空数据库。
612 | [[link](#db-schema-load)]
613 |
614 | *
615 | 应在迁移文件中设置默认值,而不是在应用层面设置。
616 | [[link](#default-migration-values)]
617 |
618 | ```Ruby
619 | # 差——在应用中设置默认值
620 | def amount
621 | self[:amount] or 0
622 | end
623 | ```
624 |
625 | 虽然许多 Rails 开发者建议在 Rails 中强制使用表的默认值,但这会使数据受到许多应用 bug 的影响,因而导致应用极其难以维护。考虑到大多数有一定规模的 Rails 应用都与其它应用共享数据库,保持应用的数据完整性几乎是不可能的。
626 |
627 | *
628 | 务必使用外键约束。在 Rails 4.2 中,ActiveRecord 本身已经支持外键约束。
629 | [[link](#foreign-key-constraints)]
630 |
631 | *
632 | 书写建设性的迁移(添加表或列)时,应使用 `change` 方法而不是 `up` 或 `down` 方法。
633 | [[link](#change-vs-up-down)]
634 |
635 | ```Ruby
636 | # 老式写法
637 | class AddNameToPeople < ActiveRecord::Migration
638 | def up
639 | add_column :people, :name, :string
640 | end
641 |
642 | def down
643 | remove_column :people, :name
644 | end
645 | end
646 |
647 | # 新式写法(更好)
648 | class AddNameToPeople < ActiveRecord::Migration
649 | def change
650 | add_column :people, :name, :string
651 | end
652 | end
653 | ```
654 |
655 | *
656 | 不要在迁移中使用模型类。由于模型的变化,模型类也一直处在变化当中,过去运行正常的迁移可能不知什么时候就不能正常进行了。
657 | [[link](#no-model-class-migrations)]
658 |
659 | ## 视图
660 |
661 | *
662 | 不要直接从视图调用模型层。
663 | [[link](#no-direct-model-view)]
664 |
665 | *
666 | 复杂的格式化不应放在视图中,而应提取为视图 helper 或模型中的方法。
667 | [[link](#no-complex-view-formatting)]
668 |
669 | *
670 | 应使用 partial 模版与布局来减少代码重复。
671 | [[link](#partials)]
672 |
673 | ## 国际化
674 |
675 | *
676 | 不应在视图、模型或控制器里添加语言相关的设置,应在 `config/locales` 目录下进行设置。
677 | [[link](#locale-texts)]
678 |
679 | *
680 | 当 ActiveRecord 模型的标签需要被翻译时,应使用`activerecord` scope:
681 | [[link](#translated-labels)]
682 |
683 | ```
684 | en:
685 | activerecord:
686 | models:
687 | user: Member
688 | attributes:
689 | user:
690 | name: "Full name"
691 | ```
692 |
693 | 然后 `User.model_name.human` 会返回 "Member" ,而 `User.human_attribute_name("name")` 会返回 "Full name"。这些属性的翻译会作为视图中的标签。
694 |
695 |
696 | *
697 | 把在视图中使用的文字与 ActiveRecord 的属性翻译分开。把模型使用的语言文件放在 `models` 目录下,把视图使用的文字放在 `views` 目录下。
698 | [[link](#organize-locale-files)]
699 |
700 | * 当使用额外目录来设置语言文件时,应在 `application.rb` 文件里列出这些目录以加载设置。
701 |
702 | ```Ruby
703 | # config/application.rb
704 | config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
705 | ```
706 |
707 | *
708 | 把共享的本地化选项,如日期或货币格式,放在 `locales` 的根目录下。
709 | [[link](#shared-localization)]
710 |
711 | *
712 | 应使用精简形式的 I18n 方法:
713 | 使用 `I18n.t` 而非 `I18n.translate`;
714 | 使用 `I18n.l` 而非 `I18n.localize`。
715 | [[link](#short-i18n)]
716 |
717 | *
718 | 应使用 "懒惰" 查询来获取视图中使用的文本。假设我们有以下结构:
719 | [[link](#lazy-lookup)]
720 |
721 | ```
722 | en:
723 | users:
724 | show:
725 | title: "User details page"
726 | ```
727 |
728 | `users.show.title` 的数值能这样被 `app/views/users/show.html.haml` 获取:
729 |
730 | ```Ruby
731 | = t '.title'
732 | ```
733 |
734 | *
735 | 应在控制器与模型中使用点分隔的键,而非指定 `:scope` 选项。点分隔的调用更容易阅读,也更易追踪层级关系。
736 | [[link](#dot-separated-keys)]
737 |
738 | ```Ruby
739 | # 差
740 | I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages]
741 |
742 | # 好
743 | I18n.t 'activerecord.errors.messages.record_invalid'
744 | ```
745 |
746 | *
747 | 更详细的 Rails i18n 信息可以在 [Rails Guides](http://guides.rubyonrails.org/i18n.html) 找到。
748 | [[link](#i18n-guides)]
749 |
750 | ## Assets
751 |
752 | 应使用 [assets pipeline](http://guides.rubyonrails.org/asset_pipeline.html) 来管理应用的资源结构。
753 |
754 | *
755 | 自定义的样式表、JavaScript 文件或图片文件,应放在 `app/assets` 目录下。
756 | [[link](#reserve-app-assets)]
757 |
758 | *
759 | 把自己开发但不好归类的库文件,应放在 `lib/assets/` 目录下。
760 | [[link](#lib-assets)]
761 |
762 | *
763 | 第三方代码,如 [jQuery](http://jquery.com/) 或 [bootstrap](http://twitter.github.com/bootstrap/),应放在 `vendor/assets` 目录下。
764 | [[link](#vendor-assets)]
765 |
766 | *
767 | 尽可能使用资源的 gem 版。例如:
768 | [jquery-rails](https://github.com/rails/jquery-rails),
769 | [jquery-ui-rails](https://github.com/joliss/jquery-ui-rails),
770 | [bootstrap-sass](https://github.com/thomas-mcdonald/bootstrap-sass),
771 | [zurb-foundation](https://github.com/zurb/foundation)
772 | [[link](#gem-assets)]
773 |
774 | ## Mailers
775 |
776 | *
777 | 应将 mailer 命名为 `SomethingMailer`。若没有 Mailer 后缀,不能立即断定它是否为一个 mailer,也不能断定哪个视图与它有关。
778 | [[link](#mailer-name)]
779 |
780 | *
781 | 提供 HTML 与纯文本两份视图模版。
782 | [[link](#html-plain-email)]
783 |
784 | *
785 | 在开发环境下应显示发信失败错误。这些错误默认是关闭的。
786 | [[link](#enable-delivery-errors)]
787 |
788 | ```Ruby
789 | # config/environments/development.rb
790 |
791 | config.action_mailer.raise_delivery_errors = true
792 | ```
793 |
794 | *
795 | 在开发环境下使用诸如 [Mailcatcher](https://github.com/sj26/mailcatcher) 的本地 SMTP 服务器。
796 | [[link](#local-smtp)]
797 |
798 | ```Ruby
799 | # config/environments/development.rb
800 |
801 | config.action_mailer.smtp_settings = {
802 | address: 'localhost',
803 | port: 1025,
804 | # 更多设置
805 | }
806 | ```
807 |
808 | *
809 | 为域名设置默认项。
810 | [[link](#default-hostname)]
811 |
812 | ```Ruby
813 | # config/environments/development.rb
814 | config.action_mailer.default_url_options = { host: "#{local_ip}:3000" }
815 |
816 | # config/environments/production.rb
817 | config.action_mailer.default_url_options = { host: 'your_site.com' }
818 |
819 | # 在 mailer 类中
820 | default_url_options[:host] = 'your_site.com'
821 | ```
822 |
823 | *
824 | 若需要在邮件中添加到网站的超链接,应总是使用 `_url` 方法,而非
825 | `_path` 方法。`_url` 方法产生的超链接包含域名,而 `_path`
826 | 方法产生相对链接。
827 | [[link](#url-not-path-in-email)]
828 |
829 | ```Ruby
830 | # 差
831 | You can always find more info about this course
832 | <%= link_to 'here', course_path(@course) %>
833 |
834 | # 好
835 | You can always find more info about this course
836 | <%= link_to 'here', course_url(@course) %>
837 | ```
838 |
839 |
840 | *
841 | 正确地设置寄件人与收件人地址的格式。应使用下列格式:
842 | [[link](#email-addresses)]
843 |
844 | ```Ruby
845 | # 在你的 mailer 类中
846 | default from: 'Your Name '
847 | ```
848 |
849 | *
850 | 确保测试环境下的 email 发送方法设置为 `test`:
851 | [[link](#delivery-method-test)]
852 |
853 | ```Ruby
854 | # config/environments/test.rb
855 |
856 | config.action_mailer.delivery_method = :test
857 | ```
858 |
859 | *
860 | 开发环境和生产环境下的发送方法应设置为 `smtp`:
861 | [[link](#delivery-method-smtp)]
862 |
863 | ```Ruby
864 | # config/environments/development.rb, config/environments/production.rb
865 |
866 | config.action_mailer.delivery_method = :smtp
867 | ```
868 |
869 | *
870 | 当发送 HTML 邮件时,所有样式应为行内样式,这是因为某些客户端不能正确显示外部样式。然而,这使得邮件难以维护理并会导致代码重复。有两个类似的 gem 可以转换样式,并将样式放在对应的 html 标签里: [premailer-rails3](https://github.com/fphilipe/premailer-rails3) 和
871 | [roadie](https://github.com/Mange/roadie)。
872 | [[link](#inline-email-styles)]
873 |
874 | *
875 | 避免在产生页面响应的同时发送邮件。若有多个邮件需要发送,这会导致页面加载延迟甚至请求超时。有鉴于此,应使用 [sidekiq](https://github.com/mperham/sidekiq) 这个 gem 在后台发送邮件。
876 | [[link](#background-email)]
877 |
878 | ## Time
879 |
880 | *
881 | 在 `application.rb` 里设置相应的时区。
882 | [[link](#time-now)]
883 |
884 | ```Ruby
885 | config.time_zone = 'Eastern European Time'
886 | # 可选配置——注意取值只能是 :utc 或 :local 中的一个(默认为 :utc)
887 | config.active_record.default_timezone = :local
888 | ```
889 |
890 | *
891 | 不要使用 `Time.parse`。
892 | [[link](#time-parse)]
893 |
894 | ```Ruby
895 | # 差
896 | Time.parse('2015-03-02 19:05:37') # => 会假设时间是基于操作系统的时区。
897 |
898 | # 好
899 | Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00
900 | ```
901 |
902 | *
903 | 不要使用 `Time.now`。
904 | [[link](#time-now)]
905 |
906 | ```Ruby
907 | # 差
908 | Time.now # => 无视所配置的时区,返回操作系统时间。
909 |
910 | # 好
911 | Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00
912 | Time.current # 结果同上,但更简洁
913 | ```
914 |
915 | ## Bundler
916 |
917 | *
918 | 只在开发环境或测试环境下使用的 gem 应进行适当的分组。
919 | [[link](#dev-test-gems)]
920 |
921 | *
922 | 在项目中只使用广为人知的 gem。如果你考虑引入某些鲜为人所知的 gem,应该先仔细检查一下其源代码。
923 | [[link](#only-good-gems)]
924 |
925 | *
926 | 关于多个开发者使用不同操作系统的项目,与操作系统有关的 gem 默认情况下会产生经常变动的 `Gemfile.lock`。 在 Gemfile 文件里,所有与 OS X 相关的 gem 放在 `darwin` 群组,而所有与 Linux 有关的 gem 应放在 `linux` 群组:
927 | [[link](#os-specific-gemfile-locks)]
928 |
929 | ```Ruby
930 | # Gemfile
931 | group :darwin do
932 | gem 'rb-fsevent'
933 | gem 'growl'
934 | end
935 |
936 | group :linux do
937 | gem 'rb-inotify'
938 | end
939 | ```
940 |
941 | 要在正确的环境下加载合适的 gem,需添加以下代码至 `config/application.rb` :
942 |
943 | ```Ruby
944 | platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym
945 | Bundler.require(platform)
946 | ```
947 |
948 | *
949 | 不要把 `Gemfile.lock` 文件从版本控制里移除。这可不是一个随机产生的文件——它的目的是确保你所有的团队成员执行 `bundle install` 时,获得相同版本的 gem 。
950 | [[link](#gemfile-lock)]
951 |
952 | ## 有缺陷的 Gem
953 |
954 | 这是一个有问题的或有更好替代物的 gem 列表。不要在项目中使用它们。
955 |
956 | * [rmagick](http://rmagick.rubyforge.org/) - 这个 gem 因大量消耗内存而臭名昭著。应使用 [minimagick](https://github.com/probablycorey/mini_magick) 来替代它。
957 |
958 | * [autotest](http://www.zenspider.com/ZSS/Products/ZenTest/) - 测试自动化的过时方案,远不及 [guard](https://github.com/guard/guard) 和 [watchr](https://github.com/mynyml/watchr)。
959 |
960 | * [rcov](https://github.com/relevance/rcov) - 代码覆盖率工具,不兼容 Ruby 1.9。应使用 [SimpleCov](https://github.com/colszowka/simplecov) 来替代它。
961 |
962 | * [therubyracer](https://github.com/cowboyd/therubyracer) - 内存杀手,强烈不建议在生产环境中使用。建议使用 `node.js` 来替代它。
963 |
964 | 这仍是一个完善中的列表,欢迎添加流行但有缺陷的 gem。
965 |
966 | ## 进程管理
967 |
968 | *
969 | 如果项目依赖各种外界的进程,应使用 [foreman](https://github.com/ddollar/foreman) 来管理它们。
970 | [[link](#foreman)]
971 |
972 | # 延伸阅读
973 |
974 | 以下是几个极好的讲述 Rails 风格的资源,闲暇时可以考虑延伸阅读:
975 |
976 | * [The Rails 4 Way](http://www.amazon.com/The-Rails-Addison-Wesley-Professional-Ruby/dp/0321944275)
977 | * [Ruby on Rails Guides](http://guides.rubyonrails.org/)
978 | * [The RSpec Book](http://pragprog.com/book/achbd/the-rspec-book)
979 | * [The Cucumber Book](http://pragprog.com/book/hwcuc/the-cucumber-book)
980 | * [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec)
981 | * [Better Specs for RSpec](http://betterspecs.org)
982 |
983 | # 贡献
984 |
985 | 本指南的每条建议都不是定案。我渴望与对 Rails 编码风格有兴趣的大家一起协作,创造出一份对整个 Ruby 社区都有益的资源。
986 |
987 | 欢迎 open tickets 或发送带有改进的 pull request。在此提前感谢您的帮助!
988 |
989 | 您可以通过 [gittip](https://www.gittip.com/bbatsov) 对本项目(以及 RuboCop 项目)进行捐赠支持。
990 |
991 | [](https://www.gittip.com/bbatsov)
992 |
993 | ## 如何贡献?
994 |
995 | 只需遵循[贡献指南](https://github.com/bbatsov/rails-style-guide/blob/master/CONTRIBUTING.md)即可。
996 |
997 | # 许可证
998 |
999 | 
1000 | This work is licensed under a [Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/deed.zh)
1001 |
1002 | # 口耳相传
1003 |
1004 | 一份社区驱动的风格指南,若不为人所知,那有何用。请在微博转发这份指南,分享给你的朋友或同事。我们得到的每个评论、建议或意见都可以让这份指南变得更好一点。而我们想要拥有的是尽可能好的指南,不是吗?
1005 |
1006 | 共勉之,
1007 | [Bozhidar](https://twitter.com/bbatsov)
1008 |
--------------------------------------------------------------------------------
/README-zhTW.md:
--------------------------------------------------------------------------------
1 | # 序幕
2 |
3 | > Role models are important.
4 | > -- 機器戰警 Alex J. Murphy
5 |
6 | 這份指南目的於示範一整套 Rails 3 開發的風格慣例及最佳實踐。這是一份與由現存社群所驅動的 [Ruby 程式碼風格指南](https://github.com/bbatsov/ruby-style-guide)互補的指南。
7 |
8 | 而本指南中[測試 Rails 應用](#testing)小節擺在[開發 Rails 應用](#developing)之後,因為我相信[行為驅動開發](http://en.wikipedia.org/wiki/Behavior_Driven_Development)
9 | (BDD) 是最佳的軟體開發之道。銘記在心吧。
10 |
11 | Rails 是一個堅持己見的框架,而這也是一份堅持己見的指南。在我的心裡,我堅信 [RSpec](https://www.relishapp.com/rspec) 優於 Test::Unit,[Sass](http://sass-lang.com/) 優於 CSS 以及
12 | [Haml](http://haml-lang.com/),([Slim](http://slim-lang.com/)) 優於 Erb。所以不要期望在這裡找到 Test::Unit, CSS 及 Erb 的忠告。
13 |
14 | 某些忠告僅適用於 Rails 3.1+ 以上版本。
15 |
16 | 你可以使用 [Transmuter](https://github.com/TechnoGate/transmuter) 來產生本指南的一份 PDF 或 HTML 複本。
17 |
18 | 本指南被翻譯成下列語言:
19 |
20 | * [简体中文](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md)
21 | * [英文原版](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README.md)
22 | * [德文](https://github.com/arbox/de-rails-style-guide/blob/master/README-deDE.md)
23 | * [日文](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md)
24 | * [俄文](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md)
25 | * [土耳其文](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md)
26 | * [韓文](https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md)
27 |
28 | # 目錄
29 |
30 | * [開發 Rails 應用程式](#開發-rails-應用程式)
31 | * [設定檔 (Config)](#設定檔-config)
32 | * [路由 (Routes)](#路由-routes)
33 | * [控制器 (Controller)](#控制器-controller)
34 | * [資料模型 (Model)](#資料模型-model)
35 | * [遷移 (Migrations)](#遷移-migrations)
36 | * [視圖 (Views)](#視圖-views)
37 | * [國際化 (I18n)](#國際化-i18n)
38 | * [資產 (Assets)](#資產-assets)
39 | * [郵件 (Mailers)](#郵件-mailers)
40 | * [打包 (Bundler)](#打包-bundler)
41 | * [貴重的 Gems](#貴重的-gems)
42 | * [缺陷的 Gems](#有缺陷的-gems)
43 | * [管理處理程序 (Process)](#管理處理程序-process)
44 | * [測試 Rails 應用程式](#測試-rails-應用程式)
45 | * [Cucumber](#cucumber)
46 | * [RSpec](#rspec)
47 |
48 |
49 | # 開發 Rails 應用程式
50 |
51 | ## 設定檔 (config)
52 |
53 | * 把自訂的初始化程式碼放在 `config/initializers`。在 initializers 內的程式碼會在應用程式啟動時執行。
54 | * 每一個 gem 相關的初始化程式碼應當使用同樣的名稱,放在不同的文件裡,如: `carrierwave.rb`, `active_admin.rb`, 等等。
55 | * 為開發、測試及生產環境分別調整設定(在 `config/environments/` 下對應的文件)
56 | * 標記額外的資產 (assets) 給預編譯(如果有的話):
57 |
58 | ```Ruby
59 | # config/environments/production.rb
60 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
61 | config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js )
62 | ```
63 |
64 | * 將所有環境都通用的設定檔放在 `config/application.rb` 檔案。
65 | * 另外開一個與生產環境(production enviroment)幾乎相同的 `staging` 環境。
66 |
67 | ## 路由 (Routes)
68 |
69 | * 當你需要加入一個或多個動作 (action) 至一個 RESTful 資源時(你真的需要嗎?),使用 `member` and `collection` 路由。
70 |
71 | ```Ruby
72 | # 劣
73 | get 'subscriptions/:id/unsubscribe'
74 | resources :subscriptions
75 |
76 | # 優
77 | resources :subscriptions do
78 | get 'unsubscribe', on: :member
79 | end
80 |
81 | # 劣
82 | get 'photos/search'
83 | resources :photos
84 |
85 | # 優
86 | resources :photos do
87 | get 'search', on: :collection
88 | end
89 | ```
90 |
91 | * 若需要定義多個 `member/collection` 路由,請改用區塊語法(block syntax)。
92 |
93 | ```Ruby
94 | resources :subscriptions do
95 | member do
96 | get 'unsubscribe'
97 | # 更多路由
98 | end
99 | end
100 |
101 | resources :photos do
102 | collection do
103 | get 'search'
104 | # 更多路由
105 | end
106 | end
107 | ```
108 |
109 | * 使用巢狀路由(nested routes)來更佳地表達各 ActiveRecord 資料模型之間的關係。
110 |
111 | ```Ruby
112 | class Post < ActiveRecord::Base
113 | has_many :comments
114 | end
115 |
116 | class Comments < ActiveRecord::Base
117 | belongs_to :post
118 | end
119 |
120 | # routes.rb
121 | resources :posts do
122 | resources :comments
123 | end
124 | ```
125 |
126 | * 使用命名空間路由來分類相關的行為。
127 |
128 | ```Ruby
129 | namespace :admin do
130 | # Directs /admin/products/* to Admin::ProductsController
131 | # (app/controllers/admin/products_controller.rb)
132 | resources :products
133 | end
134 | ```
135 |
136 | * 不要使用地圖砲路由。這種路由會讓每個控制器的動作透過 GET 請求存取。
137 |
138 | ```Ruby
139 | # 極劣
140 | match ':controller(/:action(/:id(.:format)))'
141 | ```
142 |
143 | ## 控制器 (Controller)
144 |
145 | * 讓你的控制器保持苗條──它們應該只替視圖層取出資料且不包含任何業務邏輯(所有業務邏輯應當放在資料模型裡)。
146 | * 每個控制器裡的動作 (action) 應當只呼叫一個除了初始的 find 或 new 以外的方法(理想狀態)。
147 | * 控制器與視圖之間共享不超過兩個實體變數 (instance variable)。
148 |
149 | ## 資料模型 (Model)
150 |
151 | * 請任意引入不是 ActiveRecord 的資料模型。
152 | * 替資料模型命名有意義(但簡短)且不帶縮寫的名字。
153 | * 如果你需要普通的資料模型有著 ActiveRecord 的行為,比方說驗證,可使用 [ActiveAttr](https://github.com/cgriego/active_attr) gem。
154 |
155 | ```Ruby
156 | class Message
157 | include ActiveAttr::Model
158 |
159 | attribute :name
160 | attribute :email
161 | attribute :content
162 | attribute :priority
163 |
164 | attr_accessible :name, :email, :content
165 |
166 | validates_presence_of :name
167 | validates_format_of :email, :with => /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i
168 | validates_length_of :content, :maximum => 500
169 | end
170 | ```
171 |
172 | 更完整的範例,參考 [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr)。
173 |
174 | ### ActiveRecord
175 |
176 | * 避免改動預設的 ActiveRecord(表的名字、主鍵,等等),除非你有一個非常好的理由(像是不受你控制的資料庫)。
177 | * 把巨集風格的方法放在類別定義的前面(`has_many`, `validates`, 等等)。
178 |
179 | ```Ruby
180 | class User < ActiveRecord::Base
181 | # 預設的 scope 放在最前面(如果有的話)
182 | default_scope { where(active: true) }
183 |
184 | # 接下來是常數
185 | GENDERS = %w(male female)
186 |
187 | # 然後放一些 attr 相關的巨集
188 | attr_accessor :formatted_date_of_birth
189 |
190 | attr_accessible :login, :first_name, :last_name, :email, :password
191 |
192 | # 緊接著是關聯的巨集
193 | belongs_to :country
194 |
195 | has_many :authentications, dependent: :destroy
196 |
197 | # 以及巨集的驗證
198 | validates :email, presence: true
199 | validates :username, presence: true
200 | validates :username, uniqueness: { case_sensitive: false }
201 | validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ }
202 | validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true}
203 |
204 | # 接著是回呼
205 | before_save :cook
206 | before_save :update_username_lower
207 |
208 | # 其它的巨集 (像 devise 的) 應該放在回呼的後面
209 |
210 | ...
211 | end
212 | ```
213 |
214 | * 偏好 `has_many :through` 勝於 `has_and_belongs_to_many`。使用 `has_many :through` 允許在 join 資料模型有附加的屬性及驗證
215 |
216 | ```Ruby
217 | # 使用 has_and_belongs_to_many
218 | class User < ActiveRecord::Base
219 | has_and_belongs_to_many :groups
220 | end
221 |
222 | class Group < ActiveRecord::Base
223 | has_and_belongs_to_many :users
224 | end
225 |
226 | # 建議的寫法 - 使用 has_many :through
227 | class User < ActiveRecord::Base
228 | has_many :memberships
229 | has_many :groups, through: :memberships
230 | end
231 |
232 | class Membership < ActiveRecord::Base
233 | belongs_to :user
234 | belongs_to :group
235 | end
236 |
237 | class Group < ActiveRecord::Base
238 | has_many :memberships
239 | has_many :users, through: :memberships
240 | end
241 | ```
242 |
243 | * 務必使用新的 ["sexy" validation](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/)。
244 | * 如果一個自訂的驗證程序使用超過一次,或驗證程序是透過某個正則表達式的時候,請建立一個自訂的 validator 檔。
245 |
246 | ```Ruby
247 | # 劣
248 | class Person
249 | validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
250 | end
251 |
252 | # 優
253 | class EmailValidator < ActiveModel::EachValidator
254 | def validate_each(record, attribute, value)
255 | record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
256 | end
257 | end
258 |
259 | class Person
260 | validates :email, email: true
261 | end
262 | ```
263 |
264 | * 所有自訂的驗證器應放在一個共享的 gem 。
265 | * 可任意使用具名的作用域 (scope)。
266 |
267 | ```Ruby
268 | class User < ActiveRecord::Base
269 | scope :active, -> { where(active: true) }
270 | scope :inactive, -> { where(active: false) }
271 |
272 | scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }
273 | end
274 | ```
275 |
276 | * 將具名的作用域包在 `lambda` 裡使其延遲初始化。
277 |
278 | ```Ruby
279 | # 劣
280 | class User < ActiveRecord::Base
281 | scope :active, where(active: true)
282 | scope :inactive, where(active: false)
283 |
284 | scope :with_orders, joins(:orders).select('distinct(users.id)')
285 | end
286 |
287 | # 優
288 | class User < ActiveRecord::Base
289 | scope :active, -> { where(active: true) }
290 | scope :inactive, -> { where(active: false) }
291 |
292 | scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }
293 | end
294 | ```
295 |
296 | *按:在 Rails 4 會強制使用 lambda*
297 |
298 | * 當一個由 lambda 及參數定義的作用域變得過於複雜時,更好的方式是建立一個作為同樣用途的類別方法,並回傳一個 `ActiveRecord::Relation` 物件。你也可以這麼定義更精簡的作用域。
299 |
300 | ```Ruby
301 | class User < ActiveRecord::Base
302 | def self.with_orders
303 | joins(:orders).select('distinct(users.id)')
304 | end
305 | end
306 | ```
307 |
308 | * 注意 `update_attribute` 方法的行為。它不會執行資料模型驗證(不同於 `update_attributes` )並且可能把資料模型狀態給搞砸。
309 | * 使用用戶友好的網址。在網址顯示具描述性的資料模型屬性,而不只是 `id` 。
310 | 有不止一種方法可以達成:
311 | * 覆寫資料模型的 `to_param` 方法。這是 Rails 用來給物件建立網址的方法。預設的實作會以字串形式回傳該 `id` 的記錄。它可以用另一個人類可讀的屬性來覆寫。
312 |
313 | ```Ruby
314 | class Person
315 | def to_param
316 | "#{id} #{name}".parameterize
317 | end
318 | end
319 | ```
320 |
321 | 為了要轉換成對網址友好 (URL-friendly)的值,字串應當呼叫 `parameterize` 。物件的 `id` 要放在開頭,以便給 ActiveRecord 的 `find` 方法查找。
322 |
323 | * 使用 `friendly_id` gem。它允許藉由某些具描述性的資料模型屬性來建立人類可讀的網址,而不是用 `id` 。
324 |
325 | ```Ruby
326 | class Person
327 | extend FriendlyId
328 | friendly_id :name, use: :slugged
329 | end
330 | ```
331 |
332 | 查看 [gem 說明文件](https://github.com/norman/friendly_id)獲得更多關於使用的資訊。
333 |
334 | ### ActiveResource
335 |
336 | * 如果 HTTP 回應是一個與現有的格式(XML 和 JSON)不同的格式,或是需要某些額外的格式解析,這時候請建立一個自訂格式,並在類別中使用它。自訂格式應當實作下列方法:`extension`, `mime_type`,
337 | `encode` 以及 `decode`。
338 |
339 | ```Ruby
340 | module ActiveResource
341 | module Formats
342 | module Extend
343 | module CSVFormat
344 | extend self
345 |
346 | def extension
347 | 'csv'
348 | end
349 |
350 | def mime_type
351 | 'text/csv'
352 | end
353 |
354 | def encode(hash, options = nil)
355 | # 資料以新格式編碼並回傳
356 | end
357 |
358 | def decode(csv)
359 | # 資料以新格式解碼並回傳
360 | end
361 | end
362 | end
363 | end
364 | end
365 |
366 | class User < ActiveResource::Base
367 | self.format = ActiveResource::Formats::Extend::CSVFormat
368 |
369 | ...
370 | end
371 | ```
372 |
373 | * 若要讓產生的網址不包含副檔名,請覆寫 `ActiveResource::Base` 的 `element_path` 及 `collection_path` 方法,並移除副檔名。
374 |
375 | ```Ruby
376 | class User < ActiveResource::Base
377 | ...
378 |
379 | def self.collection_path(prefix_options = {}, query_options = nil)
380 | prefix_options, query_options = split_options(prefix_options) if query_options.nil?
381 | "#{prefix(prefix_options)}#{collection_name}#{query_string(query_options)}"
382 | end
383 |
384 | def self.element_path(id, prefix_options = {}, query_options = nil)
385 | prefix_options, query_options = split_options(prefix_options) if query_options.nil?
386 | "#{prefix(prefix_options)}#{collection_name}/#{URI.parser.escape id.to_s}#{query_string(query_options)}"
387 | end
388 | end
389 | ```
390 |
391 | 如有任何改動網址的需求時,這些方法也可以被覆寫。
392 |
393 | ## 遷移 (Migrations)
394 |
395 | * 把 `schema.rb` 放進版本控制系統裡面。
396 | * 用 `rake db:scheme:load` 來初始化空的資料庫,而不是用 `rake db:migrate`。
397 | * 用 `rake db:test:prepare` 來更新測試資料庫的 schema。
398 | * 避免在資料表裡放預設資料。請使用資料模型層來取代。
399 |
400 | ```Ruby
401 | def amount
402 | self[:amount] or 0
403 | end
404 | ```
405 |
406 | 然而 `self[:attr_name]` 卻相當常見,你也可以考慮使用更繁瑣的 `read_attribute` 來取代(有爭議,但更好讀):
407 |
408 | ```Ruby
409 | def amount
410 | read_attribute(:amount) or 0
411 | end
412 | ```
413 |
414 | * 在寫建設性的遷移時(加表格或加欄位),請使用 Rails 3.1 的新方式 - 使用 `change` 方法取代 `up` 與 `down` 方法。
415 |
416 |
417 | ```Ruby
418 | # 以前的寫法
419 | class AddNameToPerson < ActiveRecord::Migration
420 | def up
421 | add_column :persons, :name, :string
422 | end
423 |
424 | def down
425 | remove_column :person, :name
426 | end
427 | end
428 |
429 | # 推薦的新寫法
430 | class AddNameToPerson < ActiveRecord::Migration
431 | def change
432 | add_column :persons, :name, :string
433 | end
434 | end
435 | ```
436 |
437 | ## 視圖 (Views)
438 |
439 | * 不要直接從視圖呼叫資料模型層 (Model)。
440 | * 不要在視圖裡做複雜的格式化,把它們寫成方法丟到 helper 或 model 裡面。
441 | * 使用 partial view 與佈局 (layouts) 來減少重複的程式碼。
442 | * 給自訂的檢驗器 (validators) 加上 [瀏覽器端的驗證器](https://github.com/bcardarella/client_side_validations)。方法如下:
443 | * 宣告一個由 `ClientSideValidations::Middleware::Base` 繼承來的自訂 validator
444 |
445 | ```Ruby
446 | module ClientSideValidations::Middleware
447 | class Email < Base
448 | def response
449 | if request.params[:email] =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
450 | self.status = 200
451 | else
452 | self.status = 404
453 | end
454 | super
455 | end
456 | end
457 | end
458 | ```
459 |
460 | * 開新檔案
461 | `public/javascripts/rails.validations.custom.js.coffee` 並且包進 `application.js.coffee` 裡面:
462 |
463 | ```Ruby
464 | # app/assets/javascripts/application.js.coffee
465 | #= require rails.validations.custom
466 | ```
467 |
468 | * 加上瀏覽器端的驗證器:
469 |
470 | ```Ruby
471 | #public/javascripts/rails.validations.custom.js.coffee
472 | clientSideValidations.validators.remote['email'] = (element, options) ->
473 | if $.ajax({
474 | url: '/validators/email.json',
475 | data: { email: element.val() },
476 | async: false
477 | }).status == 404
478 | return options.message || 'invalid e-mail format'
479 | ```
480 |
481 | ## 國際化 (I18n)
482 |
483 | * 視圖、資料模型與控制器裡都不應該使用特定語言的設定值或字串。這些文字應搬到在 `config/locales` 下的語系檔裡。
484 | * 要翻譯 ActiveRecord 資料模型的文字標籤時,請使用 `activerecord` 作用域:
485 |
486 | ```
487 | zh-TW:
488 | activerecord:
489 | models:
490 | user: "會員"
491 | attributes:
492 | user:
493 | name: "全名"
494 | ```
495 |
496 | 這樣子 `User.model_name.human` 會回傳 "會員" ,而 `User.human_attribute_name("name")` 會回傳 "全名"。這些屬性的翻譯會被視圖作為標籤使用。
497 |
498 | * 把在視圖使用的文字與 ActiveRecord 的屬性翻譯分別放在不同的資料夾。把給資料模型使用的語系檔放在名為 `models` 的資料夾,給視圖使用的文字放在名為 `views` 的資料夾。
499 | * 把額外的語系檔放進各別資料夾之後,要在 `application.rb` 檔裡面指定這些資料夾,才能載入。
500 |
501 | ```Ruby
502 | # config/application.rb
503 | config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
504 | ```
505 |
506 | * 把共用的語系選項,像是日期或貨幣格式,直接放在 `locale` 資料夾底下。
507 | * 請使用精簡形式的 I18n 方法: `I18n.t` ,而不是 `I18n.translate` ;使用 `I18n.l` ,而不是 `I18n.localize`。
508 | * 使用「懶惰法」來查詢視圖中使用的文字。假設我們有以下結構:
509 |
510 | ```
511 | zh-TW:
512 | users:
513 | show:
514 | title: "使用者詳細資料"
515 | ```
516 |
517 | `users.show.title` 的值在 `app/views/users/show.html.haml` 裡面可以這樣子查到:
518 |
519 | ```Ruby
520 | = t '.title'
521 | ```
522 |
523 | * 在控制器與資料模型使用「點分隔」的 key,來取代指定 `:scope` 選項。點分隔的呼叫方式,更容易閱讀及追蹤層級。
524 |
525 | ```Ruby
526 | # 這樣子呼叫
527 | I18n.t 'activerecord.errors.messages.record_invalid'
528 |
529 | # 而不是這樣
530 | I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages]
531 | ```
532 |
533 | * 關於 Rails i18n 更詳細的資訊可以在 [Rails Guides](http://guides.rubyonrails.org/i18n.html) 找到。
534 |
535 | ## 資產 (Assets)
536 |
537 | 在應用程式裡,利用 [Assets Pipeline](http://guides.rubyonrails.org/asset_pipeline.html) 來組織資產。
538 |
539 | * 保留 `app/assets` 給自定的樣式表、Javascripts 或圖片。
540 | * 把自己開發,但不屬於應用程式本身的函式庫,放在 `lib/assets`。
541 | * 第三方程式如 [jQuery](http://jquery.com/) 或 [bootstrap](http://twitter.github.com/bootstrap/) 應放在`vendor/assets`。
542 | * 盡可能使用包成 gem 的 assets 。 (如: [jquery-rails](https://github.com/rails/jquery-rails))。
543 |
544 | ## 郵件 (Mailers)
545 |
546 | * 把 mails 命名為 `SomethingMailer`。沒有 Mailer 結尾的話,不能一望即知誰是 Mailer,以及跟哪個視圖有關。
547 | * 要同時提供 HTML 與純文字的視圖模版。
548 | * 在你的開發環境打開寄信失敗時要拋出錯誤的選項。這些錯誤預設是不會拋出的。
549 |
550 | ```Ruby
551 | # config/environments/development.rb
552 |
553 | config.action_mailer.raise_delivery_errors = true
554 | ```
555 |
556 | * 在開發環境使用 `smtp.gmail.com` 設定 SMTP 伺服器(當然了,除非你自己有本機 SMTP 伺服器)。
557 |
558 | ```Ruby
559 | # config/environments/development.rb
560 |
561 | config.action_mailer.smtp_settings = {
562 | address: 'smtp.gmail.com',
563 | # 更多設定
564 | }
565 | ```
566 |
567 | * 要提供預設的主機名稱 (hostname)。
568 |
569 | ```Ruby
570 | # config/environments/development.rb
571 | config.action_mailer.default_url_options = {host: "#{local_ip}:3000"}
572 |
573 |
574 | # config/environments/production.rb
575 | config.action_mailer.default_url_options = {host: 'your_site.com'}
576 |
577 | # 在你的 mailer 類別
578 | default_url_options[:host] = 'your_site.com'
579 | ```
580 |
581 | * 如果你需要在 email 裡設超連結到你的網站,務必使用 `_url` 方法,而不是 `_path` 方法。 `_url` 方法包含了主機名稱,而 `_path` 方法則沒有。
582 |
583 | ```Ruby
584 | # 錯誤
585 | You can always find more info about this course
586 | = link_to 'here', url_for(course_path(@course))
587 |
588 | # 正確
589 | You can always find more info about this course
590 | = link_to 'here', url_for(course_url(@course))
591 | ```
592 |
593 | * 應把寄件人與收件人地址的格式給寫正確。格式如下:
594 |
595 | ```Ruby
596 | # 在你的 mailer 類別
597 | default from: 'Your Name '
598 | ```
599 |
600 | * 確認測試環境的 email 發送方法設定為 `test` :
601 |
602 | ```Ruby
603 | # config/environments/test.rb
604 |
605 | config.action_mailer.delivery_method = :test
606 | ```
607 |
608 | * 開發環境與生產環境的發送方法應為 `smtp` :
609 |
610 | ```Ruby
611 | # config/environments/development.rb, config/environments/production.rb
612 |
613 | config.action_mailer.delivery_method = :smtp
614 | ```
615 |
616 | * 當發送 HTML email 時,所有樣式應為行內樣式 (inline style),這是由於某些 email 軟體處理外部樣式表會有問題。雖然這樣會讓程式更難維護、程式碼也容易重覆。有兩個 gem 可以把樣式表轉換成行內樣式,並將放在對應的 html 標籤裡: [premailer-rails3](https://github.com/fphilipe/premailer-rails3) 和 [roadie](https:// github.com/Mange/roadie)。
617 |
618 | * 應避免在發送回應的同時同步寄出 email,因為這樣會造成網頁載入時間過久、而且要是寄送多個 email 還可能會造成連線逾時。請使用 [delayed_job](https://github.com/tobi/delayed_job) gem 來把寄送 email 放到背景去處理。
619 |
620 | ## 打包 (Bundler)
621 |
622 | * 把只給開發環境或測試環境的 gem 在 Gemfile 檔裡面妥善分組。
623 | * 在你的專案中只使用公認的 gem。如果你考慮引入某些鮮為人所知的 gem ,你應該先仔細審查它的原始碼。
624 | * 要是開發人員各自使用不同的作業系統,那麼與作業系統相關的那些 gem 會導致 `Gemfile.lock` 經常變動。解決方法是,在 Gemfile 裡,把與 OS X 相關的 gem 放在 `darwin` 群組,與 Linux 相關的 gem 放在 `linux` 群組:
625 |
626 | ```Ruby
627 | # Gemfile
628 | group :darwin do
629 | gem 'rb-fsevent'
630 | gem 'growl'
631 | end
632 |
633 | group :linux do
634 | gem 'rb-inotify'
635 | end
636 | ```
637 |
638 | 要在正確的環境 require 正確的 gem,請新增以下程式碼至 `config/application.rb` :
639 |
640 | ```Ruby
641 | platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym
642 | Bundler.require(platform)
643 | ```
644 |
645 | * 不要把 `Gemfile.lock` 檔從版本控制系統裡移出。這不是隨機產生的文件──它確保你所有的成員執行 `bundle install` 時,都拿到相同版本的 gem 。
646 |
647 | ## 貴重的 Gems
648 |
649 | 一個最重要的程式設計理念是「不要重造輪子!」。若你遇到一個特定問題,你應該要在你開始手刻之前,找一下是否有現有的解決方案。以下是一些在很多 Rails 專案中的「無價至寶」 gem 列表(全部相容 Rails 3.1):
650 |
651 | * [active_admin](https://github.com/gregbell/active_admin) - 有了 ActiveAdmin,建立 Rails 應用的管理介面就像兒戲。你會有一個很好的後台,圖形化 CRUD 介面以及更多東西。非常靈活且可客製化。
652 | * [better_errors](https://github.com/charliesome/better_errors) - Better Errors 用更好更有效的錯誤頁面,取代了 Rails 標準的錯誤頁面。不僅可用在 Rails,任何將 Rack 當作 middleware 的 app 都可使用。
653 | * [bullet](https://github.com/flyerhzm/bullet) - Bullet 就是為了幫助你提升應用程式的效能而打造的 gem (藉由減少資料庫查詢)。會在你開發應用程式時,替你注意你的資料庫查詢,並在需要 eager loading (N+1 查詢) 時、或你在不必要的情況使用 eager loading 時,或是在應該要使用 counter cache 時,都會提醒你。
654 | * [cancan](https://github.com/ryanb/cancan) - CanCan 是一個權限管理的 gem,
655 | 讓你可以管制用戶可存取的資源。所有的權限都定義在一個檔案裡(ability.rb),並提供許多方便的方法,讓你在整個應用程式裡都可以檢查及確保權限是否獲准。
656 | * [capybara](https://github.com/jnicklas/capybara) - Capybara 旨在簡化整合測試 Rack 應用程式的流程,像是 Rails、Sinatra 或 Merb。 Capybara 模擬了真實用戶使用 web 應用程式的互動過程。它與你用的測試工具無關,並原生搭載 Rack::Test 及 Selenium 支援。透過外部 gem 支援 HtmlUnit、WebKit 及 env.js 。與 RSpec & Cucumber 一起使用時工作良好。
657 | * [carrierwave](https://github.com/jnicklas/carrierwave) - Rails 最新的檔案上傳的解決方案。支援上傳檔案到本地儲存與雲端儲存(及很多其它的酷玩意)。良好結合了 ImageMagick 來進行圖片後處理。
658 | * [client_side_validations](https://github.com/bcardarella/client_side_validations) -
659 | 一個很棒的 gem,替你從現有的伺服器端資料模型驗證,自動產生 Javascript 瀏覽器端驗證。強烈推薦!
660 | * [compass-rails](https://github.com/chriseppstein/compass) - 一個優秀的 gem,加入了某些 css 框架的支持。包括了一些 sass mixin ,讓你減少 css 檔的程式碼,並幫你解決瀏覽器相容問題。
661 | * [cucumber-rails](https://github.com/cucumber/cucumber-rails) - Cucumber 是一個由 Ruby 所寫,開發功能測試的頂級工具。 cucumber-rails 提供了 Cucumber 的 Rails 整合。
662 | * [devise](https://github.com/plataformatec/devise) - Devise 是 Rails 應用程式的登入系統完整解決方案。多數情況偏好使用 devise 來開始客製登入流程。
663 | * [fabrication](http://fabricationgem.org/) - 一個很好的 fixture 測資替代品(編輯推薦)。
664 | * [factory_girl](https://github.com/thoughtbot/factory_girl) - Fabrication 的替代品。一個成熟的 fixture 測資產生器。 Fabrication 的精神領袖先驅。
665 | * [ffaker](https://github.com/EmmanuelOga/ffaker) - 產生假資料的實用 gem(名字、地址,等等)。
666 | * [feedzirra](https://github.com/pauldix/feedzirra) - 非常快速、靈活的 RSS / Atom Feed 解析器。
667 | * [friendly_id](https://github.com/norman/friendly_id) - 透過使用某些具描述性的資料模型屬性,而不是使用 id,來讓你建立人類可讀的網址。
668 | * [globalize3](https://github.com/svenfuchs/globalize3.git) - Globalize3 是 Globalize Gem 的後繼者,針對 ActiveRecord 3.x 設計。基於新的 I18n API 打造而成,並幫 ActiveRecord 資料模型新增了交易功能 (transaction)。
669 | * [guard](https://github.com/guard/guard) - 監控檔案變化並呼叫任務的極佳 gem。搭載了很多實用的擴充。樂勝 autotest 與 [watchr](https://github.com/mynyml/watchr)。
670 | * [haml-rails](https://github.com/indirect/haml-rails) - haml-rails 提供了 Haml 的 Rails 整合。
671 | * [haml](http://haml-lang.com) - Haml 是一個簡潔的資料模型語言,被很多人認為(包括我)遠優於Erb。
672 | * [kaminari](https://github.com/amatsuda/kaminari) - 很棒的分頁解決方案。
673 | * [machinist](https://github.com/notahat/machinist) - fixture 測資不好玩,Machinist 才好玩。
674 | * [rspec-rails](https://github.com/rspec/rspec-rails) - RSpec 是 Test::MiniTest 的替代品。我不高度推薦 RSpec。 rspec-rails 提供了 RSpec 的 Rails 整合。
675 | * [simple_form](https://github.com/plataformatec/simple_form) - 一旦用過 simple_form(或 formatastic),你就回不去 Rails 預設的表單產生器了。它提供很棒的 DSL 可以建立表單,讓你不必在意表單的 HTML 怎麼寫。
676 | * [simplecov-rcov](https://github.com/fguillen/simplecov-rcov) - 為了 SimpleCov 打造的 RCov formatter。若你想使用 SimpleCov 搭配 Hudson 持續整合伺服器 (CI Server),它很有用。
677 | * [simplecov](https://github.com/colszowka/simplecov) - 檢查程式碼覆蓋率 (code coverage) 的工具。但不像 RCov,它完全相容 Ruby 1.9。它有精美的報表。必須用!
678 | * [slim](http://slim-lang.com) - Slim 是一個簡潔的模版語言,被視為是遠遠優於 HAML 的程式語言 (至於 Erb 就不用說了) 。唯一會阻止我大規模地使用它的是,主流 IDE 及編輯器對它的支援不好。但它的效能是非凡的。
679 | * [spork](https://github.com/sporkrb/spork) - 一個給測試框架(RSpec / Cucumber)用的 DRb 伺服器,每次運行前確保 fork 出一個乾淨的測試狀態。簡單的說,預載很多測試環境的結果是大幅降低你的測試啟動時間,絕對必須用!
680 | * [sunspot](https://github.com/sunspot/sunspot) - 基於 SOLR 的全文搜尋引擎。
681 |
682 | 這不是完整的清單,其它的 gem 也可以在之後加進來。以上清單上的所有 gems 皆經測試,處於活躍開發階段,有社群,程式碼的品質很高。
683 |
684 | ## 有缺陷的 Gems
685 |
686 | 這是一個有問題的或被別的 gem 取代的 gem 清單。你應該在你的專案裡避免使用它們。
687 |
688 | * [rmagick](http://rmagick.rubyforge.org/) - 這個 gem 因大量消耗記憶體而聲名狼藉。請改用 [minimagick](https://github.com/probablycorey/mini_magick)。
689 | * [autotest](http://www.zenspider.com/ZSS/Products/ZenTest/) - 自動化測試的舊方法。遠不如 guard 及 [watchr](https://github.com/mynyml/watchr)。
690 | * [rcov](https://github.com/relevance/rcov) - 程式碼覆蓋率工具,不相容於 Ruby 1.9。請改用 [SimpleCov](https://github.com/colszowka/simplecov)。
691 | * [therubyracer](https://github.com/cowboyd/therubyracer) - 極度不鼓勵在生產模式使用這個 gem,它會消耗大量的記憶體。我會推薦改用 `node.js`。
692 |
693 | 這仍是一個完善中的清單。請告訴我受人歡迎但有缺陷的 gems 。
694 |
695 | ## 管理處理程序 (process)
696 |
697 | * 若你的專案依賴各種外部的處理程序,使用 [foreman](https://github.com/ddollar/foreman) 來管理它們。
698 |
699 | # 測試 Rails 應用程式
700 |
701 | 也許 BDD 方法是實作一個新功能最好的方法。你從開始寫一些高階的測試(通常使用 Cucumber),然後使用這些測試來驅使你實作功能。一開始你給功能的視圖寫測試,並使用這些測試來建立相關的視圖。接著,你寫控制器測試要求把資料丟給視圖用,藉此來實作控制器。最後你實作資料模型的測試,以及資料模型自身。
702 |
703 | ## Cucumber
704 |
705 | * 用 `@wip` (工作進行中)標籤來標記尚未完成的情境 (scenario)。這些情境將不納入考慮,且不會被標記為測試失敗。當完成這個情境且功能測試通過時,為了把此情境加至測試套件裡,請移除 `@wip` 標籤。
706 | * 修改預設的 profile,排除掉標記為 `@javascript` 的情境。它們將使用瀏覽器來測試,建議停用它們來增進一般情境的執行速度。
707 | * 替標記著 `@javascript` 的情境,設定另一個 profile。
708 | * profile 可在 `cucumber.yml` 檔案裡設定。
709 |
710 | ```Ruby
711 | # profile 的定義:
712 | profile_name: --tags @tag_name
713 | ```
714 |
715 | * 用這個指令來執行特定的 profile:
716 |
717 | ```
718 | cucumber -p profile_name
719 | ```
720 |
721 | * 若使用 [fabrication](http://fabricationgem.org/) 來替換 fixtures,請使用預先定義的 [fabrication steps](http://fabricationgem.org/#!cucumber-steps)。
722 | * 不要使用舊的 `web_steps.rb` 來定義步驟 ,用這個會導致多餘的情境,這些情境無法正確反映出應用程式的領域。
723 | * 當檢查一元素的可見文字時(如超連結、按鈕),請檢查元素的文字而不是檢查 id。這樣可以查出 i18n 的問題。
724 | * 為同物件的各種功能,各自建立不同的 feature:
725 |
726 | ```Ruby
727 | # 劣
728 | Feature: Articles
729 | # ... 功能實作 ...
730 |
731 | # 優
732 | Feature: Article Editing
733 | # ... 功能實作 ...
734 |
735 | Feature: Article Publishing
736 | # ... 功能實作 ...
737 |
738 | Feature: Article Search
739 | # ... 功能實作 ...
740 |
741 | ```
742 |
743 | * 每一個 feature 有三個主要成分:
744 | * Title
745 | * Narrative - 簡短說明這個 feature 關於什麼。
746 | * Acceptance criteria - 每個由獨立步驟組成的一套情境。
747 | * 最常見的格式稱為 Connextra 格式。
748 |
749 | ```Ruby
750 | In order to [benefit] ...
751 | A [stakeholder]...
752 | Wants to [feature] ...
753 | ```
754 |
755 | 這種格式最常見,但並不強求要這樣寫, narrative 敘述句可以因功能的複雜度而任意書寫。
756 |
757 | * 可任意使用情境概述使你的情境可備作它用(keep your scenarios DRY)。
758 |
759 | ```Ruby
760 | Scenario Outline: User cannot register with invalid e-mail
761 | When I try to register with an email ""
762 | Then I should see the error message ""
763 |
764 | Examples:
765 | |email |error |
766 | | |The e-mail is required|
767 | |invalid email |is not a valid e-mail |
768 | ```
769 |
770 | * 情境的步驟放在 `step_definitions` 目錄下的 `.rb` 檔。步驟檔命名慣例為 `[description]_steps.rb`。步驟根據不同的標準放在不同的檔案裡。每一個 feature 可能有一個步驟檔(`home_page_steps.rb`)
771 | 。也可以給每個特定物件的 feature,開一個步驟檔(`articles_steps.rb`)。
772 | * 使用多行步驟參數來避免重複
773 |
774 | ```Ruby
775 | Scenario: User profile
776 | Given I am logged in as a user "John Doe" with an e-mail "user@test.com"
777 | When I go to my profile
778 | Then I should see the following information:
779 | |First name|John |
780 | |Last name |Doe |
781 | |E-mail |user@test.com|
782 |
783 | # 步驟:
784 | Then /^I should see the following information:$/ do |table|
785 | table.raw.each do |field, value|
786 | find_field(field).value.should =~ /#{value}/
787 | end
788 | end
789 | ```
790 |
791 | * 使用複合步驟來讓情境可備作它用(Keep your scenarios DRY)
792 |
793 | ```Ruby
794 | # ...
795 | When I subscribe for news from the category "Technical News"
796 | # ...
797 |
798 | # 步驟:
799 | When /^I subscribe for news from the category "([^"]*)"$/ do |category|
800 | steps %Q{
801 | When I go to the news categories page
802 | And I select the category #{category}
803 | And I click the button "Subscribe for this category"
804 | And I confirm the subscription
805 | }
806 | end
807 | ```
808 | * 務必使用 Capybara 的否定配對來取代在肯定情況裡使用 should_not,這樣子當 ajax 操作逾時就會重試。見 [Capybara 的 README 檔](https://github.com/jnicklas/capybara)獲得更多說明。
809 |
810 | ## RSpec
811 |
812 | * 每個測試案例應只有一個期望值 (expection)。
813 |
814 | ```Ruby
815 | # 劣
816 | describe ArticlesController do
817 | #...
818 |
819 | describe 'GET new' do
820 | it 'assigns new article and renders the new article template' do
821 | get :new
822 | assigns[:article].should be_a_new Article
823 | response.should render_template :new
824 | end
825 | end
826 |
827 | # ...
828 | end
829 |
830 | # 優
831 | describe ArticlesController do
832 | #...
833 |
834 | describe 'GET new' do
835 | it 'assigns a new article' do
836 | get :new
837 | assigns[:article].should be_a_new Article
838 | end
839 |
840 | it 'renders the new article template' do
841 | get :new
842 | response.should render_template :new
843 | end
844 | end
845 |
846 | end
847 | ```
848 |
849 | * 應大量使用 `describe` 及 `context` 。
850 | * `describe` 區塊的命名方式應如下:
851 | * 非方法使用 "description"
852 | * 實體方法使用井字號 "#method"
853 | * 類別方法使用點 ".method"
854 |
855 | ```Ruby
856 | class Article
857 | def summary
858 | #...
859 | end
860 |
861 | def self.latest
862 | #...
863 | end
864 | end
865 |
866 | # the spec...
867 | describe Article do
868 | describe '#summary' do
869 | #...
870 | end
871 |
872 | describe '.latest' do
873 | #...
874 | end
875 | end
876 | ```
877 |
878 | * 使用 [fabricators](http://fabricationgem.org/) 來建立測資物件。
879 |
880 | * 應大量使用 mocks 與 stubs。
881 |
882 | ```Ruby
883 | # mocking 一個資料模型
884 | article = mock_model(Article)
885 |
886 | # stubbing 一個方法
887 | Article.stub(:find).with(article.id).and_return(article)
888 | ```
889 |
890 | * 當 mocking 一個資料模型時,可以使用 `as_null_object` 方法,讓輸出的物件只回應我們有 stub 的方法,不理會其他方法。
891 |
892 | ```Ruby
893 | article = mock_model(Article).as_null_object
894 | ```
895 |
896 | * 使用 `let` 區塊,不要用 `before(:each)` 區塊來為 spec 測試案例建立資料。 `let` 區塊會被延遲求值 (lazily evaluated)。
897 |
898 | ```Ruby
899 | # 使用這個:
900 | let(:article) { Fabricate(:article) }
901 |
902 | # ... 而不是這個:
903 | before(:each) { @article = Fabricate(:article) }
904 | ```
905 |
906 | * 盡可能使用 `subject`。
907 |
908 | ```Ruby
909 | describe Article do
910 | subject { Fabricate(:article) }
911 |
912 | it 'is not published on creation' do
913 | subject.should_not be_published
914 | end
915 | end
916 | ```
917 |
918 | * 盡可能使用 `specify`。它是 `it` 的同義詞,但在沒 docstring 的情況下更好讀。
919 |
920 | ```Ruby
921 | # 劣
922 | describe Article do
923 | before { @article = Fabricate(:article) }
924 |
925 | it 'is not published on creation' do
926 | @article.should_not be_published
927 | end
928 | end
929 |
930 | # 優
931 | describe Article do
932 | let(:article) { Fabricate(:article) }
933 | specify { article.should_not be_published }
934 | end
935 | ```
936 |
937 | * 盡可能使用 `its` 。
938 |
939 | ```Ruby
940 | # 劣
941 | describe Article do
942 | subject { Fabricate(:article) }
943 |
944 | it 'has the current date as creation date' do
945 | subject.creation_date.should == Date.today
946 | end
947 | end
948 |
949 | # 優
950 | describe Article do
951 | subject { Fabricate(:article) }
952 | its(:creation_date) { should == Date.today }
953 | end
954 | ```
955 |
956 | * 如果要建立共用的 spec 群組,請使用 `shared_examples`。
957 |
958 | ```Ruby
959 | # 劣
960 | describe Array do
961 | subject { Array.new [7, 2, 4] }
962 |
963 | context "initialized with 3 items" do
964 | its(:size) { should eq(3) }
965 | end
966 | end
967 |
968 | describe Set do
969 | subject { Set.new [7, 2, 4] }
970 |
971 | context "initialized with 3 items" do
972 | its(:size) { should eq(3) }
973 | end
974 | end
975 |
976 | # 優
977 | shared_examples "a collection" do
978 | subject { described_class.new([7, 2, 4]) }
979 |
980 | context "initialized with 3 items" do
981 | its(:size) { should eq(3) }
982 | end
983 | end
984 |
985 | describe Array do
986 | it_behaves_like "a collection"
987 | end
988 |
989 | describe Set do
990 | it_behaves_like "a collection"
991 | end
992 |
993 |
994 | ### 視圖 (Views)
995 |
996 | * 視圖測試的目錄結構要與 `app/views` 裡面的結構一致。舉例來說, `app/views/users` 的視圖測試應放在 `spec/views/users`。
997 | * 視圖測試的命名慣例是把 `_spec.rb` 加到視圖名稱的後面,舉例來說,視圖 `_form.html.haml` 有一個對應的測試叫做 `_form.html.haml_spec.rb`。
998 | * 每個視圖測試檔都需要 `spec_helper.rb`。
999 | * 外圍的 `describe` 區塊要使用不包含 `app/views` 前綴的視圖路徑,這在 `render` 方法沒有傳入參數的時候會用到。
1000 |
1001 | ```Ruby
1002 | # spec/views/articles/new.html.haml_spec.rb
1003 | require 'spec_helper'
1004 |
1005 | describe 'articles/new.html.haml' do
1006 | # ...
1007 | end
1008 | ```
1009 |
1010 | * 務必要在視圖測試裡面 mock 資料模型。視圖的目的只有顯示資訊而已。
1011 | * 原本由控制器提供給視圖使用的實體變數(instance variable),可以用 `assign` 方法來提供。
1012 |
1013 | ```Ruby
1014 | # spec/views/articles/edit.html.haml_spec.rb
1015 | describe 'articles/edit.html.haml' do
1016 | it 'renders the form for a new article creation' do
1017 | assign(
1018 | :article,
1019 | mock_model(Article).as_new_record.as_null_object
1020 | )
1021 | render
1022 | rendered.should have_selector('form',
1023 | method: 'post',
1024 | action: articles_path
1025 | ) do |form|
1026 | form.should have_selector('input', type: 'submit')
1027 | end
1028 | end
1029 | ```
1030 |
1031 | * 最好使用 capybara 的否定情況選擇器,而非 should_not 配上正面情況。
1032 |
1033 | ```Ruby
1034 | # 劣
1035 | page.should_not have_selector('input', type: 'submit')
1036 | page.should_not have_xpath('tr')
1037 |
1038 | # 優
1039 | page.should have_no_selector('input', type: 'submit')
1040 | page.should have_no_xpath('tr')
1041 | ```
1042 |
1043 | * 當視圖要使用 helper 方法時,要先把這些方法給 stub 掉,這件事要在 `template` 物件裡面做:
1044 |
1045 | ```Ruby
1046 | # app/helpers/articles_helper.rb
1047 | class ArticlesHelper
1048 | def formatted_date(date)
1049 | # ...
1050 | end
1051 | end
1052 |
1053 | # app/views/articles/show.html.haml
1054 | = "Published at: #{formatted_date(@article.published_at)}"
1055 |
1056 | # spec/views/articles/show.html.haml_spec.rb
1057 | describe 'articles/show.html.haml' do
1058 | it 'displays the formatted date of article publishing' do
1059 | article = mock_model(Article, published_at: Date.new(2012, 01, 01))
1060 | assign(:article, article)
1061 |
1062 | template.stub(:formatted_date).with(article.published_at).and_return('01.01.2012')
1063 |
1064 | render
1065 | rendered.should have_content('Published at: 01.01.2012')
1066 | end
1067 | end
1068 | ```
1069 |
1070 | * helper specs 測試檔要要從視圖 specs 測試裡面拆出來,放在 `spec/helpers` 目錄下。
1071 |
1072 | ### 控制器
1073 |
1074 | * 請 mock 資料模型並 stub 他們的方法。測試控制器時不應依賴於資料模型的建立。
1075 | * 請只測試控制器需負責的行為:
1076 | * 某幾個特定方法的執行
1077 | * 從動作 (action) 回傳的資料 - assigns, 等等。
1078 | * 動作所產生的結果 - template render, redirect, 等等。
1079 |
1080 | ```Ruby
1081 | # 常用的控制器 spec 範例
1082 | # spec/controllers/articles_controller_spec.rb
1083 | # 我們只對控制器應執行的動作感興趣
1084 | # 所以我們 mock 資料模型及 stub 它的方法
1085 | # 並且專注在控制器該做的事情上
1086 |
1087 | describe ArticlesController do
1088 | # 資料模型將會在測試中被所有控制器的方法所使用
1089 | let(:article) { mock_model(Article) }
1090 |
1091 | describe 'POST create' do
1092 | before { Article.stub(:new).and_return(article) }
1093 |
1094 | it 'creates a new article with the given attributes' do
1095 | Article.should_receive(:new).with(title: 'The New Article Title').and_return(article)
1096 | post :create, message: { title: 'The New Article Title' }
1097 | end
1098 |
1099 | it 'saves the article' do
1100 | article.should_receive(:save)
1101 | post :create
1102 | end
1103 |
1104 | it 'redirects to the Articles index' do
1105 | article.stub(:save)
1106 | post :create
1107 | response.should redirect_to(action: 'index')
1108 | end
1109 | end
1110 | end
1111 | ```
1112 |
1113 | * 當控制器根據不同參數有不同行為時,請使用 context。
1114 |
1115 | ```Ruby
1116 | # 一個在控制器中使用 context 的典型例子是,建立或更新物件時,可能因為儲存成功與否而有不同行為。
1117 |
1118 | describe ArticlesController do
1119 | let(:article) { mock_model(Article) }
1120 |
1121 | describe 'POST create' do
1122 | before { Article.stub(:new).and_return(article) }
1123 |
1124 | it 'creates a new article with the given attributes' do
1125 | Article.should_receive(:new).with(title: 'The New Article Title').and_return(article)
1126 | post :create, article: { title: 'The New Article Title' }
1127 | end
1128 |
1129 | it 'saves the article' do
1130 | article.should_receive(:save)
1131 | post :create
1132 | end
1133 |
1134 | context 'when the article saves successfully' do
1135 | before { article.stub(:save).and_return(true) }
1136 |
1137 | it 'sets a flash[:notice] message' do
1138 | post :create
1139 | flash[:notice].should eq('The article was saved successfully.')
1140 | end
1141 |
1142 | it 'redirects to the Articles index' do
1143 | post :create
1144 | response.should redirect_to(action: 'index')
1145 | end
1146 | end
1147 |
1148 | context 'when the article fails to save' do
1149 | before { article.stub(:save).and_return(false) }
1150 |
1151 | it 'assigns @article' do
1152 | post :create
1153 | assigns[:article].should be_eql(article)
1154 | end
1155 |
1156 | it 're-renders the "new" template' do
1157 | post :create
1158 | response.should render_template('new')
1159 | end
1160 | end
1161 | end
1162 | end
1163 | ```
1164 |
1165 | ### 資料模型
1166 |
1167 | * 不要在資料模型自己的測試裡 mock 該資料模型。
1168 | * 使用 fabrication 來建立實際的物件
1169 | * 可以 mock 別的資料模型或子物件。
1170 | * 為避免重覆,請在測試裡建立可以給所有測試案例使用的資料模型。
1171 |
1172 | ```Ruby
1173 | describe Article do
1174 | let(:article) { Fabricate(:article) }
1175 | end
1176 | ```
1177 |
1178 | * 新增一個測試案例來確保 fabrication 做出來的資料模型是可以用的。
1179 |
1180 | ```Ruby
1181 | describe Article do
1182 | it 'is valid with valid attributes' do
1183 | article.should be_valid
1184 | end
1185 | end
1186 | ```
1187 |
1188 | * 寫跟驗證程序有關的測試案例時,請使用 `have(x).errors_on` 來指定要被驗證的屬性。使用 `be_valid` 並不能保證問題一定會發生在要被驗證的屬性。
1189 |
1190 | ```Ruby
1191 | # 劣
1192 | describe '#title' do
1193 | it 'is required' do
1194 | article.title = nil
1195 | article.should_not be_valid
1196 | end
1197 | end
1198 |
1199 | # 推薦使用
1200 | describe '#title' do
1201 | it 'is required' do
1202 | article.title = nil
1203 | article.should have(1).error_on(:title)
1204 | end
1205 | end
1206 | ```
1207 |
1208 | * 替每個有驗證程序的屬性,另外加另一個 `describe`。
1209 |
1210 | ```Ruby
1211 | describe Article do
1212 | describe '#title'
1213 | it 'is required' do
1214 | article.title = nil
1215 | article.should have(1).error_on(:title)
1216 | end
1217 | end
1218 | end
1219 | ```
1220 |
1221 | * 當測試資料模型屬性的唯一性時,將另一個重覆的物件命名為 `another_object`。
1222 |
1223 | ```Ruby
1224 | describe Article do
1225 | describe '#title' do
1226 | it 'is unique' do
1227 | another_article = Fabricate.build(:article, title: article.title)
1228 | article.should have(1).error_on(:title)
1229 | end
1230 | end
1231 | end
1232 | ```
1233 |
1234 | ### Mailers
1235 |
1236 | * 在 Mailer 測試的資料模型應該要被 mock 掉。 Mailer 不應依賴資料模型的建立。
1237 | * Mailer 的測試應該要檢驗這些:
1238 | * 主旨正確
1239 | * 收件人 e-mail 正確
1240 | * e-mail 有寄送至正確的 e-mail 地址
1241 | * e-mail 有包含所要寄送的訊息
1242 |
1243 | ```Ruby
1244 | describe SubscriberMailer do
1245 | let(:subscriber) { mock_model(Subscription, email: 'johndoe@test.com', name: 'John Doe') }
1246 |
1247 | describe 'successful registration email' do
1248 | subject { SubscriptionMailer.successful_registration_email(subscriber) }
1249 |
1250 | its(:subject) { should == 'Successful Registration!' }
1251 | its(:from) { should == ['info@your_site.com'] }
1252 | its(:to) { should == [subscriber.email] }
1253 |
1254 | it 'contains the subscriber name' do
1255 | subject.body.encoded.should match(subscriber.name)
1256 | end
1257 | end
1258 | end
1259 | ```
1260 |
1261 | ### Uploaders
1262 |
1263 | * 我們可以測試上傳的圖片是否有正確產生縮圖。以下是 [carrierwave](https://github.com/jnicklas/carrierwave) 圖片上傳器的範例 spec:
1264 |
1265 | ```Ruby
1266 | # rspec/uploaders/person_avatar_uploader_spec.rb
1267 | require 'spec_helper'
1268 | require 'carrierwave/test/matchers'
1269 |
1270 | describe PersonAvatarUploader do
1271 | include CarrierWave::Test::Matchers
1272 |
1273 | # 在執行測試案例之前,先打開圖片處理
1274 | before(:all) do
1275 | UserAvatarUploader.enable_processing = true
1276 | end
1277 |
1278 | # 建立一個新的 uploader。要把資料模型給 mock 掉,使上傳及縮圖的時候不會依賴於資料模型的建立。
1279 | before(:each) do
1280 | @uploader = PersonAvatarUploader.new(mock_model(Person).as_null_object)
1281 | @uploader.store!(File.open(path_to_file))
1282 | end
1283 |
1284 | # 執行完測試案例時,關閉圖片處理
1285 | after(:all) do
1286 | UserAvatarUploader.enable_processing = false
1287 | end
1288 |
1289 | # 測試縮圖是否不比給定的尺寸大
1290 | context 'the default version' do
1291 | it 'scales down an image to be no larger than 256 by 256 pixels' do
1292 | @uploader.should be_no_larger_than(256, 256)
1293 | end
1294 | end
1295 |
1296 | # 測試縮圖是否有完全一致的尺寸
1297 | context 'the thumb version' do
1298 | it 'scales down an image to be exactly 64 by 64 pixels' do
1299 | @uploader.thumb.should have_dimensions(64, 64)
1300 | end
1301 | end
1302 | end
1303 | ```
1304 |
1305 | # 延伸閱讀
1306 |
1307 | 有幾個絕妙講述 Rails 風格的資源,若有閒暇時應當考慮閱讀之:
1308 |
1309 | * [The Rails 3 Way](http://www.amazon.com/Rails-Way-Addison-Wesley-Professional-Ruby/dp/0321601661)
1310 | * [Ruby on Rails Guides](http://guides.rubyonrails.org/)
1311 | * [The RSpec Book](http://pragprog.com/book/achbd/the-rspec-book)
1312 | * [The Cucumber Book](http://pragprog.com/book/hwcuc/the-cucumber-book)
1313 | * [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec)
1314 |
1315 | # 貢獻
1316 |
1317 | 在本指南所寫的每個東西都不是定案。這只是我渴望想與同樣對 Rails 程式設計風格有興趣的大家一起工作,這樣子最終我們可以創造出對整個 Ruby 社群都有益的資源。
1318 |
1319 | 歡迎開票或發送一個帶有改進的 Pull Request。在此提前感謝你的幫助!
1320 |
1321 | # 授權
1322 |
1323 | 
1324 | This work is licensed under a [Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/deed.zh_TW)
1325 |
1326 | # 口耳相傳
1327 |
1328 | 一份社群驅動的風格指南,對於沒聽過這份指南的其他社群人士來說,幾乎沒什麼用。請上 Twitter 轉貼這份指南,分享給你的朋友或同事。我們得到的每個註解、建議或意見都可以讓這份指南變得更好一點。而我們都想要有最好的指南,對吧?
1329 |
1330 | 共勉之,
1331 | [Bozhidar](https://twitter.com/bbatsov)
1332 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Prelude
2 |
3 | > Role models are important.
4 | > -- Officer Alex J. Murphy / RoboCop
5 |
6 | The goal of this guide is to present a set of best practices and style
7 | prescriptions for Ruby on Rails 4 development. It's a
8 | complementary guide to the already existing community-driven
9 | [Ruby coding style guide](https://github.com/bbatsov/ruby-style-guide).
10 |
11 | Some of the advice here is applicable only to Rails 4.0+.
12 |
13 | You can generate a PDF or an HTML copy of this guide using
14 | [Pandoc](http://pandoc.org/).
15 |
16 | Translations of the guide are available in the following languages:
17 |
18 | * [Chinese Simplified](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md)
19 | * [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md)
20 | * [German](https://github.com/arbox/de-rails-style-guide/blob/master/README-deDE.md)
21 | * [Japanese](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md)
22 | * [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md)
23 | * [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md)
24 | * [Korean](https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md)
25 | * [Vietnamese](https://github.com/CQBinh/rails-style-guide/blob/master/README-viVN.md)
26 |
27 | # The Rails Style Guide
28 |
29 | This Rails style guide recommends best practices so that real-world Rails
30 | programmers can write code that can be maintained by other real-world Rails
31 | programmers. A style guide that reflects real-world usage gets used, and a
32 | style guide that holds to an ideal that has been rejected by the people it
33 | is supposed to help risks not getting used at all – no matter how good
34 | it is.
35 |
36 | The guide is separated into several sections of related rules. I've tried to add
37 | the rationale behind the rules (if it's omitted I've assumed it's pretty
38 | obvious).
39 |
40 | I didn't come up with all the rules out of nowhere - they are mostly based on my
41 | extensive career as a professional software engineer, feedback and suggestions
42 | from members of the Rails community and various highly regarded Rails
43 | programming resources.
44 |
45 | ## Table of Contents
46 |
47 | * [Configuration](#configuration)
48 | * [Routing](#routing)
49 | * [Controllers](#controllers)
50 | * [Rendering](#rendering)
51 | * [Models](#models)
52 | * [ActiveRecord](#activerecord)
53 | * [ActiveRecord Queries](#activerecord-queries)
54 | * [Migrations](#migrations)
55 | * [Views](#views)
56 | * [Internationalization](#internationalization)
57 | * [Assets](#assets)
58 | * [Mailers](#mailers)
59 | * [Active Support Core Extensions](#active-support-core-extensions)
60 | * [Time](#time)
61 | * [Bundler](#bundler)
62 | * [Managing processes](#managing-processes)
63 |
64 | ## Configuration
65 |
66 | *
67 | Put custom initialization code in `config/initializers`. The code in
68 | initializers executes on application startup.
69 | [[link](#config-initializers)]
70 |
71 | *
72 | Keep initialization code for each gem in a separate file with the same name
73 | as the gem, for example `carrierwave.rb`, `active_admin.rb`, etc.
74 | [[link](#gem-initializers)]
75 |
76 | *
77 | Adjust accordingly the settings for development, test and production
78 | environment (in the corresponding files under `config/environments/`)
79 | [[link](#dev-test-prod-configs)]
80 |
81 | * Mark additional assets for precompilation (if any):
82 |
83 | ```Ruby
84 | # config/environments/production.rb
85 | # Precompile additional assets (application.js, application.css,
86 | #and all non-JS/CSS are already added)
87 | config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js )
88 | ```
89 |
90 | *
91 | Keep configuration that's applicable to all environments in the
92 | `config/application.rb` file.
93 | [[link](#app-config)]
94 |
95 | *
96 | Create an additional `staging` environment that closely resembles the
97 | `production` one.
98 | [[link](#staging-like-prod)]
99 |
100 | *
101 | Keep any additional configuration in YAML files under the `config/` directory.
102 | [[link](#yaml-config)]
103 |
104 | Since Rails 4.2 YAML configuration files can be easily loaded with the new `config_for` method:
105 |
106 | ```Ruby
107 | Rails::Application.config_for(:yaml_file)
108 | ```
109 |
110 | ## Routing
111 |
112 | *
113 | When you need to add more actions to a RESTful resource (do you really need
114 | them at all?) use `member` and `collection` routes.
115 | [[link](#member-collection-routes)]
116 |
117 | ```Ruby
118 | # bad
119 | get 'subscriptions/:id/unsubscribe'
120 | resources :subscriptions
121 |
122 | # good
123 | resources :subscriptions do
124 | get 'unsubscribe', on: :member
125 | end
126 |
127 | # bad
128 | get 'photos/search'
129 | resources :photos
130 |
131 | # good
132 | resources :photos do
133 | get 'search', on: :collection
134 | end
135 | ```
136 |
137 | *
138 | If you need to define multiple `member/collection` routes use the
139 | alternative block syntax.
140 | [[link](#many-member-collection-routes)]
141 |
142 | ```Ruby
143 | resources :subscriptions do
144 | member do
145 | get 'unsubscribe'
146 | # more routes
147 | end
148 | end
149 |
150 | resources :photos do
151 | collection do
152 | get 'search'
153 | # more routes
154 | end
155 | end
156 | ```
157 |
158 | *
159 | Use nested routes to express better the relationship between ActiveRecord
160 | models.
161 | [[link](#nested-routes)]
162 |
163 | ```Ruby
164 | class Post < ActiveRecord::Base
165 | has_many :comments
166 | end
167 |
168 | class Comments < ActiveRecord::Base
169 | belongs_to :post
170 | end
171 |
172 | # routes.rb
173 | resources :posts do
174 | resources :comments
175 | end
176 | ```
177 |
178 | *
179 | If you need to nest routes more than 1 level deep then use the `shallow: true` option. This will save user from long urls `posts/1/comments/5/versions/7/edit` and you from long url helpers `edit_post_comment_version`.
180 |
181 | ```Ruby
182 | resources :posts, shallow: true do
183 | resources :comments do
184 | resources :versions
185 | end
186 | end
187 | ```
188 |
189 | *
190 | Use namespaced routes to group related actions.
191 | [[link](#namespaced-routes)]
192 |
193 | ```Ruby
194 | namespace :admin do
195 | # Directs /admin/products/* to Admin::ProductsController
196 | # (app/controllers/admin/products_controller.rb)
197 | resources :products
198 | end
199 | ```
200 |
201 | *
202 | Never use the legacy wild controller route. This route will make all actions
203 | in every controller accessible via GET requests.
204 | [[link](#no-wild-routes)]
205 |
206 | ```Ruby
207 | # very bad
208 | match ':controller(/:action(/:id(.:format)))'
209 | ```
210 |
211 | *
212 | Don't use `match` to define any routes unless there is need to map multiple request types among `[:get, :post, :patch, :put, :delete]` to a single action using `:via` option.
213 | [[link](#no-match-routes)]
214 |
215 | ## Controllers
216 |
217 | *
218 | Keep the controllers skinny - they should only retrieve data for the view
219 | layer and shouldn't contain any business logic (all the business logic
220 | should naturally reside in the model).
221 | [[link](#skinny-controllers)]
222 |
223 | *
224 | Each controller action should (ideally) invoke only one method other than an
225 | initial find or new.
226 | [[link](#one-method)]
227 |
228 | *
229 | Share no more than two instance variables between a controller and a view.
230 | [[link](#shared-instance-variables)]
231 |
232 |
233 | ### Rendering
234 |
235 | *
236 | Prefer using a template over inline rendering.
237 | [[link](#inline-rendering)]
238 |
239 | ```Ruby
240 | # very bad
241 | class ProductsController < ApplicationController
242 | def index
243 | render inline: "<% products.each do |p| %><%= p.name %>
<% end %>", type: :erb
244 | end
245 | end
246 |
247 | # good
248 | ## app/views/products/index.html.erb
249 | <%= render partial: 'product', collection: products %>
250 |
251 | ## app/views/products/_product.html.erb
252 | <%= product.name %>
253 | <%= product.price %>
254 |
255 | ## app/controllers/foo_controller.rb
256 | class ProductsController < ApplicationController
257 | def index
258 | render :index
259 | end
260 | end
261 | ```
262 |
263 | *
264 | Prefer `render plain:` over `render text:`.
265 | [[link](#plain-text-rendering)]
266 |
267 | ```Ruby
268 | # bad - sets MIME type to `text/html`
269 | ...
270 | render text: 'Ruby!'
271 | ...
272 |
273 | # bad - requires explicit MIME type declaration
274 | ...
275 | render text: 'Ruby!', content_type: 'text/plain'
276 | ...
277 |
278 | # good - short and precise
279 | ...
280 | render plain: 'Ruby!'
281 | ...
282 | ```
283 |
284 | *
285 | Prefer [corresponding symbols](https://gist.github.com/mlanett/a31c340b132ddefa9cca) to numeric HTTP status codes. They are meaningful and do not look like "magic" numbers for less known HTTP status codes.
286 | [[link](#http-status-code-symbols)]
287 |
288 | ```Ruby
289 | # bad
290 | ...
291 | render status: 500
292 | ...
293 |
294 | # good
295 | ...
296 | render status: :forbidden
297 | ...
298 | ```
299 |
300 | ## Models
301 |
302 | *
303 | Introduce non-ActiveRecord model classes freely.
304 | [[link](#model-classes)]
305 |
306 | *
307 | Name the models with meaningful (but short) names without abbreviations.
308 | [[link](#meaningful-model-names)]
309 |
310 | *
311 | If you need model objects that support ActiveRecord behavior (like validation)
312 | without the ActiveRecord database functionality use the
313 | [ActiveAttr](https://github.com/cgriego/active_attr) gem.
314 | [[link](#activeattr-gem)]
315 |
316 | ```Ruby
317 | class Message
318 | include ActiveAttr::Model
319 |
320 | attribute :name
321 | attribute :email
322 | attribute :content
323 | attribute :priority
324 |
325 | attr_accessible :name, :email, :content
326 |
327 | validates :name, presence: true
328 | validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i }
329 | validates :content, length: { maximum: 500 }
330 | end
331 | ```
332 |
333 | For a more complete example refer to the
334 | [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr).
335 |
336 | *
337 | Unless they have some meaning in the business domain, don't put methods in
338 | your model that just format your data (like code generating HTML). These
339 | methods are most likely going to be called from the view layer only, so their
340 | place is in helpers. Keep your models for business logic and data-persistence
341 | only.
342 | [[link](#model-business-logic)]
343 |
344 | ### ActiveRecord
345 |
346 | *
347 | Avoid altering ActiveRecord defaults (table names, primary key, etc) unless
348 | you have a very good reason (like a database that's not under your control).
349 | [[link](#keep-ar-defaults)]
350 |
351 | ```Ruby
352 | # bad - don't do this if you can modify the schema
353 | class Transaction < ActiveRecord::Base
354 | self.table_name = 'order'
355 | ...
356 | end
357 | ```
358 |
359 | *
360 | Group macro-style methods (`has_many`, `validates`, etc) in the beginning of
361 | the class definition.
362 | [[link](#macro-style-methods)]
363 |
364 | ```Ruby
365 | class User < ActiveRecord::Base
366 | # keep the default scope first (if any)
367 | default_scope { where(active: true) }
368 |
369 | # constants come up next
370 | COLORS = %w(red green blue)
371 |
372 | # afterwards we put attr related macros
373 | attr_accessor :formatted_date_of_birth
374 |
375 | attr_accessible :login, :first_name, :last_name, :email, :password
376 |
377 | # Rails4+ enums after attr macros, prefer the hash syntax
378 | enum gender: { female: 0, male: 1 }
379 |
380 | # followed by association macros
381 | belongs_to :country
382 |
383 | has_many :authentications, dependent: :destroy
384 |
385 | # and validation macros
386 | validates :email, presence: true
387 | validates :username, presence: true
388 | validates :username, uniqueness: { case_sensitive: false }
389 | validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ }
390 | validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true }
391 |
392 | # next we have callbacks
393 | before_save :cook
394 | before_save :update_username_lower
395 |
396 | # other macros (like devise's) should be placed after the callbacks
397 |
398 | ...
399 | end
400 | ```
401 |
402 | *
403 | Prefer `has_many :through` to `has_and_belongs_to_many`. Using `has_many
404 | :through` allows additional attributes and validations on the join model.
405 | [[link](#has-many-through)]
406 |
407 | ```Ruby
408 | # not so good - using has_and_belongs_to_many
409 | class User < ActiveRecord::Base
410 | has_and_belongs_to_many :groups
411 | end
412 |
413 | class Group < ActiveRecord::Base
414 | has_and_belongs_to_many :users
415 | end
416 |
417 | # preferred way - using has_many :through
418 | class User < ActiveRecord::Base
419 | has_many :memberships
420 | has_many :groups, through: :memberships
421 | end
422 |
423 | class Membership < ActiveRecord::Base
424 | belongs_to :user
425 | belongs_to :group
426 | end
427 |
428 | class Group < ActiveRecord::Base
429 | has_many :memberships
430 | has_many :users, through: :memberships
431 | end
432 | ```
433 |
434 | *
435 | Prefer `self[:attribute]` over `read_attribute(:attribute)`.
436 | [[link](#read-attribute)]
437 |
438 | ```Ruby
439 | # bad
440 | def amount
441 | read_attribute(:amount) * 100
442 | end
443 |
444 | # good
445 | def amount
446 | self[:amount] * 100
447 | end
448 | ```
449 |
450 | *
451 | Prefer `self[:attribute] = value` over `write_attribute(:attribute, value)`.
452 | [[link](#write-attribute)]
453 |
454 | ```Ruby
455 | # bad
456 | def amount
457 | write_attribute(:amount, 100)
458 | end
459 |
460 | # good
461 | def amount
462 | self[:amount] = 100
463 | end
464 | ```
465 |
466 | *
467 | Always use the new ["sexy"
468 | validations](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/).
469 | [[link](#sexy-validations)]
470 |
471 | ```Ruby
472 | # bad
473 | validates_presence_of :email
474 | validates_length_of :email, maximum: 100
475 |
476 | # good
477 | validates :email, presence: true, length: { maximum: 100 }
478 | ```
479 |
480 | *
481 | When a custom validation is used more than once or the validation is some
482 | regular expression mapping, create a custom validator file.
483 | [[link](#custom-validator-file)]
484 |
485 | ```Ruby
486 | # bad
487 | class Person
488 | validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
489 | end
490 |
491 | # good
492 | class EmailValidator < ActiveModel::EachValidator
493 | def validate_each(record, attribute, value)
494 | record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
495 | end
496 | end
497 |
498 | class Person
499 | validates :email, email: true
500 | end
501 | ```
502 |
503 | *
504 | Keep custom validators under `app/validators`.
505 | [[link](#app-validators)]
506 |
507 | *
508 | Consider extracting custom validators to a shared gem if you're maintaining
509 | several related apps or the validators are generic enough.
510 | [[link](#custom-validators-gem)]
511 |
512 | *
513 | Use named scopes freely.
514 | [[link](#named-scopes)]
515 |
516 | ```Ruby
517 | class User < ActiveRecord::Base
518 | scope :active, -> { where(active: true) }
519 | scope :inactive, -> { where(active: false) }
520 |
521 | scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }
522 | end
523 | ```
524 |
525 | *
526 | When a named scope defined with a lambda and parameters becomes too
527 | complicated, it is preferable to make a class method instead which serves the
528 | same purpose of the named scope and returns an `ActiveRecord::Relation`
529 | object. Arguably you can define even simpler scopes like this.
530 | [[link](#named-scope-class)]
531 |
532 | ```Ruby
533 | class User < ActiveRecord::Base
534 | def self.with_orders
535 | joins(:orders).select('distinct(users.id)')
536 | end
537 | end
538 | ```
539 |
540 | *
541 | Beware of the behavior of the
542 | [following](http://guides.rubyonrails.org/active_record_validations.html#skipping-validations)
543 | methods. They do not run the model validations and
544 | could easily corrupt the model state.
545 | [[link](#beware-skip-model-validations)]
546 |
547 | ```Ruby
548 | # bad
549 | Article.first.decrement!(:view_count)
550 | DiscussionBoard.decrement_counter(:post_count, 5)
551 | Article.first.increment!(:view_count)
552 | DiscussionBoard.increment_counter(:post_count, 5)
553 | person.toggle :active
554 | product.touch
555 | Billing.update_all("category = 'authorized', author = 'David'")
556 | user.update_attribute(:website, 'example.com')
557 | user.update_columns(last_request_at: Time.current)
558 | Post.update_counters 5, comment_count: -1, action_count: 1
559 |
560 | # good
561 | user.update_attributes(website: 'example.com')
562 | ```
563 |
564 | *
565 | Use user-friendly URLs. Show some descriptive attribute of the model in the URL
566 | rather than its `id`. There is more than one way to achieve this:
567 | [[link](#user-friendly-urls)]
568 |
569 | * Override the `to_param` method of the model. This method is used by Rails
570 | for constructing a URL to the object. The default implementation returns
571 | the `id` of the record as a String. It could be overridden to include another
572 | human-readable attribute.
573 |
574 | ```Ruby
575 | class Person
576 | def to_param
577 | "#{id} #{name}".parameterize
578 | end
579 | end
580 | ```
581 |
582 | In order to convert this to a URL-friendly value, `parameterize` should be
583 | called on the string. The `id` of the object needs to be at the beginning so
584 | that it can be found by the `find` method of ActiveRecord.
585 |
586 | * Use the `friendly_id` gem. It allows creation of human-readable URLs by
587 | using some descriptive attribute of the model instead of its `id`.
588 |
589 | ```Ruby
590 | class Person
591 | extend FriendlyId
592 | friendly_id :name, use: :slugged
593 | end
594 | ```
595 |
596 | Check the [gem documentation](https://github.com/norman/friendly_id) for more
597 | information about its usage.
598 |
599 | *
600 | Use `find_each` to iterate over a collection of AR objects. Looping through a
601 | collection of records from the database (using the `all` method, for example)
602 | is very inefficient since it will try to instantiate all the objects at once.
603 | In that case, batch processing methods allow you to work with the records in
604 | batches, thereby greatly reducing memory consumption.
605 | [[link](#find-each)]
606 |
607 |
608 | ```Ruby
609 | # bad
610 | Person.all.each do |person|
611 | person.do_awesome_stuff
612 | end
613 |
614 | Person.where('age > 21').each do |person|
615 | person.party_all_night!
616 | end
617 |
618 | # good
619 | Person.find_each do |person|
620 | person.do_awesome_stuff
621 | end
622 |
623 | Person.where('age > 21').find_each do |person|
624 | person.party_all_night!
625 | end
626 | ```
627 |
628 | *
629 | Since [Rails creates callbacks for dependent
630 | associations](https://github.com/rails/rails/issues/3458), always call
631 | `before_destroy` callbacks that perform validation with `prepend: true`.
632 | [[link](#before_destroy)]
633 |
634 | ```Ruby
635 | # bad (roles will be deleted automatically even if super_admin? is true)
636 | has_many :roles, dependent: :destroy
637 |
638 | before_destroy :ensure_deletable
639 |
640 | def ensure_deletable
641 | fail "Cannot delete super admin." if super_admin?
642 | end
643 |
644 | # good
645 | has_many :roles, dependent: :destroy
646 |
647 | before_destroy :ensure_deletable, prepend: true
648 |
649 | def ensure_deletable
650 | fail "Cannot delete super admin." if super_admin?
651 | end
652 | ```
653 |
654 | *
655 | Define the `dependent` option to the `has_many` and `has_one` associations.
656 | [[link](#has_many-has_one-dependent-option)]
657 |
658 | ```Ruby
659 | # bad
660 | class Post < ActiveRecord::Base
661 | has_many :comments
662 | end
663 |
664 | # good
665 | class Post < ActiveRecord::Base
666 | has_many :comments, dependent: :destroy
667 | end
668 | ```
669 |
670 | *
671 | When persisting AR objects always use the exception raising bang! method or handle the method return value.
672 | This applies to `create`, `save`, `update`, `destroy`, `first_or_create` and `find_or_create_by`.
673 | [[link](#save-bang)]
674 |
675 | ```Ruby
676 | # bad
677 | user.create(name: 'Bruce')
678 |
679 | # bad
680 | user.save
681 |
682 | # good
683 | user.create!(name: 'Bruce')
684 | # or
685 | bruce = user.create(name: 'Bruce')
686 | if bruce.persisted?
687 | ...
688 | else
689 | ...
690 | end
691 |
692 | # good
693 | user.save!
694 | # or
695 | if user.save
696 | ...
697 | else
698 | ...
699 | end
700 | ```
701 |
702 | ### ActiveRecord Queries
703 |
704 | *
705 | Avoid string interpolation in
706 | queries, as it will make your code susceptible to SQL injection
707 | attacks.
708 | [[link](#avoid-interpolation)]
709 |
710 | ```Ruby
711 | # bad - param will be interpolated unescaped
712 | Client.where("orders_count = #{params[:orders]}")
713 |
714 | # good - param will be properly escaped
715 | Client.where('orders_count = ?', params[:orders])
716 | ```
717 |
718 | *
719 | Consider using named placeholders instead of positional placeholders
720 | when you have more than 1 placeholder in your query.
721 | [[link](#named-placeholder)]
722 |
723 | ```Ruby
724 | # okish
725 | Client.where(
726 | 'created_at >= ? AND created_at <= ?',
727 | params[:start_date], params[:end_date]
728 | )
729 |
730 | # good
731 | Client.where(
732 | 'created_at >= :start_date AND created_at <= :end_date',
733 | start_date: params[:start_date], end_date: params[:end_date]
734 | )
735 | ```
736 |
737 | *
738 | Favor the use of `find` over `where`
739 | when you need to retrieve a single record by id.
740 | [[link](#find)]
741 |
742 | ```Ruby
743 | # bad
744 | User.where(id: id).take
745 |
746 | # good
747 | User.find(id)
748 | ```
749 |
750 | *
751 | Favor the use of `find_by` over `where` and `find_by_attribute`
752 | when you need to retrieve a single record by some attributes.
753 | [[link](#find_by)]
754 |
755 | ```Ruby
756 | # bad
757 | User.where(first_name: 'Bruce', last_name: 'Wayne').first
758 |
759 | # bad
760 | User.find_by_first_name_and_last_name('Bruce', 'Wayne')
761 |
762 | # good
763 | User.find_by(first_name: 'Bruce', last_name: 'Wayne')
764 | ```
765 |
766 | *
767 | Favor the use of `where.not` over SQL.
768 | [[link](#where-not)]
769 |
770 | ```Ruby
771 | # bad
772 | User.where("id != ?", id)
773 |
774 | # good
775 | User.where.not(id: id)
776 | ```
777 | *
778 | When specifying an explicit query in a method such as `find_by_sql`, use
779 | heredocs with `squish`. This allows you to legibly format the SQL with
780 | line breaks and indentations, while supporting syntax highlighting in many
781 | tools (including GitHub, Atom, and RubyMine).
782 | [[link](#squished-heredocs)]
783 |
784 | ```Ruby
785 | User.find_by_sql(<<-SQL.squish)
786 | SELECT
787 | users.id, accounts.plan
788 | FROM
789 | users
790 | INNER JOIN
791 | accounts
792 | ON
793 | accounts.user_id = users.id
794 | # further complexities...
795 | SQL
796 | ```
797 |
798 | [`String#squish`](http://apidock.com/rails/String/squish) removes the indentation and newline characters so that your server
799 | log shows a fluid string of SQL rather than something like this:
800 |
801 | ```
802 | SELECT\n users.id, accounts.plan\n FROM\n users\n INNER JOIN\n acounts\n ON\n accounts.user_id = users.id
803 | ```
804 |
805 | ## Migrations
806 |
807 | *
808 | Keep the `schema.rb` (or `structure.sql`) under version control.
809 | [[link](#schema-version)]
810 |
811 | *
812 | Use `rake db:schema:load` instead of `rake db:migrate` to initialize an empty
813 | database.
814 | [[link](#db-schema-load)]
815 |
816 | *
817 | Enforce default values in the migrations themselves instead of in the
818 | application layer.
819 | [[link](#default-migration-values)]
820 |
821 | ```Ruby
822 | # bad - application enforced default value
823 | class Product < ActiveRecord::Base
824 | def amount
825 | self[:amount] || 0
826 | end
827 | end
828 |
829 | # good - database enforced
830 | class AddDefaultAmountToProducts < ActiveRecord::Migration
831 | def change
832 | change_column_default :products, :amount, 0
833 | end
834 | end
835 | ```
836 |
837 | While enforcing table defaults only in Rails is suggested by many
838 | Rails developers, it's an extremely brittle approach that
839 | leaves your data vulnerable to many application bugs. And you'll
840 | have to consider the fact that most non-trivial apps share a
841 | database with other applications, so imposing data integrity from
842 | the Rails app is impossible.
843 |
844 | *
845 | Enforce foreign-key constraints. As of Rails 4.2, ActiveRecord
846 | supports foreign key constraints natively.
847 | [[link](#foreign-key-constraints)]
848 |
849 | *
850 | When writing constructive migrations (adding tables or columns),
851 | use the `change` method instead of `up` and `down` methods.
852 | [[link](#change-vs-up-down)]
853 |
854 | ```Ruby
855 | # the old way
856 | class AddNameToPeople < ActiveRecord::Migration
857 | def up
858 | add_column :people, :name, :string
859 | end
860 |
861 | def down
862 | remove_column :people, :name
863 | end
864 | end
865 |
866 | # the new preferred way
867 | class AddNameToPeople < ActiveRecord::Migration
868 | def change
869 | add_column :people, :name, :string
870 | end
871 | end
872 | ```
873 |
874 | *
875 | If you have to use models in migrations, make sure you define them
876 | so that you don't end up with broken migrations in the future
877 | [[link](#define-model-class-migrations)]
878 |
879 | ```Ruby
880 | # db/migrate/.rb
881 | # frozen_string_literal: true
882 |
883 | # bad
884 | class ModifyDefaultStatusForProducts < ActiveRecord::Migration
885 | def change
886 | old_status = 'pending_manual_approval'
887 | new_status = 'pending_approval'
888 |
889 | reversible do |dir|
890 | dir.up do
891 | Product.where(status: old_status).update_all(status: new_status)
892 | change_column :products, :status, :string, default: new_status
893 | end
894 |
895 | dir.down do
896 | Product.where(status: new_status).update_all(status: old_status)
897 | change_column :products, :status, :string, default: old_status
898 | end
899 | end
900 | end
901 | end
902 |
903 | # good
904 | # Define `table_name` in a custom named class to make sure that
905 | # you run on the same table you had during the creation of the migration.
906 | # In future if you override the `Product` class
907 | # and change the `table_name`, it won't break
908 | # the migration or cause serious data corruption.
909 | class MigrationProduct < ActiveRecord::Base
910 | self.table_name = :products
911 | end
912 |
913 | class ModifyDefaultStatusForProducts < ActiveRecord::Migration
914 | def change
915 | old_status = 'pending_manual_approval'
916 | new_status = 'pending_approval'
917 |
918 | reversible do |dir|
919 | dir.up do
920 | MigrationProduct.where(status: old_status).update_all(status: new_status)
921 | change_column :products, :status, :string, default: new_status
922 | end
923 |
924 | dir.down do
925 | MigrationProduct.where(status: new_status).update_all(status: old_status)
926 | change_column :products, :status, :string, default: old_status
927 | end
928 | end
929 | end
930 | end
931 | ```
932 |
933 | *
934 | Name your foreign keys explicitly instead of relying on Rails auto-generated
935 | FK names. (http://guides.rubyonrails.org/active_record_migrations.html#foreign-keys)
936 | [[link](#meaningful-foreign-key-naming)]
937 |
938 | ```Ruby
939 | # bad
940 | class AddFkArticlesToAuthors < ActiveRecord::Migration
941 | def change
942 | add_foreign_key :articles, :authors
943 | end
944 | end
945 |
946 | # good
947 | class AddFkArticlesToAuthors < ActiveRecord::Migration
948 | def change
949 | add_foreign_key :articles, :authors, name: :articles_author_id_fk
950 | end
951 | end
952 | ```
953 |
954 | *
955 | Don't use non-reversible migration commands in the `change` method.
956 | Reversible migration commands are listed below.
957 | [ActiveRecord::Migration::CommandRecorder](http://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html)
958 | [[link](#reversible-migration)]
959 |
960 | ```ruby
961 | # bad
962 | class DropUsers < ActiveRecord::Migration
963 | def change
964 | drop_table :users
965 | end
966 | end
967 |
968 | # good
969 | class DropUsers < ActiveRecord::Migration
970 | def up
971 | drop_table :users
972 | end
973 |
974 | def down
975 | create_table :users do |t|
976 | t.string :name
977 | end
978 | end
979 | end
980 |
981 | # good
982 | # In this case, block will be used by create_table in rollback
983 | # http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table
984 | class DropUsers < ActiveRecord::Migration
985 | def change
986 | drop_table :users do |t|
987 | t.string :name
988 | end
989 | end
990 | end
991 | ```
992 |
993 | ## Views
994 |
995 | *
996 | Never call the model layer directly from a view.
997 | [[link](#no-direct-model-view)]
998 |
999 | *
1000 | Never make complex formatting in the views, export the formatting to a method
1001 | in the view helper or the model.
1002 | [[link](#no-complex-view-formatting)]
1003 |
1004 | *
1005 | Mitigate code duplication by using partial templates and layouts.
1006 | [[link](#partials)]
1007 |
1008 | ## Internationalization
1009 |
1010 | *
1011 | No strings or other locale specific settings should be used in the views,
1012 | models and controllers. These texts should be moved to the locale files in the
1013 | `config/locales` directory.
1014 | [[link](#locale-texts)]
1015 |
1016 | *
1017 | When the labels of an ActiveRecord model need to be translated, use the
1018 | `activerecord` scope:
1019 | [[link](#translated-labels)]
1020 |
1021 | ```
1022 | en:
1023 | activerecord:
1024 | models:
1025 | user: Member
1026 | attributes:
1027 | user:
1028 | name: 'Full name'
1029 | ```
1030 |
1031 | Then `User.model_name.human` will return "Member" and
1032 | `User.human_attribute_name("name")` will return "Full name". These
1033 | translations of the attributes will be used as labels in the views.
1034 |
1035 |
1036 | *
1037 | Separate the texts used in the views from translations of ActiveRecord
1038 | attributes. Place the locale files for the models in a folder `locales/models` and the
1039 | texts used in the views in folder `locales/views`.
1040 | [[link](#organize-locale-files)]
1041 |
1042 | * When organization of the locale files is done with additional directories,
1043 | these directories must be described in the `application.rb` file in order
1044 | to be loaded.
1045 |
1046 | ```Ruby
1047 | # config/application.rb
1048 | config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
1049 | ```
1050 |
1051 | *
1052 | Place the shared localization options, such as date or currency formats, in
1053 | files under the root of the `locales` directory.
1054 | [[link](#shared-localization)]
1055 |
1056 | *
1057 | Use the short form of the I18n methods: `I18n.t` instead of `I18n.translate`
1058 | and `I18n.l` instead of `I18n.localize`.
1059 | [[link](#short-i18n)]
1060 |
1061 | *
1062 | Use "lazy" lookup for the texts used in views. Let's say we have the following
1063 | structure:
1064 | [[link](#lazy-lookup)]
1065 |
1066 | ```
1067 | en:
1068 | users:
1069 | show:
1070 | title: 'User details page'
1071 | ```
1072 |
1073 | The value for `users.show.title` can be looked up in the template
1074 | `app/views/users/show.html.haml` like this:
1075 |
1076 | ```Ruby
1077 | = t '.title'
1078 | ```
1079 |
1080 | *
1081 | Use the dot-separated keys in the controllers and models instead of specifying
1082 | the `:scope` option. The dot-separated call is easier to read and trace the
1083 | hierarchy.
1084 | [[link](#dot-separated-keys)]
1085 |
1086 | ```Ruby
1087 | # bad
1088 | I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
1089 |
1090 | # good
1091 | I18n.t 'activerecord.errors.messages.record_invalid'
1092 | ```
1093 |
1094 | *
1095 | More detailed information about the Rails I18n can be found in the [Rails
1096 | Guides](http://guides.rubyonrails.org/i18n.html)
1097 | [[link](#i18n-guides)]
1098 |
1099 | ## Assets
1100 |
1101 | Use the [assets pipeline](http://guides.rubyonrails.org/asset_pipeline.html) to leverage organization within
1102 | your application.
1103 |
1104 | *
1105 | Reserve `app/assets` for custom stylesheets, javascripts, or images.
1106 | [[link](#reserve-app-assets)]
1107 |
1108 | *
1109 | Use `lib/assets` for your own libraries that don’t really fit into the
1110 | scope of the application.
1111 | [[link](#lib-assets)]
1112 |
1113 | *
1114 | Third party code such as [jQuery](http://jquery.com/) or
1115 | [bootstrap](http://twitter.github.com/bootstrap/) should be placed in
1116 | `vendor/assets`.
1117 | [[link](#vendor-assets)]
1118 |
1119 | *
1120 | When possible, use gemified versions of assets (e.g.
1121 | [jquery-rails](https://github.com/rails/jquery-rails),
1122 | [jquery-ui-rails](https://github.com/joliss/jquery-ui-rails),
1123 | [bootstrap-sass](https://github.com/twbs/bootstrap-sass),
1124 | [zurb-foundation](https://github.com/zurb/foundation)).
1125 | [[link](#gem-assets)]
1126 |
1127 | ## Mailers
1128 |
1129 | *
1130 | Name the mailers `SomethingMailer`. Without the Mailer suffix it isn't
1131 | immediately apparent what's a mailer and which views are related to the
1132 | mailer.
1133 | [[link](#mailer-name)]
1134 |
1135 | *
1136 | Provide both HTML and plain-text view templates.
1137 | [[link](#html-plain-email)]
1138 |
1139 | *
1140 | Enable errors raised on failed mail delivery in your development environment.
1141 | The errors are disabled by default.
1142 | [[link](#enable-delivery-errors)]
1143 |
1144 | ```Ruby
1145 | # config/environments/development.rb
1146 |
1147 | config.action_mailer.raise_delivery_errors = true
1148 | ```
1149 |
1150 | *
1151 | Use a local SMTP server like
1152 | [Mailcatcher](https://github.com/sj26/mailcatcher) in the development
1153 | environment.
1154 | [[link](#local-smtp)]
1155 |
1156 | ```Ruby
1157 | # config/environments/development.rb
1158 |
1159 | config.action_mailer.smtp_settings = {
1160 | address: 'localhost',
1161 | port: 1025,
1162 | # more settings
1163 | }
1164 | ```
1165 |
1166 | *
1167 | Provide default settings for the host name.
1168 | [[link](#default-hostname)]
1169 |
1170 | ```Ruby
1171 | # config/environments/development.rb
1172 | config.action_mailer.default_url_options = { host: "#{local_ip}:3000" }
1173 |
1174 | # config/environments/production.rb
1175 | config.action_mailer.default_url_options = { host: 'your_site.com' }
1176 |
1177 | # in your mailer class
1178 | default_url_options[:host] = 'your_site.com'
1179 | ```
1180 |
1181 | *
1182 | If you need to use a link to your site in an email, always use the `_url`, not
1183 | `_path` methods. The `_url` methods include the host name and the `_path`
1184 | methods don't.
1185 | [[link](#url-not-path-in-email)]
1186 |
1187 | ```Ruby
1188 | # bad
1189 | You can always find more info about this course
1190 | <%= link_to 'here', course_path(@course) %>
1191 |
1192 | # good
1193 | You can always find more info about this course
1194 | <%= link_to 'here', course_url(@course) %>
1195 | ```
1196 |
1197 | *
1198 | Format the from and to addresses properly. Use the following format:
1199 | [[link](#email-addresses)]
1200 |
1201 | ```Ruby
1202 | # in your mailer class
1203 | default from: 'Your Name '
1204 | ```
1205 |
1206 | *
1207 | Make sure that the e-mail delivery method for your test environment is set to
1208 | `test`:
1209 | [[link](#delivery-method-test)]
1210 |
1211 | ```Ruby
1212 | # config/environments/test.rb
1213 |
1214 | config.action_mailer.delivery_method = :test
1215 | ```
1216 |
1217 | *
1218 | The delivery method for development and production should be `smtp`:
1219 | [[link](#delivery-method-smtp)]
1220 |
1221 | ```Ruby
1222 | # config/environments/development.rb, config/environments/production.rb
1223 |
1224 | config.action_mailer.delivery_method = :smtp
1225 | ```
1226 |
1227 | *
1228 | When sending html emails all styles should be inline, as some mail clients
1229 | have problems with external styles. This however makes them harder to maintain
1230 | and leads to code duplication. There are two similar gems that transform the
1231 | styles and put them in the corresponding html tags:
1232 | [premailer-rails](https://github.com/fphilipe/premailer-rails) and
1233 | [roadie](https://github.com/Mange/roadie).
1234 | [[link](#inline-email-styles)]
1235 |
1236 | *
1237 | Sending emails while generating page response should be avoided. It causes
1238 | delays in loading of the page and request can timeout if multiple email are
1239 | sent. To overcome this emails can be sent in background process with the help
1240 | of [sidekiq](https://github.com/mperham/sidekiq) gem.
1241 | [[link](#background-email)]
1242 |
1243 |
1244 | ## Active Support Core Extensions
1245 |
1246 | *
1247 | Prefer Ruby 2.3's safe navigation operator `&.` over `ActiveSupport#try!`.
1248 | [[link](#try-bang)]
1249 |
1250 | ```ruby
1251 | # bad
1252 | obj.try! :fly
1253 |
1254 | # good
1255 | obj&.fly
1256 | ```
1257 |
1258 | *
1259 | Prefer Ruby's Standard Library methods over `ActiveSupport` aliases.
1260 | [[link](#active_support_aliases)]
1261 |
1262 | ```ruby
1263 | # bad
1264 | 'the day'.starts_with? 'th'
1265 | 'the day'.ends_with? 'ay'
1266 |
1267 | # good
1268 | 'the day'.start_with? 'th'
1269 | 'the day'.end_with? 'ay'
1270 | ```
1271 |
1272 | *
1273 | Prefer Ruby's Standard Library over uncommon ActiveSupport extensions.
1274 | [[link](#active_support_extensions)]
1275 |
1276 | ```ruby
1277 | # bad
1278 | (1..50).to_a.forty_two
1279 | 1.in? [1, 2]
1280 | 'day'.in? 'the day'
1281 |
1282 | # good
1283 | (1..50).to_a[41]
1284 | [1, 2].include? 1
1285 | 'the day'.include? 'day'
1286 | ```
1287 |
1288 | *
1289 | Prefer Ruby's comparison operators over ActiveSupport's `Array#inquiry`, `Numeric#inquiry` and `String#inquiry`.
1290 | [[link](#inquiry)]
1291 |
1292 | ```ruby
1293 | # bad - String#inquiry
1294 | ruby = 'two'.inquiry
1295 | ruby.two?
1296 |
1297 | # good
1298 | ruby = 'two'
1299 | ruby == 'two'
1300 |
1301 | # bad - Array#inquiry
1302 | pets = %w(cat dog).inquiry
1303 | pets.gopher?
1304 |
1305 | # good
1306 | pets = %w(cat dog)
1307 | pets.include? 'cat'
1308 |
1309 | # bad - Numeric#inquiry
1310 | 0.positive?
1311 | 0.negative?
1312 |
1313 | # good
1314 | 0 > 0
1315 | 0 < 0
1316 | ```
1317 |
1318 | ## Time
1319 |
1320 | *
1321 | Config your timezone accordingly in `application.rb`.
1322 | [[link](#tz-config)]
1323 |
1324 | ```Ruby
1325 | config.time_zone = 'Eastern European Time'
1326 | # optional - note it can be only :utc or :local (default is :utc)
1327 | config.active_record.default_timezone = :local
1328 | ```
1329 |
1330 | *
1331 | Don't use `Time.parse`.
1332 | [[link](#time-parse)]
1333 |
1334 | ```Ruby
1335 | # bad
1336 | Time.parse('2015-03-02 19:05:37') # => Will assume time string given is in the system's time zone.
1337 |
1338 | # good
1339 | Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00
1340 | ```
1341 |
1342 | *
1343 | Don't use `Time.now`.
1344 | [[link](#time-now)]
1345 |
1346 | ```Ruby
1347 | # bad
1348 | Time.now # => Returns system time and ignores your configured time zone.
1349 |
1350 | # good
1351 | Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00
1352 | Time.current # Same thing but shorter.
1353 | ```
1354 |
1355 | ## Bundler
1356 |
1357 | *
1358 | Put gems used only for development or testing in the appropriate group in the
1359 | Gemfile.
1360 | [[link](#dev-test-gems)]
1361 |
1362 | *
1363 | Use only established gems in your projects. If you're contemplating on
1364 | including some little-known gem you should do a careful review of its source
1365 | code first.
1366 | [[link](#only-good-gems)]
1367 |
1368 | *
1369 | OS-specific gems will by default result in a constantly changing
1370 | `Gemfile.lock` for projects with multiple developers using different operating
1371 | systems. Add all OS X specific gems to a `darwin` group in the Gemfile, and
1372 | all Linux specific gems to a `linux` group:
1373 | [[link](#os-specific-gemfile-locks)]
1374 |
1375 | ```Ruby
1376 | # Gemfile
1377 | group :darwin do
1378 | gem 'rb-fsevent'
1379 | gem 'growl'
1380 | end
1381 |
1382 | group :linux do
1383 | gem 'rb-inotify'
1384 | end
1385 | ```
1386 |
1387 | To require the appropriate gems in the right environment, add the
1388 | following to `config/application.rb`:
1389 |
1390 | ```Ruby
1391 | platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym
1392 | Bundler.require(platform)
1393 | ```
1394 |
1395 | *
1396 | Do not remove the `Gemfile.lock` from version control. This is not some
1397 | randomly generated file - it makes sure that all of your team members get the
1398 | same gem versions when they do a `bundle install`.
1399 | [[link](#gemfile-lock)]
1400 |
1401 | ## Managing processes
1402 |
1403 | *
1404 | If your projects depends on various external processes use
1405 | [foreman](https://github.com/ddollar/foreman) to manage them.
1406 | [[link](#foreman)]
1407 |
1408 | # Further Reading
1409 |
1410 | There are a few excellent resources on Rails style, that you should consider if
1411 | you have time to spare:
1412 |
1413 | * [The Rails 4 Way](http://www.amazon.com/The-Rails-Addison-Wesley-Professional-Ruby/dp/0321944275)
1414 | * [Ruby on Rails Guides](http://guides.rubyonrails.org/)
1415 | * [The RSpec Book](https://pragprog.com/book/achbd/the-rspec-book)
1416 | * [The Cucumber Book](https://pragprog.com/book/hwcuc/the-cucumber-book)
1417 | * [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec)
1418 | * [Rails 4 Test Prescriptions](https://pragprog.com/book/nrtest2/rails-4-test-prescriptions)
1419 | * [Better Specs for RSpec](http://betterspecs.org)
1420 |
1421 | # Contributing
1422 |
1423 | Nothing written in this guide is set in stone. It's my desire to work together
1424 | with everyone interested in Rails coding style, so that we could ultimately
1425 | create a resource that will be beneficial to the entire Ruby community.
1426 |
1427 | Feel free to open tickets or send pull requests with improvements. Thanks in
1428 | advance for your help!
1429 |
1430 | You can also support the project (and RuboCop) with financial contributions via
1431 | [gittip](https://gratipay.com/~bbatsov/).
1432 |
1433 | [](https://gratipay.com/~bbatsov/)
1434 |
1435 | ## How to Contribute?
1436 |
1437 | It's easy, just follow the [contribution guidelines](https://github.com/bbatsov/rails-style-guide/blob/master/CONTRIBUTING.md).
1438 |
1439 | # License
1440 |
1441 | 
1442 | This work is licensed under a [Creative Commons Attribution 3.0 Unported
1443 | License](http://creativecommons.org/licenses/by/3.0/deed.en_US)
1444 |
1445 | # Spread the Word
1446 |
1447 | A community-driven style guide is of little use to a community that doesn't know
1448 | about its existence. Tweet about the guide, share it with your friends and
1449 | colleagues. Every comment, suggestion or opinion we get makes the guide just a
1450 | little bit better. And we want to have the best possible guide, don't we?
1451 |
1452 | Cheers,
1453 | [Bozhidar](https://twitter.com/bbatsov)
1454 |
--------------------------------------------------------------------------------