Rails4 Patterns I: Models
今天起春節希望每天介紹 Rails4 Patterns 內的一個章節給大家,這是一個在 Code School 的課程,原址:https://www.codeschool.com/courses/rails-4-patterns
肥肥的 Controller 是不好的
原因有幾個:
- 會難以理解
- 商業邏輯難以統整
- 程式碼間容易衝突
- 難以開發新功能
像是以下程式碼
class ItemsController < ApplicationController
def publish
if @item.is_approved?
@item.published_on = Time.now
if @item.save
flash[:notice] = "Your item published!"
else
flash[:notice] = "There was an error."
end
else
flash[:notice] = "There was an error."
end
redirect_to @item
end
end
當時間的推進,應用的發展,條件式只會越來越複雜
if @item.is_approved? # 原先的條件式
# ->
if @item.is_approved? && @iterm.user != "Loblaw" # 新增條件
這裡切記一個原則:「Tell, Don't Ask」,應該要直接的告訴物件它要做什麼事情。而不是透過問句的方式去確認目前它的狀態是什麼後,再決定作什麼事情。
商業邏輯該丟到 Models 內
如剛剛的 controller,我們可以將邏輯性的功能包裝丟至 model 內,那對 controller 而言就可以符合 Tell, Don't Ask 原則。
# app/controllers/items_controller.rb
class ItemsController < ApplicationController
def publish
if @item.publish # 明確知道這邊的動作是將 item 發佈
flash[:notice] = "Your item published!"
else
flash[:notice] = "There was an error."
end
redirect_to @item
end
end
# app/models/item.rb
class Item < ActiveRecord::Base
belongs_to :user
def publish
if !is_approved? || user == 'Loblaw'
return false
end
self.published_on = Time.now
self.save
end
end
函式為確保是簡潔與可讀性的程式碼,特別將 if-else 的邏輯單純化,如果一個 if 就能乾淨俐落,為什麼還要來個 else,大家都愛直線,不要拐彎抹角阿!
所以這邊才會是
if !is_approved? || user == 'Loblaw'
return false
end
...
而非
# wrong case
if is_approved? || user != 'Loblaw'
...
else
return false
end
避免在 Callbacks 中呼叫其他功用的物件
因為 callback 本身就像是一個物件生命週期的一個環節,如果牽扯到其他的物件,不只讓人在看程式碼會覺得有點跳,甚至因為交流密切,間接影響到物件的資料庫運作週期。
class User < ActiveRecord::Base
before_create :set_token
protected
def set_token
self.token = TokenGenerator.create(self) # another class object
end
end
為此建立一個客製化的函式
# app/models/user.rb
class User < ActiveRecord::Base
def register # 另外拉一個函式,專作註冊
self.token = TokenGenerator.create(self)
save
end
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.register # 使用客製化的函式
redirect_to @user, notice: 'Success'
else
...
end
end
end
Callbacks 應該是用來作為更改欄位值或其他內在因素為主
# app/models/user.rb
class User < ActiveRecord::Base
before_create :set_name
protected
def set_name
self.name = self.login.capitalize if name.blank?
end
end
也要特別留意,callbacks 放在 protected 內是 Rails 的 Convention,務必要遵守
不是所有的 Models 都一定要是 ActiveRecord
以被檢舉的使用者需要的停權動作為例,裡面牽扯到許多要考慮的層面,例如停權後就不能發言、不能登入等等
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def suspend
@user = User.find(params[:id])
@user.suspend!
redirect_to @user, notice: 'Successfully suspended.'
end
end
# app/models/user.rb
class User < ActiveRecord::Base
has_many :items
has_many :reviews
def suspend!
self.class.transaction do # transaction 包起來可以讓這些動作對於資料庫來說只是一件事情
self.update!(is_approved: false)
self.items.each { |item| item.update!(is_approved: false) }
self.reviews.each { |review| review.update!(is_approved: false) }
end
end
end
以上例來說,問題是同個函式內包含太多的邏輯
同個 class 裡面太多邏輯,可以拆成各自負責的工作。也要注意函式牽扯到太多其他的物件,可能會導致「God Object」現象發生,就是指作太多事情的意思。
class User < ActiveRecord::Base
...
def suspend!
self.class.transaction do
disapprove_user!
disapprove_items!
disapprove_reviews!
end
end
def disapprove_user!
def disapprove_items!
def disapprove_reviews!
end
抽象化:不是所有跟使用者有關係就得都在使用者的 Model 內
以停權使用者這件事情來說,抽象化有幾個好處:
- PORO: Plain Old Ruby Object
- 只需一個正規的 instance method
- 這個類別就只需要專注一件事情就好
# app/models/user_suspension.rb
class UserSuspension
def initialize(user)
@user = user
end
def create!
@user.class.transaction do
disapprove_user!
disapprove_items!
disapprove_reviews!
end
end
private
def disapprove_user!
def disapprove_items!
def disapprove_reviews!
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def suspend
@user = User.find(params[:id])
suspension = UserSuspension.new(@user)
suspension.create!
redirect_to @user, notice: 'Successfully suspended.'
end
end
其他常見的抽象化:
- UserRegistration
- PlanSubscription
- CreditCardCharge
- ShoppingCart
小結一下,所以常會使用非 ActiveRecord 的方式將獨立且唯一的商業邏輯搬出來成一個新的類別。