Nic Lin's Blog

喜歡在地上滾的工程師

Dynamic Method 與 Dispatch的技巧

Dynamic Method

最近因為在應用上覺得了解Ruby的底層可以在程式撰寫的時候有佳的可讀及維護性,一方面也是在網路上看到的資源做個筆記,之前累積太多的技術債該是找時間來還了,這篇就來講講如何讓Ruby做到程式碼自己生產程式碼的一些小tips。

仰賴Ruby的特性,我們可以讓他動態產生method,動態產生是只程式在runtime情況下生成,並不是事先寫好的,而這種應用的情況常常發生於如下的範例情況:

先設定一個SourceData的class,可以從這裡拿到get_name之類的實例方法(instance method)取得實例變數(instance variables)的值。

class SourceData

  def initialize(user)
    @user = user
  end

  def get_name
    puts "#{@user}_name"
  end

  def get_address
    puts "#{@user}_address"
  end

  def get_phone
    puts "#{@user}_phone"
  end

end

我們從另一個叫做User的class,透過SourceData來獲取他的實例變數

class User
  def initialize(source_data)
    @source_data = source_data
  end
  
  def name
    @source_data.get_name
  end
  
  def address
     @source_data.get_address
  end
  
  def phone
     @source_data.get_phone
  end
  
 end

這裡會產生一個問題,如果我們在SourceData的class新增一個get_email的method,勢必得回去User的class裡面多寫一次email的method,這違反了Open/closed principle,在軟體設計原則有個最常聽到的DRY(Don’t Repeat Yourself),簡單來說不需要一直重複寫相同或是類似的程式碼,除了可以讓code看起來更DRY一些,也在日後維護的時候有更高的可讀性與彈性。

舉例來說如上例的程式碼,實作的內容都相近差不多的,其實我們可以用動態定義的方式來整理這些像是重複的程式碼,Ruby定義動態方法使用define_method

define_mehtod用法

define_method :hello do |param|
    puts "Hello, #{param}"
end

這樣一來就可以用define_method來整理剛剛那段看起來有點重複的程式碼

class User

  def initialize(source_data)
    @source_data = source_data
    @source_data.methods.grep(/^get_(.*)$/) {
      User.define_infos($1)
    }
  end

  def self.define_infos(name)
    define_method(name) {
      @source_data.send("get_#{name}")
    }
  end
end

這樣寫的好處是當程式需要延展跟擴充的時候能夠有足夠的彈性,我們不容易牽一髮動全身,可以專注在焦點本身,以上述例子來說,不論SourceData如何新增get_*的實例方法時,都能更優雅以動態方式在User class產生method。

Dynamic Dispatch

如果this_object.call_method,那麼this_object就稱為接收者,Dynamic Dispatch 是指依照接收者的物件類型,來決定呼叫的方法,這個技巧會非常需要用到 send。

send用法

send.其實一樣,不同的地方是他可以把方法當成參數,傳送給接收者去call_mehtod。

class A
    def call_method
    puts "This is a method in class A"
    end
  end
end

我們可以透過send以及.來呼叫實例方法A

a = A.new
a.call_method                           # => "This is a method in class A"
a.send(:call_method)            # => "This is a method in class A"

這裡把 call_method 當成 send 的參數(可以是 hash,也可以是 string),然後丟給接收者;既然方法可以被當成物件來丟,意思也就是可以把一些方法存起來成一個 array,然後依照不同需求的時候丟給接收者使用。

這樣一來我們學會使用send就跟上面所提到的Dynamic Method有異曲同工之妙

因為send可以當作動態定義來呼叫,所以可以試著這樣寫

a = A.new
str = "call_method"
h = :call_method
a.send(str)         # => "This is a method in class A"
a.send(h)           # => "This is a method in class A"

以上就是動態定義method跟動態call_method的一些小方法。

ref -Ruby 的 Dynamic Dispatch 技巧 -Ruby 也可這樣寫 -http://blog.chh.tw/posts/dynamic-method/

comments powered by Disqus