精華區beta Ruby 關於我們 聯絡資訊
快快樂樂學 Ruby, 平平安安... 第二話,直闖 Dice 實作 在第一話中,我們已經把最基本的 2.roll 實作出來了, 那麼我們的 Dice 需要哪些資訊?再簡單也不過了,就是骰數與 面數。於是我們可以這樣做: 2.dice.roll, 跟上面的 2.roll 完全是一樣的,只是 2.dice 時產生了一個 Dice 物件,其中 儲存了這次的骰數與面數。先來看骰子怎麼做: (btw, 我被 windows 和 unix 不同的斷行字元煩死了 Orz) class Dice attr_reader :amounts, :faces # 這一行看似單純簡單,其實是偉大的 meta-programming... # 有鑒於此篇文章不是講解高階實作技巧,故在此只簡單說明 # 這一行的功用。簡單地說,就是替你實現 amounts 和 faces 的 # getter. 也就是說,這一行等同於以下 code: # def amounts # @amounts # end # def faces # @faces # end # 其中 @ 記號表示這個變數是 instance variable, # 也就是 Java 口中的 field, C++ 口中的 data member # Ruby 用各種前綴「符號」來表示各種不同的變數… # 其實這很容易造成初學者的混亂,不過久了就可以習慣了 def initialize(amounts, faces = 20) # 這個是 Ruby 的 c'tor @amounts = amounts # 由於 Ruby 變數不需宣告,所以通常 @faces = faces # 所有的 instance variable 都是定義在 # c'tor 中,再來各處都可以直接取用 # 我們的 c'tor 有兩個參數,一個接受骰數,一個接受面數 # 和 Numeric 一樣的是,不說明面數則表示 20 面 # 順帶一提,我對於 Ruby 使用「initialize」這個字眼感到憤怒 =.= # 理由是,這個字可以排上我錯字排行榜前三名,一天到晚打錯… # 一堆 i 就不說了,c'tor 名字這麼長,煩死人了… # 而且拼錯字他當然不會告訴你你拼錯了…他只會找不到 c'tor 而已 # 還是 D 語言的 this 方便…換做 Ruby 應該叫 self end def roll @amounts.roll(@faces) # 由於我們已經記得骰數和面數, # 所以直接 delegate 給骰數就好啦 end def min @amounts # 這個東西嚴格來說是額外的, # 不過在測試程式正確性時會需要用到 # composite pattern 時也會發揮功效 end def max @amounts * @faces # 最小最大值的運算應該沒問題吧? end end 於是我們已經有基本的骰子能用了,2.roll, 2.dice.roll, 2.dice.min => 2 2.dice.max => 2*20 => 40 嗯,不知道有沒有讀者已經發現了,其實現在 Numeric 根本就沒有 dice 這個 method... 2.dice 的執行本身會錯誤 我不會承認是我忘了說,事實上是因為 2.dice 需要看到 Dice 的 實作,所以我把 2.dice 這種東西移到這裡再講。記得 Ruby 的 class 可以隨時打開來擴充對吧?現在我們就來擴充: class Numeric def dice(faces = 20) Dice.new(self, faces) end end 也就是說,其實 2.dice 只是 Dice.new(2) 的簡單呼叫法而已。 同樣你也可以寫: Dice.new(2).roll, 只是比較囉唆而已。 ok, 再來就是最複雜的 DiceSet 了,不過說是複雜其實也沒很複雜, 只是比起上面幾樣東西是稍微複雜了一點。 class DiceSet attr_reader :min, :max # 這個東西記得吧?由於 composite pattern 的緣故, # 我們很難去記得 DiceSet 的正確骰數和面數(其實只是我懶得做而已) # (有興趣的讀者可以把這個當作練習題…) # 所以現在這裡我只簡單記憶 min 和 max 是多少,而不是去計算 def initialize(*args) # 於是我們看到另外一個變數符號了,前綴 * # 這絕對不是指標的意思,在參數列中,這表示不定長度的引數 # 就有點像 C 中的 ... 參數一樣,Java 1.5 我記得也有加上這個功能, # 但怎麼用我已經忘了 XD # 總之,Ruby interpreter 會將引數打包成一個 Array 丟給 args, # 所以事實上 args 是一個 Array, 儲存著所有的引數 # p.s. 引數是 argument, 參數是 parameter # 前者是呼叫端(caller)所傳進來的東西, # 後者是接收端(callee)所宣示要吃的東西 @diceset = args # 事實上,DiceSet 就只是一個 Array 的包裝(wrapper) # 我們在 DiceSet 內部中儲存一群的 Dice, 和或 DiceSet # 當我們叫 DiceSet 擲骰時,就是把這一群的 Dice, 和或 DiceSet # 擲出結果,然後把這些結果全部加起來,再回傳出去 # 我們的參數/引數 args, 正是一開始傳進來的所有骰子/骰子組 # 日後要再加骰子/骰子組到這個骰子組中的話,就只要加到這個 Array 中即可 @min = 0 # 初始化為零 @max = 0 # 初始化為零 @diceset.each do |i| @min += i.min; @max += i.max; end # 開始計算真實的 min 和 max 是多少? # 當然這非常簡單,就是所有的骰子/骰子組的 min 和 max 合 # 所以只要環島一週(travel)並取得各觀光景點(骰子/骰子組)的 # min 和 max 合就 ok 了 end def roll result = 0 # 初始化總合 @diceset.each { |i| result += i.roll } # 將每個骰子/骰子組的丟擲結果加進總合中 result # 傳回 end def <<(dice) # 把骰子/骰子組加入這個骰子組 # 有點 append 或 insert 的意味在 @diceset << dice # delegate 給我們內部的 Array, 加入骰子/骰子組 @min += dice.min # 最大最小不要忘記加上去 @max += dice.max self # 把自己傳回去…其實沒有什麼特殊用意, # 只是 Ruby 的最後一行一定會傳出去, # 但傳個 @max 出去好像怪怪的是吧? end end ok, 全部的實作就是這樣了 這樣一行一行看的缺點在於,喪失整體性,只知道 how 不知道 why, 所以現在就來說明一下 composite pattern... 在 static typing 的系統中,要實作 composite pattern 勢必得 需要讓兩個物間中有著繼承關係,像是這樣: p.s. composite pattern 的意思是,讓群體等同於個體 也就是說不管你呼叫的對象是誰,是個體就讓他做事, 是群體就讓那一群一起去做事 C++: class Componenet{}; class Composition: public Component{}; Java: class Component{} class Composition extends Component{} 旅行的部份就可以這樣做: in Composition: // 虛擬碼 void doSomething(){ for each Component i in components{ i.doSomething(); } } 由於 Composition 也是一種 Component, 所以 Composition 也可以加到上文的 components 中。如果我們呼叫到真正的 Component, 則這個 Component 做事,但如果其實他是 Composition 呢? 他會再去呼叫他底下的 components 做事,於是形成某種形式的遞迴呼叫 要做到這點,就必須讓 Compositon 和 Component 有著繼承關係, 否則編譯器會大大抱怨,你讓不會做事的人做事,有問題! 繼承是個威力非常強大的能力,同時也讓程式複雜性大大提昇, 所以在任何時候都該謹記可以不用繼承時,不要用繼承,繼承應當視為 最後的秘密武器…。這不是在說實作 composite pattern 不應該使用繼承, 而是在 static typeing 的系統中,只有這個辦法能實作 composite pattern. 但由於 Ruby 是 dynamic typing 的系統,又是所謂 duck-typing, 也就是說只要有共同名稱的方法(method),就可以呼叫,不需要管 他真實的型別到底是什麼。只要會就去做,不管你是不是其中一份子。 這讓程式在實作時產生極大的彈性。在 static typing 的系統中, 如果程式原本就沒有設計 composite pattern, 忽然要改成這種設計 是有一點麻煩的,因為牽涉到了繼承體系。但在 dynamic typing 中, 要實作 composite pattern, 只要把名字取得一模一樣就可以了。 也就是說,要後來再掛上 Composition 這個類別,是很容易的, 只要把方法名字取得跟 Component 一模一樣就可以。 在這裡,就是 min, max, 還有 roll. 所以在 DiceSet 中,我們可以將 DiceSet 也視為一種 Dice, 直接加到 diceset 的 Array 中,需要時直接呼叫他就可以了。 這樣看起來,dynamic typing 似乎佔有極大的優勢,不過其實這也只是 一種取捨而已。dynamic typing 將編譯期的成本移動到執行期, 成本沒有消失,只是轉換和移動而已。static typing 的成本是 programmer 需要下比較多的心力去維護製作,dynamic typing 將這成本變成了電腦 執行期間對資源的索取程度。一個是由 programmer 事先告訴電腦「他」 會不會做事,另一個則是電腦在需要執行時,才去問「骰子組」會不會 做「骰子」會做的事情?所以在執行效率上,可能跟 static typing 有著好幾個數量級的差異。 回顧一下 DiceSet 用法吧: dice_set = DiceSet.new(2.dice, 3.dice(4), 8.dice(d)) dice_set.roll # 2~40 + 3~12 + 8~32 共 13~84 another_dice_set = DiceSet.new(2.dice, dice_set) another_dice_set.roll # 2~40 + 13~84 共 15~124 another_dice_set << 2.dice another_dice_set.roll # 15~124 + 2~40 共 17~164 another_dice_set << DiceSet.new(dice_set, dice_set, 2.dice) another_dice_set.roll # 17~164 + 13~84 + 13~84 + 2~40 共 35~368 不過寫到這裡我有點懶了,所以 DiceTest 我們下週再看吧 @_@ -- Nobody can take anything away from him. Nor can anyone give anything to him. What came from the sea, has returned to the sea. Chrono Cross -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 220.135.28.18 ※ 編輯: godfat 來自: 220.135.28.18 (08/10 18:25)
poga:頭推^^/ 08/10 18:28