Ruby Modules — include、extend 與 prepend

Ruby 中的 Module 同時存在 Namespace 跟 Mixin 兩種特性

  • Namespace 有點像是變數與 method 的 sandbox,包起來後可以避免命名上的衝突,這點與大部分程式語言相同。

  • Mixin 則是提供了在繼承關係外,仍有共用方法的可能性,避免產生 ”如果人可以飛,則人必須是鳥” 的情況,實現的感覺更像是引用而不是疊加(繼承),而 Module 提供了三種方式,分別是 include、prepend、extend。

Include 是常見的用法,只要在 class 內 include module,就能在物件實例化後呼叫

module A
  def say
    'A'
  end
end

class User
  include A
end
~$ User.new.say
"A"

需要注意 include 後的 module 並不是併入 class 本身,而是插入到 class 的繼承鏈當中,位置介於 superclass 與物件本身之間

~$ User.ancestors
[User, A, Object, Kernel, BasicObject]

所以當我們有多個 include 的時候就會產生順序性的問題

module A
  def say
    'A'
  end
end

module B
  def say
    'B'
  end
end

class User
  include A
  include B
end
~$ User.ancestors
[User, B, A, Object, Kernel, BasicObject]

~$ User.new.say
"B"

因為 Compiler 在繼承鏈中從左邊開始尋找 method,一旦找到就會返回,後面重複名稱的 method 自然沒有被執行的機會。

Extend 可以讓 module methods 直接在物件上呼叫,不同於 include 需要實例化後才可以使用

module A
  def say
    'A'
  end
end

class User
  extend A
end
~$ User.new.say
NoMethodError: undefined method `say'

~$ User.say
"A"

extend 不會將 module 插入物件繼承鏈當中

~$ User.ancestors
[User, Object, Kernel, BasicObject]

~$ User.new.say
NoMethodError: undefined method `say'

可以從 singleton class 找到 extend 的 module

~$ User.singleton_class.ancestors
[#<Class:User>, A, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]

由於是插入到 singleton class 右方,若有定義 class method,會優先找到 class method

module A
  def say
    'A'
  end
end

class User
  extend A

  def self.say
    'Hi'
  end
end
~$ User.say
"Hi"

Prepend 則是將 module 插入到物件本身的繼承鏈前方

module A
  def say
    'A'
  end
end

class User
  prepend A

  def say
    'Hi'
  end
end
~$ User.new.say
"A"

繼承鏈

~$ User.ancestors
[A, User, Object, Kernel, BasicObject]

在 prepend 多個的時候也要考慮順序性問題

module A
  def say
    'A'
  end
end

module B
  def say
    'B'
  end
end

class User
  prepend A
  prepend B

  def say
    'Hi'
  end
end
~$ User.ancestors
[B, A, User, Object, Kernel, BasicObject]

// 從最左邊開始尋找 method
~$ User.new.say
"B"

# 總結

繼承鏈由左向右開始尋找 繼承鏈插入的位置

  • Include — 放到 self.class 的右邊
  • Extend — 放到 self.singleton_class 的右邊
  • Prepend —放到 self.class 的最左邊

Test on Ruby 2.6.5

# references

  • http://rubylearning.com/satishtalim/modules_mixins.html
  • https://medium.com/@leo_hetsch/ruby-modules-include-vs-prepend-vs-extend-f09837a5b073
  • https://chunksofco.de/rubys-prepend-how-is-it-useful-d3bba8d11a95