iT邦幫忙

2021 iThome 鐵人賽

DAY 14
0
Modern Web

初階 Rails 工程師的養成系列 第 14

Day14. Module & #extend #prepend #include - Ruby 繼承 part1

Day14-15 一共會介紹 Ruby的2類、4種繼承方式。

Day2 我們提到 Ruby 為單一繼承的語言,若想要實現多重繼承,可以使用mixin的方式達到相似的效果。

class Warrior
  include Sword
  include Shield
end

如上程式碼所示,上方的Warrior使用了 include 繼承了Sword, Shield裡面所有的方法。除了include以外,還有prepend, extend兩種方式可以繼承。使用module繼承的方式我們稱作 mixin

以下為今天會介紹的內容

  • module
  • 實體方法繼承
  • 類別方法繼承
  • 繼承類別/實體方法
  • 總結

module & namespace

Module 的其中一個功能可以做為路徑NameSpace,我們用2個例子說明

#=========== 例1 ===========#
class Admin::Order
end

# 可以寫成以下形式
module Admin
  class Order
  end
end

#=========== 例2 ===========#
class Admin::Order < Admin::Base
end

# 可以寫成以下形式
module Admin
  class Order < Base
  end
end

Admin::Order  #=> Admin::Order

當我們搜尋Admin::Order,能夠找到剛剛宣告的類別,而:: 為路徑的概念。為了說明::的使用方法,我們以更複雜的例子來說明

module Taipei
  class Car
    TITLE = '北部車款介紹'
    
    # 廠牌: 在這裡定義常數
    module Brand
      FORD = 0
      TESLA = 1
    end
    
    def self.drive
      'boo boo'
    end
  end
end

Taipei::Car::TITLE         #=> "北部車款介紹"
Taipei::Car::Brand::TESLA  #=> 1
Taipei::Car.drive          #=> "boo boo"

接著,我們開始講module層級的繼承

include ➡️ 繼承實體方法

include是最被廣泛使用,也是最簡單的繼承方式,以下我們先說明include的用法。

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_name
    [material.capitalize, 'Sword'].join(' ')
  end
end

class Warrior
  include Sword
end

warrior = Warrior.new
warrior.sword_name        #=> "Silver Sword"

warrior.material = 'Metal'
warrior.sword_name         #=> "Metal Sword"

戰士的劍預設為銀劍,透過setter將材質改為鐵,變為鐵劍。

關於 mattr_accessor 的用法可以參考Ruby文件,在這邊我們就把它當作 Day12 提到的attr_accessor ,我們在Day13提到,實體方法不一定要在initialize裡面宣告,這邊的實體變數定義在module裡面,預設為silver

在我其中一個Rails專案中,有很多個controllerindex的行為都是呈現 DataTable 的組成表單。因此我把共同的行為都寫在module DataTable,待需要時就include 進來,就不用每使用一個Datatable就寫那麼多程式碼。

接著我們來探討,include的覆寫與繼承問題。下方的例子為在class定義了sword_name覆寫了原本的sword_name,這種覆寫的行為不管在使用繼承還是mixin都稱為monkey patch

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_name
    [material.capitalize, 'Sword'].join(' ')
  end
end

class Warrior
  include Sword
  
  def sword_name
    '倚天劍'
  end 
end

warrior = Warrior.new
warrior.sword_name       #=> "倚天劍" 

除了覆寫以外,我們能不能保留原本的行為?使用include的答案是不行,但我們可以透過其他方式調用!

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_meterial
    meterial
  end
end

class Warrior
  include Sword
  
  def sword_name
    [sword_meterial.capitalize, '劍'].join(' ')
  end 
end

warrior = Warrior.new
warrior.material = '長'
warrior.sword_name       #=> "長劍"

使用include我們無法保持原本的行為,但透過接下來我們要介紹的prepend,可以使用#super 來達到我們要的目的。

prepend ➡️ 繼承實體方法

prependinclude一樣,我們可以透過prepend繼承實體方法,不過prepend卻有一些用法使得可以和include區隔。

prepend的詞意為前面,在繼承鏈中代表著會覆蓋原本class的方法。首先,我們來看prepend是如何覆蓋原本的類別方法

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_name
    [material.capitalize, 'Sword'].join(' ')
  end
end

class Warrior
  prepend Sword
  
  def sword_name
     '倚天劍'
  end 
end

warrior = Warrior.new
warrior.sword_name       #=> "Silver Sword"

除覆蓋以外,我們提到可以用關鍵字#super來保留原本的用法。下列為使用 #super的執行結果

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_name
    puts %Q(#========= #{[material.capitalize, 'Sword'].join(' ')} =========#)
    super
  end
end

class Warrior
  prepend Sword
  
  def sword_name
     '倚天劍'
  end 
end

warrior = Warrior.new
warrior.sword_name       

#== 先印:   #========= Silver Sword =========#
#== 再回傳: => "Silver Sword"

這種方式很像是controller 常使用的 before_action 行為,反之如果我們改成以下寫法,就類似after_action

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_name
    super
    puts %Q(#========= #{[material.capitalize, 'Sword'].join(' ')} =========#)
  end
end

extend ➡️ 繼承類別方法

當我們使用extend,就可以繼承類別方法。

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_name
    [material.capitalize, 'Sword'].join(' ')
  end
end

class Warrior
  extend Sword
end

Warrior.sword_name  #=> "Silver Sword"

include & extend 的組合技 ➡️ 繼承類別/實體方法

我們可以透過 #included的方法,一併使用類別方法和實體方法。

module Notification::Helper
  # 實體方法寫在這裡
  def self.included(base)
    puts "MyModule is included to #{base}"
    
    base.extend(ClassMethods)
    
    # 等同於 def foo; 'foo' end
    define_method :foo, -> {'foo'}
    
    #=> add instance method
    subclass.class_eval do
    end
    
    #=> add class method
    subclass.instance_eval do
    end
  end

  # 類別方法寫在這裡
  module ClassMethods
    def call(params = {})
      new(params).call
    end
    
     # 等同於 def bar; 'bar' end
    define_method :bar, -> {'bar'}
  end
  
  def call
    {
      title: @title,
      content: @content
    }
  end
end

class Noti
  include Notification::Helper
  
  def initialize(**argv)
    @title, @content = argv[:title], argv[:content]
  end
end

#==            # MyModule is included to Noti
Noti.bar       #=> bar
Noti.new.foo   #=> foo
Noti.call(title: '我是標題', content: '我是內文')  #=> {:title=>"我是標題", :content=>"我是內文"}

包括#included,一共有以下這些hook可以使用。我們今天講了前三個,明天會講最後一個

  • Module#included
  • Module#extended
  • Module#prepended
  • Class#inherited

ancestors

若想要看截至為止的繼承鍊,我們可以使用ancestors

Noti.ancestors
#=> [Noti, Notification::Helper, Object, Kernel, BasicObject]

ancestors 意思為祖先,如果有看幽遊白書的讀者們,可以知道雷禪是幽助的16代前的祖先。長相如下 ⬇️

幽助.ancestors
#=> [幽助, ..., ..., ..., ..., ..., ..., ..., ..., ..., ..., ..., 雷禪]

在繼承鍊裡面的方法撞名時,ancestors 可以幫助我們判斷,最後是哪個方法覆寫了其他同名的方法。假設Noti, Notification::Helper 都有方法foo,則Noti會覆蓋Notification::Helperfoo

module A
end

module B
end

class Wanc
  include A
  include B
end

class Gang
  prepend A
  prepend B
end

Wanc.ancestors #=> [Wanc, B, A, Object, Kernel, BasicObject]
Gang.ancestors #=> [B, A, Gang, Object, Kernel, BasicObject]

搭配include, prepend的用法搭配ancestors查看,可以看到先後順序不一樣。

總結

以下總結class, module之間用法的差別。

Class Module
可初始化
用法 創建物件 namespace & mixin 繼承
繼承 可以 不行
可用的hook #inherited #included, #extended, #prepended

明天開始講 Ruby 的繼承!

參考資料


上一篇
Day13. class_eval & instance_eval - 解答什麼是 MetaClass & Singleton
下一篇
Day15. Inheritance & Super - Ruby 繼承 part2
系列文
初階 Rails 工程師的養成34

尚未有邦友留言

立即登入留言