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
,我們用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
的用法。
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
專案中,有很多個controller
的index
的行為都是呈現 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
和include
一樣,我們可以透過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
,就可以繼承類別方法。
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"
我們可以透過 #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
。
Noti.ancestors
#=> [Noti, Notification::Helper, Object, Kernel, BasicObject]
ancestors
意思為祖先,如果有看幽遊白書的讀者們,可以知道雷禪是幽助的16代前的祖先。長相如下 ⬇️
幽助.ancestors
#=> [幽助, ..., ..., ..., ..., ..., ..., ..., ..., ..., ..., ..., 雷禪]
在繼承鍊裡面的方法撞名時,ancestors
可以幫助我們判斷,最後是哪個方法覆寫了其他同名的方法。假設Noti
, Notification::Helper
都有方法foo
,則Noti
會覆蓋Notification::Helper
的foo
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 的繼承!