iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 23
0
Modern Web

向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List系列 第 23

[DAY 23] 復刻 Rails - 用 Rails 的方式整理程式碼 ActionController

經過這幾天的重構整理,看起來越來越有點樣子了,雖然剩下 7 天(扣除掉最後一天完賽宣言衝篇數),但其實還有很多功能還沒實作完成,尤其是看到 application.rb 裡面 call 那段程式碼目前的處理,就覺得還是很母湯,那就繼續一步一步看怎麼改善吧!

一樣的起手式 ActionController::Base

還記得 Active Record 和 Active Support 的架構嗎?Action Controller 也是一樣,先建立一個 base.rb 的檔案

# mavericks/lib/action_controller/base.rb

module ActionController
  class Base
  end
end

接著將原先的 controller.rb 裡面的程式碼搬移到 base.rb 底下

因為原先的 controller.rb 有用到 erubi,所以連同 action_contorller 要一起寫進 all.rb 裡面

# mavericks/lib/mavericks/all.rb

require 'erubi'
require 'yaml'
require "mavericks"
require "active_support"
require "active_record"
require "action_controller"
require "mavericks/routing"
# 原先的 controller 可以拿掉
# require "mavericks/controller"

接著一樣要建立 action_controller.rb 做 autoload

# mavericks/lib/action_controller.rb

module ActionController
  autoload :Base, "action_controller/base"
end

最後別忘了 just_do 的 TasksController 要改繼承 ActionController::Base

# just_do/app/controllers/tasks_controller.rb

class TasksController < ActionController::Base
  def index
    @tasks = Task.all
  end

  def show
    @task = Task.find(params['id'])
  end
end

另外前面我們曾經實作過 Controller 呼叫 Action 的功能,但其實這個做法會有些問題,會不好處理 before_action 之類的問題,所以為了要實作 before_action,我們需要用一個 method 將他包起來處理

# mavericks/lib/mavericks/application.rb

# (略)
# .
# .
def call
  klass, act =  get_controller_and_action(env)
  controller = klass.new(env)
  
  # 原本的寫法
  # controller.send(act)
  # 改成這個
  controller.process(act)
  
  # .
  # .
  # (略)
# mavericks/lib/action_controller/base.rb

module ActionController
  class Base
    attr_reader :env, :content

    def initialize(env)
      @env = env
      @content = nil
    end

    # 加上一個 process 來執行 Action
    def process(action)
      send action
    end

# .
# .
# (略)

這樣就完成基本架構,接著就看看要加上那些功能吧!

關於 The Include-and-Extend Trick

在實作 before_action,我們先來看看 Rails 的 before_action 寫法

# 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

我們預期希望像 Rails 一樣有一個 before_action 的 callback,可以讓我們在指定的 Action 之前先處理好一些事情,為了要做到這樣的效果,我們需要在 Controller 上面建立一個 before_action 的 class method,因為 callback 不只是只有 before_action,所以我們勢必要做一個 callback 的 module

# mavericks/lib/action_controller/callbacks.rb

module ActionController
  module Callbacks
    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def before_action(method, options={})
        # TODO
      end
    end
  end
end

接著只要在 base.rb 裡面 include Callbacks 就可以有...等等,為什麼是用 include Callbacks?而不是 extend Callbacks

其實你仔細看 callbacks module,長的跟平常的 module 有點不太一樣,這裡我們用了 Ruby 的 Hook Methods 的技巧來實作Metaprogramming 所提到的 The Include-and-Extend Trick,這樣的方式可以讓我們在 include 時,同時新增 instance method 和 class method,在 Rails 這個 framework 裡面是 是一個很常見的技巧

仔細看 self.included 的部分

def self.included(base)
  base.extend ClassMethods
end

這就是剛剛所提到的 Hook Methods,在呼叫 include 的時候,會觸發這個方法,其中參數 base 就是看那個 Class 觸發了這個 Hook,而裡面 base.extend ClassMethods 才是我們真正實作 extend method 的地方,至於 ClassMethods 其實也就是一個 module,裡面包含了一個 before_action

關於 super

在了解怎麼同時 include 和 extend method 之後,接著我們就繼續實作 before_action 的部分,不過在實作前,我們還要再聊聊另一個觀念,就是關於 Ruby 的 super

class Parent
  def say
    puts "I am parent"
  end
end

module A
  def say
    puts "I am A"
    super
  end
end

module B
  def say
    puts "I am B"
    super
  end
end

class Parent
  include A, B
end

Parent.new.say

上面這個例子我們有一個 class Parent,為了要加入 module AB 的方法,在底下做 Open Classes 並且 include AB,預期希望會是這樣的結果

I am A
I am B
I am parent

但實際上只有

I am parent

為什麼?

那是因為 Ruby 有繼承鏈的概念,什麼意思?我們可以在剛剛的程式碼底下加上這段程式碼

puts Parent.ancestors

你會發現這串東西,這個就是所謂的繼承鏈

Parent
A
B
Object
Kernel
BasicObject

這也是 Ruby 在找尋「方法」時的順序,也就是說 Ruby 在找 say 這個方法時,會先在 Parent 裡面找到,而在 Parent 裡面的 say 並沒有呼叫 super,所以 Ruby 執行完 Parentsay 以後就不會再執行 AB 裡面的 say

反過來說,如果我們想辦法把 AB 擺在 Parent,是不是就可以做到我們要的擴充方法效果?

是的,Ruby 剛好就有提供這樣的方法,我們把 include 換成 prepend

class Parent
  prepend A, B
end

接著再執行一次,就會發現是我們要的結果,再檢查一次繼承鏈

A
B
Parent
Object
Kernel
BasicObject

會發現 AB 確實排在 Parent 前面,也因為 AB 都有呼叫 super,所以就會繼續執行 Parent 裡面的程式碼

但是其實還有另一個做法,就是建立一個子類別來 include AB,像是這樣

class Parent
  def say
    puts "I am parent"
  end
end

module A
  def say
    puts "I am A"
    super
  end
end

module B
  def say
    puts "I am B"
    super
  end
end

# 建立一個子類別
class Child < Parent
  include A, B
end

puts Child.ancestors

你可以看到繼承鏈變成這樣

Child
A
B
Parent
Object
Kernel
BasicObject

利用這樣的技巧,我們就可以在子類別做 include,來擴充父類別的方法,一種簡單用乾淨的做法,會提到這個是因為 Rails 也是利用這個技巧,來實作擴充的效果,明天就會繼續來實作這部分

Mavericks 程式碼
https://github.com/apayu/mavericks


上一篇
[DAY 22]復刻 Rails - Application 啟動過程
下一篇
[DAY 24] 復刻 Rails - 關於 before_action
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30

尚未有邦友留言

立即登入留言