iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 18
0

我們現在可以選擇要使用什麼資料庫了,今天就來把 Attribube 加上去吧!疑,等等,我們不是在前幾天已經做過了 Attribute 嗎? (你是不是在偷篇數),對,沒錯,我們的確是已經完成了 Attribube,但那是用 method_missing 搭配 define_method 做出來的,不過其實還有效能更快的做法,就是使用 class_eval

有些讀者可能會覺得,那這樣為什麼一開始不直接寫使用 class_eval 呢?其實這個系列也是我邊看書和影片,邊找資料學習的過程,希望透過各種方式,嘗試許多做法,來了解其中的差異

現在我們就來直接進入正題吧!

淺談 class_eval 和 define_method

但其實也不是 class_eval 就特別的快,應該說各有各的優缺點,依照你的架構需求來選擇要用那種方式來做到 Dynamic Method Definitions 的效果,而在 Rails 比較常見到的是 class_eval,我不太確定 Rails 真正選擇 class_eval 的用意,但我們可以透過比較來知道兩個的差異

我們就用 benchmark 來測試看看效能差異吧!

# demo/test.rb

require 'benchmark'

N = 100000
Benchmark.bm(7) do |x|
  x.report("define_method")   {
    class Foo
      N.times { |i| define_method("foo_#{i}") { } }
    end
  }

  x.report("class_eval")   {
    class Foo
      N.times { |i| class_eval "def bar_#{i}; end" }
    end
  }
end

我們先隨便開啟一個資料夾,並且寫兩個 benchmark 的測試,一個是用 define_method定義方法,另一個是用 class_eval,接著來看看結果

              user     system      total        real
define_method  0.215727   0.015528   0.231255 (  0.232347)
class_eval  1.401018   0.038479   1.439497 (  1.443611)

會發現 class_eval 明顯慢很多,疑,休淡幾勒!你一開頭說 class_eval 效能比較好不是嗎?你幹嘛自己打自己臉...,先別急,這個速度指的是 Definition Performance,也就是光指 定義方法 的效能表現,至於為什麼會有這樣的差異,文末的參考連結有比較詳細的探討這裡就不多加深入,只要知道一個結論就是,每執行一次 class_eval 會重新 compiles 一次 定義方法 的指令,但 define_method 不管定義幾個方法,都只會 compiles 一次

那既然 define_method 比較快,何不就用 define_method 就好,剛剛提到的是 定義方法,但呼叫方法可不一樣了,讓我們接著看看下一個測試

# demo/test.rb

require 'benchmark'

class Foo
  define_method("foo") { }
  class_eval 'def bar; end'
end

N = 100000
Benchmark.bm(7) do |x|
  foo = Foo.new
  x.report("define_method")   {
    N.times { foo.bar }
  }

  x.report("class_eval")   {
    N.times { foo.bar }
  }
end

我們用 define_methodclass_eval 各定義了一個方法來執行,看看結果

              user     system      total        real
define_method  0.004096   0.000003   0.004099 (  0.004095)
class_eval  0.004042   0.000002   0.004044 (  0.004043)

好像差不多耶?但我們在定義的 方法 裡面並沒有做任何事情,現實生活中多少會在 方法 裡面做一些運算,讓我們加點工作在 方法 裡面再試試看吧

# demo/test.rb

require 'benchmark'

class Foo
  define_method("foo") { 10.times.map { "foo".length } }
  class_eval 'def bar; 10.times.map { "foo".length }; end'
end

N = 100000
Benchmark.bm(7) do |x|
  foo = Foo.new
  x.report("define_method")   
    N.times { foo.bar }
  }

  x.report("class_eval")   {
    N.times { foo.bar }
  }
end

我們在各自的方法裡面加了一點簡單的運算工作,來看看結果

              user     system      total        real
define_method  0.144662   0.000705   0.145367 (  0.146443)
class_eval  0.138658   0.000306   0.138964 (  0.139517)

嗯!很明顯的出現差距了,用 class_eval 所定義出來的 method 執行速度會比較快

測試結果

我們知道用 class_eval定義方法 雖然定義的速度會比較慢,但定義完後 執行 方法裡面的運算卻比較快

實作加強版的 Attribue

現在我們就來仿照 Rails,來實作動態定義 Attribute,首先打開 persistence.rb,我們這次加強版的內容除了支援 postgresql,也支援新的定義 Attribute 的方式

# mavericks/lib/ mavericks/data_record/persistence.rb

module Mavericks
  module DataRecord
    module Persistence
      def initialize(attributes = {})
        self.class.set_column_to_attribute
        @attributes = attributes
      end
    end
  end
end

我們讓繼承的 class,例如 Task,可以在 new 的時候就定義好 Attribute,定義的方式寫在set_column_to_attribute

def set_column_to_attribute
  columns = self.connection.execute("SELECT column_name FROM information_schema.columns
    WHERE table_name= '#{self.table_name}'").map{|m|  m["column_name"]}
  columns.each{ |column| define_method_attribute(column) }
end

最後用 class_eval 來執行 attribue

def define_method_attribute(name)
  class_eval <<-STR
    def #{name}
      @attributes[:#{name}] || @attributes["#{name}"]
    end

    def #{name}=(value)
      @attributes[:#{name}] = value
    end
  STR
end

實作原理跟前面不管是 sqlite_model.rb 或是 file_model.rb 都很類似就不多說明,差別在於我們這次用 class_eval,而且是在 new 一個物件時就產生,所以不會有 method_missing 的效能問題產生

接著一樣在 just_do 的 sqlite_test.rb 測試看看

# just_do/sqlite_test.rb

require 'mavericks/data_record'

Mavericks::DataRecord::Base.establish_connection

class Task < Mavericks::DataRecord::Base
end

task = Task.new(title: '鐵人30')
puts task.title
# 鐵人30

task.title = '鐵人40'
puts task.title
# 鐵人40

如果成功印出內容,代表我們成功了!

參考:Dynamic Method Definitions


上一篇
[DAY 17] 復刻 Rails - ORM-威力加強版
下一篇
[DAY 19] 復刻 Rails - ORM - 加上 where
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言