堆疊擴充


之後會談到,如果你為物件定義單例方法,實際上該方法定義,會存在於物件的匿名單例類別中(Anonymous singleton class),你可以使用class << object語法,開啟object的單例類別。例如:
o = Object.new
def o.some
    puts "some"
end

實際上也可以如下定義:
o = Object.new
class << o
    def some
        puts "some"
    end
end

這兩種方式的效果幾乎是相等的,目前就暫且看作是相等。你可以使用class << object語法,開啟object的單例類別,也就可以在其中作任何類別可以進行的定義,包括了include模組。例如臨時為某個物件增加 Enumerable 模組的作用:
class Pond
def initialize(list = [])
@list = list
end
def <<(obj)
@list << obj
end
end

pond = Pond.new([2, 1, 5, 3, 6, 4])
class << pond
include Enumerable
def each
@list.each do |obj|
yield(obj)
end
end
end

print "#{pond.sort}\n" # [1, 2, 3, 4, 5, 6]
puts "Max: #{pond.max}" # Max: 6
puts "Min: #{pond.min}" # Min: 1

pond.each_with_index do |ball, index|
puts "#{index} - #{ball}"
end

就語法上來看,模組與類別的不同點,就是模組無法實例化,還有類別只能繼承一個父類別,但可以include多個模組

模組中定義方法時,self與super是動態綁定的,被包括到某個類別時,模組定義方法中的self代表該類別的實例,而super代表模組被include前同名的模組方法或類別方法(如果是類別繼承,super代表父類別中的同名方法)。

由於模組中的super是動態綁定,而一個類別可以include多個模組,因此可執行所謂堆疊擴充(Stackable  extension)的功能。

舉個例子來說,你打算設計一個點餐程式,目前主餐有炸雞、漢堡,你打算讓點了主餐的客入選擇附餐時可以有優惠,如果使用繼承的方式來達到這個目的,例如:
class FriedChicken
    def content
        "不黑心炸雞"
    end
    def price
        49.0
    end
end

class SideDishOne < FriedChicken
    def content
        super + " | 可樂 | 薯條"
    end
    def price
        super + 30.0
    end
end

這個需求直接使用繼承並不適當,你繼承父類別之後,只是取得父類別的price結果再進一步加以處理,另一方面,如果漢堡也想要搭配附餐一,目前的SideDishOne顯然無法給漢堡重用,你還得為漢堡建立有附餐一的子類別。

為了讓附餐設計可以重複使用,你可以將附餐的行為抽出為模組:
# encoding: Big5
module SideDishOne
def content
super + " | 可樂 | 薯條"
end
def price
super + 30.0
end
end

module SideDishTwo
def content
super + " | 玉米濃湯 | 咖啡"
end
def price
super + 50.0
end
end

class FriedChicken
def content
"不黑心炸雞"
end
def price
49.0
end
end

附餐一與附餐二都定義了content與price,不過由於其中用到了super呼叫,目前並無法知道這個呼叫到底是呼叫哪個方法。

現在如何搭配附餐呢?為了詳細示範,先中規中矩地寫的話,可以這麼用:
class FriedChickenSideDishOne < FriedChicken
include SideDishOne
end

meal = FriedChickenSideDishOne.new
puts meal.content # 顯示 不黑心炸雞 | 可樂 | 薯條
puts meal.price # 顯示 79.0

你的FriedChickenSideDishOne類別繼承FriedChicken類別並include了SideDishOne模組,FriedChickenSideDishOne類別中,SideDishOne模組的content 與price方法重新定義了FriedChicken類別中content與price方法,你呼叫meal.content時,根據模組中的定義,就是 先呼叫類別FriedChicken的content取得結果,再附加上字串後傳回,呼叫meal.price時也是同樣的道理。

特別定義出FriedChickenSideDishOne類別看來奇怪,實際上,你可以這麼使用:
meal = FriedChicken.new

class << meal
include SideDishOne
end

puts meal.content # 顯示 不黑心炸雞 | 可樂 | 薯條
puts meal.price # 顯示 79.0

除了開啟單例類別來include模組的方式之外,物件有個extend方法,可以執行相同的作用。例如:
meal = FriedChicken.new.extend(SideDishOne)
puts meal.content # 顯示 不黑心炸雞 | 可樂 | 薯條
puts meal.price # 顯示 79.0

有了extend方法,
你可以隨意地組合套餐:
meal1 = FriedChicken.new.extend(SideDishOne)
meal2 = FriedChicken.new.extend(SideDishTwo)
meal3 = FriedChicken.new.extend(SideDishOne).extend(SideDishTwo)
meal4 = FriedChicken.new.extend(SideDishTwo).extend(SideDishOne)
puts meal1.content # 不黑心炸雞 | 可樂 | 薯條
puts meal1.price # 79.0
puts meal2.content # 不黑心炸雞 | 玉米濃湯 | 咖啡
puts meal2.price # 99.0
puts meal3.content # 不黑心炸雞 | 可樂 | 薯條 | 玉米濃湯 | 咖啡
puts meal3.price # 129.0
puts meal4.content # 不黑心炸雞 | 玉米濃湯 | 咖啡 | 可樂 | 薯條
puts meal4.price # 129.0

注 意meal3與meal4,這也是為什麼這個特性被稱為堆疊擴充的原因,你讓物件擴充以上的模組時,最右邊的模組若在content中使用super呼叫,其 實是在呼叫左邊模組的content方法,而左邊模組的content中的super則呼叫父類別的content,呼叫的結果就像是將最左邊的類別看作是 堆疊底部,最右邊的模組看作是堆疊頂端。

同時也注意到,meal3是先擴充SideDishOne模組再擴充SideDishTwo,而meal4則是先擴充SideDishTwo再擴充SideDishOne,擴充模組的順序不同,則呼叫的順序不同,則結果就有所不同。

事實上,這是Ruby版本的 Decorator 模式 之實現,雖然語法不同,不過其在不改變被修飾物件功能的情況下,動態地為物件的操作結果作修飾,這樣的精神是相同的。