介紹一種跟類相似的構造:模組(module)。在設計程式的時候,我們會把大的組件分割成小塊,你可以混合與匹配對象的行為。
跟類差不多,模組也捆綁方法與常量。不一樣的是,模組沒有執行個體。你可以把擁有特定功能的模組放到類或某個特定的對象裡使用。
Class 這個類是 Module 類的一個子類,也就是所有的 class 對象應該也是一個 module 對象。
上午10:26 ***
建立與使用模組
上午10:26 ***
module MyFirstModule
def say_hello
puts 'hello'
end
end
我們建立了類以後可以去建立這個類的執行個體,執行個體可以執行類裡面的執行個體方法。不過模組是沒有執行個體的,模組可以混合(mixed in,mix-in,mixin)到類裡面,用的方法是 include 還有 prepend 。這樣類的執行個體就可以使用在模組裡面定義的執行個體方法了。
使用一下上面定義的那個模組:
class ModuleTester
include MyFirstModule
end
mt = ModuleTester.new
mt.say_hello
上面的 ModuleTester 對象調用了 say_hello 這個方法,這樣會輸出一個 hello 。這個方法是混合到 ModuleTester 類裡面的 MyFirstModule 裡定義的執行個體方法。
在類裡混合使用模組很像是去繼承一個 superclass 。比如 B 類繼承了 A 類,這樣 B 類的執行個體就可以調用來自 A 類的執行個體方法。再比如 C 類混合了模組 M,這樣 C 類的執行個體就可以調用在模組 M 裡定義的方法。繼承類與混合模組的區別是,你可以在一個類裡混合使用多個模組,你不能讓一個類去繼承多個類。
模組可以讓我們在多個類之間共用它的代碼,因為任何的類都可以混合使用同一個模組。
建立一個模組
模組給我們提供了收集與封裝行為的方法。下面我們可以寫一個模組,去封裝一些像堆(stack)的特性,然後把模組混合到一個或多個類裡面,這樣模組裡的行為就會傳授給對象。
堆(stack)是一種資料格式,後進來的,先出去(LIFO:last in, first out)。比如一堆盤子,用的第一個盤子,是最後一次放到這堆裡的那個。經常跟堆一起討論的還有個概念:隊列(queue),它是先進來的,先出去(FIFO),比如在民政局視窗前排的隊,排在第一位置上的人最先辦完手續。
先把下面代碼放到 stacklike.rb 檔案裡:
module Stacklike
def stack
@stack ||= []
end
def add_to_stack(obj)
stack.push(obj)
end
def take_from_stack
stack.pop
end
end
在上面的 Stacklike 模組裡,我們使用了一個數組來表示堆,這個數組會儲存在一個執行個體變數裡面,名字是 @stack,這個執行個體變數可以通過 stack 這個方法得到。這個方法使用了條件設定變數,||= 是一個操作符,只有變數不是 nil 或 false 的時候,才會讓這個變數的值等於一個特定的值。這裡就是第一次調用 stack 的時候,它會設定 @stack 讓它等於一個空白的數組,後續再次調用的時候,@stack 已經有值了,也就會去返回它的值。
調用 add_to_stack 方法會把一個對象添加到堆裡面,就是會把對象添加到 @stack 數組的最後。take_from_stack 會刪除掉數組裡的最後一個對象。這些方法裡用的 push 還有 pop ,它們是 Array 類裡的執行個體方法。
我們定義的這個 Stacklike 模組,其實就是有選擇的實施了已經在 Array 對象裡存在的一些行為,添加一個元素到數組的最後,刪除數組裡的最後一個元素。相比堆,數組更靈活一些,堆不能幹所有數組能乾的事。比如你可以刪除掉數組裡的任意順序的項目,在堆裡就不行,你只能刪除掉最近添加進來的元素。
現在我們定義好了一個模組,它實施了堆的一些行為,也就是管理一些項目,新的項目可以添加到最後,最近添加進來的可以被刪除掉。下面再看一下怎麼樣使用模組。
在類裡混合模組
做個實驗,建立一個檔案,名字是 stack.rb,添加下面這段代碼:
require_relative 'stacklike'
class Stack
include Stacklike
end
這裡混合用的方法是 include ,把 Stacklike 這個模組混合到了 Stack 這個類裡,這樣 Stack 類的對象就會擁有在 Stacklike 模組裡定義的方法了。
使用 require 或 load 的時候,要載入的東西放到了一組引號裡面,但是使用 include 與 prepend 的時候載入的東西不需要使用引號。因為 require 與 load 要使用字串作為它們的參數值,include 載入的是模組的名字,模組的名字是常量。require 與 load 要找到在磁碟上的檔案,include 與 prepend 會在記憶體裡操作。
類的名字用的是名詞,模組的名字用的是形容詞。Stack objects are stacklike 。
做個實驗:
s = Stack.new
s.add_to_stack('項目 1')
s.add_to_stack('項目 2')
s.add_to_stack('項目 3')
puts '當前在堆裡的對象:'
puts s.stack
taken = s.take_from_stack
puts '刪除了對象:'
puts taken
puts '現在堆裡是:'
puts s.stack
執行一下會輸出:
當前在堆裡的對象:
項目 1
項目 2
項目 3
刪除了對象:
項目 3
現在堆裡是:
項目 1
項目 2
繼續使用模組
再做個實驗,建立一個檔案,名字是 cargohold.rb(飛機貨艙),代碼如下:
require_relative 'stacklike'
class Suitcase
end
class CargoHold
include Stacklike
def load_and_report(obj)
print 'loading object:'
puts obj.object_id
add_to_stack(obj)
end
def unload
take_from_stack
end
end
ch = CargoHold.new
sc1 = Suitcase.new
sc2 = Suitcase.new
sc3 = Suitcase.new
ch.load_and_report(sc1)
ch.load_and_report(sc2)
ch.load_and_report(sc3)
first_unloaded = ch.unload
print '第一個下飛機的行裡是:'
puts first_unloaded.object_id
執行它的結果是:
loading object:70328907390400
loading object:70328907390380
loading object:70328907390360
第一個下飛機的行裡是:70328907390360
下午12:00 ***
模組,類與方法尋找
下午12:06 ***
對象收到發送給它的資訊以後,它會試著去執行跟資訊一樣的方法,方法可以是對象所屬的類裡面定義的,或者這個類的 superclass,或者是混合到這個類裡的模組提供的。發送資訊給對象究竟發生了什嗎?
方法尋找
下面這個例子示範了載入模組與類的繼承:
module M
def report
puts "'report' 方法在模組 M 裡"
end
end
class C
include M
end
class D < C
end
obj = D.new
obj.report
report 這個執行個體方法是在模組 M 裡定義的,在 C 類裡面混合了模組 M ,D 類是 C 類的子類,obj 是 D 類的一個執行個體,obj 這個對象可以調用 report 方法。
從對象的視角來看一下,假設你就是一個對象,有人給你發了個資訊,你得想辦法作出回應,想法大概像這樣:
我是個 Ruby 對象,別人給我發了個 'report' 資訊,我得在我的方法尋找路徑裡,試著去找一個叫 report 的方法,它可能在一個類或者模組裡。
我是 D 類的一個執行個體。D 類裡有沒有 report 這個方法?
沒有
D 類有沒有混合使用模組?
沒有
D 類的超級類(superclass)C,裡面有沒有定義 report 這個執行個體方法?
沒有
C 類裡混合模組了沒?
是的,混合了模組 M
那 M 模組裡有沒有 report 這個方法?
有
好地,就調用一下這個方法。
找到了這個方法搜尋就結束了,沒找到就會觸發錯誤,這個錯誤是用 method_missing 方法觸發的。
同名方法
同一個名字的方法在任何時候,在每個類或模組裡只能出現一次。一個對象會使用它最先在找到的方法。
做個實驗:
module M
def report
puts '在模組 M 中的 report'
end
end
module N
def report
puts '在模組 N 中的 report'
end
end
class C
include M
include N
end
c = C.new
c.report
執行它的結果會是:
在模組 N 中的 report
多次載入同一個模組是無效的,這樣試一下:
class C
include M
include N
include M
end
執行的結果仍然會是:
在模組 N 中的 report
下午12:49 ****
prepend
下午1:40 ***
使用 prepend 載入的模組,對象會先使用。也就是如果一個方法在類與模組裡都定義了,會使用用了 prepend 載入的模組裡的方法。
來看個例子:
module MeFirst
def report
puts '來自模組的問候'
end
end
class Person
prepend MeFirst
def report
puts '來自類的問候'
end
end
p = Person.new
p.report
執行的結果會是:
來自模組的問候
super
做個實驗:
module M
def report
puts '在模組 M 裡的 report 方法'
end
end
class C
include M
def report
puts 'C 類裡的 report 方法'
puts '觸發上一層級的 report 方法'
super
puts "從調用 super 那裡回來了"
end
end
c = C.new
c.report
執行的結果是:
C 類裡的 report 方法
觸發上一層級的 report 方法
在模組 M 裡的 report 方法
從調用 super 那裡回來了
c 是 C 類的一個執行個體,c.report 是給 c 發送了一個 report 資訊,收到以後開始尋找方法,先找到的 C 類,這裡定義了 report 方法,所以會去執行它。
在 C 類裡的 report 方法裡,調用了 super,意思就是即使對象找到了跟 report 這個資訊對應的方法,它還必須繼續尋找下一個匹配的 report 方法,下一個匹配是在模組 M 裡定義的 report 方法,也就會去執行一下它。
再試一個使用 super 的例子:
class Bicycle
attr_reader :gears, :wheels, :seats
def initialize(gears = 1)
@wheels = 2
@seats = 1
@gears = gears
end
end
class Tandem < Bicycle
def initialize(gears)
super
@seats = 2
end
end
上面有兩個類,Bicycle 單車,Tandem 雙人單車,Tandem 繼承了 Bicycle 類。在 Tandem 的 initialize 方法裡用了一個 super ,會調用 Bicycle 類裡的 initialize 方法,也就是會設定一些屬性的預設的值。雙人單車有兩個座位,所以我們又重新在 Tandem 的 initialize 方法裡設定了一下 @seats 的預設的值。
super 處理參數的行為:
不帶參數調用 — super,super 會自動轉寄參數傳遞給它調用的方法。
帶空白參數的調用 — super(),super 不會發送參數。
帶特定參數的調用 — super(a, b, c),super 只會發送這些參數。
下午2:23 ***
method_missing 方法
下午 14:35 ***
Kernel 模組提供了一個執行個體方法叫 method_missing,如果對象收到一個不知道怎麼響應的資訊,就會調用這個方法。
試一下:
>> obj = Object.new
=> #<Object:0x007fdef1958fa8>
>> obj.blah
NoMethodError: undefined method `blah' for #<Object:0x007fdef1958fa8>
from (irb):2
from /usr/local/bin/irb:11:in `<main>'
我們可以覆蓋 method_missing:
>> def obj.method_missing(m, *args)
>> puts "你不能在這個對象上調用 #{m},試試別的吧。"
>> end
=> :method_missing
>> obj.blah
你不能在這個對象上調用 blah,試試別的吧。
=> nil
組合 method_missing 與 super
一般我們會攔截未知的資訊,然後決定到底怎麼去處理它,可以處理,也可以把它發送給原來的 method_missing 。使用 super 就很容易實現,看個例子:
class Student
def method_missing(m, *args)
if m.to_s.start_with?('grade_for_')
# return the appropriate grade, based on parsing the method name
else
super
end
end
end
上面的代碼,如果調用的方法是用 grade_for 開頭的就會被處理,比如 grade_for_english 。如果不是,就會調用原始的 method_missing 。
再試一個複雜點的例子。比如我們要建立一個 Person 類,這個類可以這樣用:
j = Person.new("John")
p = Person.new("Paul")
g = Person.new("George")
r = Person.new("Ringo")
j.has_friend(p)
j.has_friend(g)
g.has_friend(p)
r.has_hobby("rings")
Person.all_with_friends(p).each do |person|
puts "#{person.name} is friends with #{p.name}"
end
Person.all_with_hobbies("rings").each do |person|
puts "#{person.name} is into rings"
end
我們想要輸出的東西像這樣:
John is friends with Paul
George is friends with Paul
Ringo is into rings
一個人可以有朋友和愛好,Person 可以找出某個人的所有的朋友,或者擁有某個愛好的所有的人。這兩個功能是用 all_with_friends 還有 all_with_hobbies 這兩個方法實現的。
Person 類上的 all_with_* 方法可以使用 method_missing 改造一下,在類裡定義一段代碼:
class Person
def self.method_missing(m, *args)
# code here
end
end
m 是方法的名字,它可以是用 all_with 開頭的,也可以不是,如果是我們就去處理一下它,如果不是就交給原始的 method_missing 。再這樣修改一下:
class Person
def self.method_missing(m, *args)
method = m.to_s
if method.start_with?('all_with_')
# 在這裡處理請求
else
super
end
end
end
Person 對象要跟蹤它所有的朋友與愛好
Person 類跟蹤所有的人
每個人都有個名字
class Person
PEOPLE = []
attr_reader :name, :hobbies, :friends
def initialize(name)
@name = name
@hobbies = []
@friends = []
PEOPLE << self
end
def has_hobby(hobby)
@hobbies << hobby
end
def has_friend(friend)
@friends << friend
end
每次執行個體化一個新人都會把它放到 PEOPLE 這個數組裡。還有幾個讀屬性,name,hobbies,friends。
initialize 方法裡有個 name 變數,把它放到了 @name 屬性裡,同時也會初始化 hobbies 和 friends ,這兩個屬性在 has_hobby 與 has_friend 方法裡用到了。
再完成 Person.method_missing :
def self.method_missing(m, *args)
method = m.to_s
if method.start_with?('all_with_')
attr = method[9..-1]
if self.public_method_defined?(attr)
PEOPLE.find_all do |person|
person.send(attr).include?(args[0])
end
else
raise ArgumentError, "Can't find #{attr}"
end
else
super
end
end
全部代碼如下:
class Person
PEOPLE = []
attr_reader :name, :hobbies, :friends
def initialize(name)
@name = name
@hobbies = []
@friends = []
PEOPLE << self
end
def has_hobby(hobby)
@hobbies << hobby
end
def has_friend(friend)
@friends << friend
end
def self.method_missing(m, *args)
method = m.to_s
if method.start_with?('all_with_')
attr = method[9..-1]
if self.public_method_defined?(attr)
PEOPLE.find_all do |person|
person.send(attr).include?(args[0])
end
else
raise ArgumentError, "Can't find #{attr}"
end
else
super
end
end
end
j = Person.new("John")
p = Person.new("Paul")
g = Person.new("George")
r = Person.new("Ringo")
j.has_friend(p)
j.has_friend(g)
g.has_friend(p)
r.has_hobby("rings")
Person.all_with_friends(p).each do |person|
puts "#{person.name} is friends with #{p.name}"
end
Person.all_with_hobbies("rings").each do |person|
puts "#{person.name} is into rings"
end
執行的結果會是:
John is friends with Paul
George is friends with Paul
Ringo is into rings