昨天我們示範了如何在 module 裡面用 super
來擴充方法,今天就來將昨天學到的觀念應用在我們的框架裡面
Rails 在 ActionController 有一個有趣的機制,就是分成ActionController::Metal
和 ActionController::Base
,會這樣分的原因是,有時候你不想要用到每一個 Controller 的功能,就可以使用 ActionController::Metal
做客製化,當然如果想要完整的 Controller, 就使用 ActionController::Metal
,所以在 Rails 的原始碼你會看到
module ActionController
class Base < Metal
# .
# .
# (略)
end
end
用 Base
來繼承 Metal
,如果要擴充呢?就像昨天舉例的 Child
和 Parent
的做法一樣,可以在 Child
裡面做 include,另外昨天也了解到 The Include-and-Extend Trick
的技巧,用一行 include 來做到許多事情
所以我們希望 Mavericks 也可以做到一樣的事情,就可以這樣規劃
# mavericks/lib/action_controller/metal.rb
module ActionController
class Metal
# 從 Base.rb 複製程式碼過來
# .
# .
# (略)
end
end
接著在 Base.rb
繼承 Metal
module ActionController
class Base < Metal
end
end
別忘了在 action_controller.rb
autoload 所有相關的檔案
# mavericks/lib/action_controller/base.rb
module ActionController
autoload :Base, "action_controller/base"
autoload :Metal, "action_controller/Metal"
end
現在我們知道 Metal
裡面放的是 Controller 最基本的功能,Base
裡面放的是擴充的功能,所以如果要做 before_action
,應該要放在 Base
裡面,並且用 include module 的方式來擴充想要的功能
現在我們直接來實作 before_action
的部分,建立一個 module,程式碼總共分兩部份
# mavericks/lib/action_controller/callbacks.rb
module ActionController
module Callbacks
# part1
class Callback
def initialize(method, options)
@method = method
@options = options
end
def match?(action)
if @options[:only]
@options[:only].include? action.to_sym
else
true
end
end
def call(controller)
controller.send @method
end
end
# part2
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def before_action(method, options = {})
before_actions << Callback.new(method, options)
end
def before_actions
@before_action ||= []
end
end
def process(action)
self.class.before_actions.each do |callback|
if callback.match?(action)
callback.call(self)
end
end
super
end
end
end
我們先從 part2 開始說起,self.included
昨天有提過他是 Ruby 的 Hook Method
,可以在 include
被觸發,裡面做得就是 extend 的事情,利用這樣的技巧,可以同時達到 include
和 extend
的效果
def self.included(base)
base.extend ClassMethods
end
ClassMethods
裡面包含了兩個 class method
,其中 before_action
就是我們之後在 just_do 的 Controller 會呼叫的方法,我們將每一個 before_action
又另外包成一個 Callback 的物件,並且放到 @before_action
保存起來
module ClassMethods
def before_action(method, options = {})
before_actions << Callback.new(method, options)
end
def before_actions
@before_action ||= []
end
end
part2 的最後,我們擴充了 process,這個用法就是昨天提到如何在 module
運用 super
,super
裡面其實就是 Action 原本要做的事情,如果 before_action
有 method 需要執行,我們就會在 super
前做事
def process(action)
self.class.before_actions.each do |callback|
if callback.match?(action)
callback.call(self)
end
end
super
end
還記得我們剛剛把 before_aciton
包成一個一個的 Callback
的物件,利用 callback 的 match 來檢查這個 Action 是不是符合條件(例如 only 條件),如果符合的話,用 call
來執行這個 method
,接下來就到 part1 看看怎麼實作 Callback
我們有提到要把 before_action
包成一個一個 Callback 的物件,現在就來實作這部分,這裡主要有兩個 method,一個是 match?
,另一個是 call
class Callback
def initialize(method, options)
@method = method
@options = options
end
def match?(action)
if @options[:only]
@options[:only].include? action.to_sym
else
true
end
end
def call(controller)
controller.send @method
end
end
在 match?
裡面,我們做了 only
的比對,如果有符合的話,這個 callback 會執行 call
,裡面就會用 send
來執行要做的方法
基本的 before_action
做差不多了,最後一樣記得做 autoload,還有在 base.rb 做 include
# mavericks/lib/action_controller/base.rb
module ActionController
class Base < Metal
include Callbacks
end
end
# mavericks/lib/action_controller.rb
module ActionController
autoload :Base, "action_controller/base"
autoload :Callbacks, 'action_controller/callbacks'
autoload :Metal, "action_controller/Metal"
end
最後回到 just_do 來看看實作後的成果吧
# just_do/app/controllers/tasks_controller.rb
class TasksController < ActionController::Base
before_action :find_task, only:[:show]
def index
@tasks = Task.all
end
def show; end
private
def find_task
@task = Task.find(params['id'])
end
end
開啟伺服器用瀏覽器執行 Action show,應該會先執行 find_task
,這樣代表我們完成了!