Ruby中的設計模式

turingbooks發表於2020-04-07

        繼續 節講述過的Singleton Proxy Iterator各模式,本節再來考察幾個別的設計模式。下面按順序來考察 Prototype Template Method Observer這三個設計模式。

4.2.2  重複使用既存物件的Prototype (原型)模式

        引用 設計模式 》一書中 解釋 Prototype 模式 明確一個例項作為要生成物件的種類原型,通過 複製 該例項來生成新的物件

   《 設計模式 中這一模式的例題使用的是迷宮生成MazeFactory 類。這一例子通過擁有生成牆、房間、門等物件的原型,不需要派生就可以生成各種各樣的迷宮。但是,實話說來,這個例題並沒有讓人真正感覺到原型的寶貴之處。

        實際上 Prototype模式本來並不太適用於 C++ 這樣的靜態語言。在動態語言中, Prototype 模式才能夠真正發揮它的巨大威力。

通常在物件導向的語言中,首先準備好所謂的類,也就是物件的雛形,然後從類來生成實際的物件,這些做法都是理所當然的。在物件導向程式設計中,類是最為基本的存在。

        但是,類真的是必需的嗎?Smalltalk 亞種 Self語言的設計人員就認為類並不是必 的。Self 中不存在所謂的類,基本操作並不是從類來生成物件,而是 複製

        基本思想就是如此。

        在需要新種類物件的時候,首先 複製 一個既存的物件,給 複製 的物件直接增加方法或例項變數等功能,生成最初的第一個新種類物件。如果該物件需要不止一個的話,那就從第一個原型來 複製 ,需要幾個就複製幾個。最初一個雖然是雛形,但它並不是所謂類這種人為生成的特別物件,它也只是一個普通的物件, 不過偶爾被用為複製的原型而已。

        雛形這個詞的英語是Prototype 。像這樣的不用類,而是 原型模式和複製的程式語言稱為原型模式的程式語言。實際上,Prototype 模式不單是一種設計模式,也許稱之為一種程式設計範例才更為合適。

        相對於類模式的程式設計,原型模式的程式設計的構成元素比較少,具有簡單實現物件導向功能設計的傾向。因此,最近有越來越多的規格較小的程式語言採用這種模式。比如,大多數Web 瀏覽器中嵌入的 JavaScript 的物件導向功能就是原型模式的 最近,受到一部分 關注的Io 語言 也是原型模式的。

4.2.3  親身體驗Io 語言

        只講理論還是難 得到具體的印象,那就讓我們來邊看實際程式,邊來親身體驗原型模式的程式設計吧。雖然用Ruby 也可以進行原型模式的程式設計, 這次我們使用更為徹底的Io 語言( 參見 4-17 )。

4-17  使用Io 語言的原型模式的程式設計

        讓我們來仔細看看圖 4-17 吧。這是描述狗的簡單例子。雖然沒有實用性,但從中應該能體會到原型模式的氣氛。

        (1)的部分生成新的物件,賦值給名為 Dog 的變數。請注意,這裡出現的 Object Dog 都不是類。在Io 語言中, Object 只是 代表性的物件,除最基本的以外 其他 什麼都不知道。以它為基礎可以生成各種雛形。這裡呼叫 clone 方法來複制一個基礎物件,並給它起個名字,叫做 Dog 。剛剛複製出來的 Dog 物件跟 Object 是一樣的,沒有狗的任何功能。

        只是 Object 的話,什麼功能都沒有,是沒有任何使用價值的,讓我們來教給它狗的功能吧。(2) 部分中 先是給它定一個 坐下 sit 方法。把一個 method 物件賦值給 Dog 物件的 sit 屬性,就給 Dog 物件追加了一個方法。

        呼叫 sit 方法就等於呼叫 " I ' m sitting/n " print ,顯示 I ' m sitting. 。這一部分相當於呼叫字串物件的 print 方法,從中可以體會到徹底的物件導向程式設計。這個例子中僅僅增加了一個 sit 方法,實際上可以根據需要想追加幾個方法就可以追加幾個。

        你看, Dog 物件就是這樣 實現 的。再強調一遍,其中作為雛形的是狗物件,而不是類。因此, ( 3 )部分中 Dog 物件呼叫 sit 方法的時候,結果與其他狗一樣顯示出 I’m sitting.

        在像Ruby 這樣類模式的語言中,作為物件雛形的類擁有與物件完全不同的方法(類的方法),而與之相對的是,原型模式的語言中根本就沒有類的存在,雛形與基於它生成的物件是完全一樣的。

        (4) 部分 使用 clone 方法基於雛形生成新的狗物件。這裡請注意,不管是生成新的雛形,還是從雛形生成新的物件,都是僅僅使用 clone 方法實現的。在類模式的語言中,用子類化和例項化這兩個不同的概念實現的程式,這裡僅僅用 clone 這樣一個簡單的程式就實現了, 讓人感動。

        當然,簡單並不都是一切,原型模式有原型模式的優點,類模式也有類模式的優點,因為原型模式的語言還不太廣為人知,這裡特意選它作為例子, 就是為了 讓大家體會一下它的單純。

4.2.4  Ruby 中的原型

        基本上講Ruby 是類模式的語言,但也擁有支援原型模式程式設計的功能 具體來說有以下3 種功能。

        (1 )複製物件的 clone 方法

        (2 )給個別物件增加方法的特異方法功能

        (3 )給個別物件增加一組功能的 extend 方法


4-18  用Ruby重寫圖4-17的程式


        因為Ruby 中沒有像 Io 語言中 Object 這樣一個物件原型,所以作為準備,程式一開始( object = Object.   new )先是生成一個 Object 類的例項。在這以後,除了語法上細微的區別之外,差不多是把Io 程式照搬過來的。

        從中我們可以看到動態語言中Prototype 模式這種令人吃驚的力量。這在必須明確物件型別的靜態語言中是實現不了的。因為在靜態語言中沒有原型程式設計,是不可能給複製的物件增加新方法的。

4.2.5  編寫抽象演算法的Template Method (模板方法)模式

        接著來看Template Method 吧。還是引用 設計模式 中的 解釋 Template Method 模式是 在父類的一個方法中定義演算法的框架,其中幾個步驟的具體內容則留給子類來實現。使用Template Method 模式,可以在不改變演算法構造的前提下,在子類中定義演算法的一些步驟

        這實際上是物件導向程式設計中使用繼承的一般技巧。

        作為例子,讓我們來看一下Ruby p 方法吧。 p 方法是程式除錯中用來顯示物件內容的方法。顯示除錯資訊演算法主要是:

        (1 )把物件的除錯用輸出資訊轉換成字串

        (2 )呼叫 puts 方法輸出

        這就是除錯輸出的演算法,因為實在是太簡單了,稱之為演算法 簡直 有些過分。

        但實際上令人意外的是,為了輸出除錯資訊,分別為各種物件定義除錯用輸出字串是非常困難的。針對各種不同的類都要分別進行不同處理,這就很困難了,每次增加新類的時候,為支援新類也都需要做大量的工作。

        這時候使用Template Method 模式,問題就變得簡單了。使用 Template Method 模式,輸出除錯資訊的 p 方法會變成如下的程式碼。簡單得令人意外。

def p(obj)

  puts obj.inspect

end

        這個簡單的方法只是把演算法的 1 和(2 )原封不動地換成了程式語言。在這個定義中,各種物件中準備除錯資訊的具體處理是由該物件的 inspect 方法來實現的。在定義新類的時候,只要給它定義了合適的 inspect 方法,就可以在任何時候使用 p 方法來輸出適當的除錯資訊。

        在父類中定義抽象化的演算法,呼叫隱 了實現細節的方法,然後在子類中實現具體的細節,這就是Template Method 模式。

4.2.6  Ruby 來嘗試 Template Method

        Ruby的類庫中最大限度靈活運用 Template Method 模式的部分,應該說是 Enumerable 模組和 Comparable 模組了。

        Enumerable模組中實現迴圈的 each 方法採用了Template Method 模式。表 4-2 Enumerable 模組的方法一覽。

4-2  Enumerable提供的方法

(續)

        這些方法的定義都是僅僅依賴於 each 方法。因此,在使用者定義的類中,只要定義了 each 方法,一旦把Enumerable 模組 include 進來,就都可以使用表中的32 個方法了。

        Enumerable模組實際上是用 C 編寫的,用 Rudy 也可以簡單地定義同樣的模組,那就讓我們邊看 Ruby 的定義,邊來考慮一下如何更好地使用 Template Method 模式

        其中一個實現起來最為簡單的方法,應該是收集所有元素的 entries 方法了。 entries 方法的實現程式碼如圖 4-19 所示。看一下圖 4-19 就能明白,處理內容是很簡單的。

4-19  用 Ruby 實現的 entries 方法

        (1 )生成陣列。

        (2 )用 each 取出每個元素。

        (3 )把元素追加到陣列裡。

        (4 )最後返回陣列。

        從中可以看出,這個方法只是定義了自己處理的 框架 ,對應於每個物件的處理則由 each 方法來提供。

        像Template Method 模式的這種使用方法, Comparable 模組中也是一樣。

        Comparable模組利用基本的比較大小方法 <=> ,提供各種比較演算。 <=> 方法把自身與引數相比較,如果自身較大,則返回正整數 若二者相等,則返回0 若自身較小,則返回負整數。以這個方法為基礎,Comparable 模組提供了 == > >= < <= 以及 between? 6 種比較運算。

        作為Comparable 提供的比較運算的代表,我們來看一看 > 方法的實現吧( 參見 4 -20 )。實際上> 方法還要加上錯誤處理,但基本處理如圖 4 -20 所示。

4-20  用 Ruby 實現的 > 方法

        像這樣,使用Template 模式,不涉及各種資料結構細節,而只在抽象的水平上編寫演算法的程式。也就是說,演算法是在抽象水平很高的狀態下表述的,同樣的程式碼能夠適用於各種各樣的情況。

        像這樣避免了程式碼的重複,從DRY 原則的觀點來看 ,也是很優秀的。

4.2.7   動態語言與 Template Method 模板方法 模式

        一般Template Method 模式與繼承往往是成對討論的,但像 Enumberable 那樣,只需要 include 進來,不管繼承關係如何,即可以向任何類裡追加功能,這一點很有魅力。本來,Ruby include 就是一種受限的多重繼承, 這本 沒有什麼不可思議的。

        Template Method模式的這種優秀性質與語言是不是靜態沒有關係。像 Java 那種含有靜態型別,而且不允許多重繼承的語言,必須強制性地擁有繼承關係。所以,像 Enumerable 這樣在各種各樣的類中都能利用的演算法集,使用 Template Method 模式很難實現( interface 與委託的組合也不是不可能),但這不是靜態語言的問題。

        但是,像Io Ruby 這種也善於原型模式程式設計的語言,更加進化一步,可以往特定物件裡追加演算法集。圖 4-21 表示往特定物件裡追加Enumerable 功能,雖然這個例子有點 牽強

4-21  往特定物件裡追加 Enumerable 功能的示例

        儘管哪兒也沒定義類,但使用 extend ,骰子物件中就能夠利用Enumerable 模組的功能了。用 extend 及特異方法往特定物件裡追加功能的做法,也能夠用來實現 Singleton 模式。

4.2.8  避免 高度 依賴 性的觀察者(Observer 模式

        觀察者模式是,當某個物件的狀態發生變化時,依存於該狀態的全部物件都自動得到通知,而且為了讓它們都得到更新,定義了物件間一對多的依存關係。

        這是控制類與類之間依存關係的一種模式。舉一個例子,想想微軟的Excel 軟體吧。以表中的資料為基礎表示圖形的時候,編輯了表中的資料之後,自然希望圖形的內容也跟著變化。或者,從同一組資料,想同時看到直方圖和扇形圖等多種圖形的情況也是有的。

        能夠實現這一要求的最簡單的方法,應該是在表編輯功能裡附加更新圖形 顯示 的處理。但是這樣做的話,附加的是與表編輯在本質上不同的處理,使事情複雜化,更重要的是,當想要再利用表編輯功能時,還要牽連到不一定必要的圖形 顯示 功能。表編輯功能與圖形 顯示 功能之間的這種關係稱為高度 依賴 性。

        一般,高度依 性不好。 本質上 ,軟體是個複雜的東西,為了控制複雜性,有效 方法是將整體分割成幾個相互獨立的部分進行開發。但是,有了高度依 性,就不能將組成程式的 零件 (類以及子程式)進行分解,一個一個的 零件 會很大,結果就很難控制複雜性。

        觀察者模式是一種 避免 這種高度依 性的手段。構成觀察者模式的有兩個物件,一個稱為Observer (觀察者),接受變更通知;另一個稱為 Subject (物件)或 Observable (被觀察者),發出變更通知。

        說說剛才的表編輯的例子,表資料就是Subject ,圖形就是 Observer 。從觀察者與被觀察者這兩個名字上,讓人得到被動的印象, 實際處理中,被觀察者會發出通知 我已經變化了哦

4.2.9  Observable 模組

        Ruby中為實現 Observer 模式提供了名為 observer 的庫。 observer 庫提供 Observer 模組。 Observer 模組的 API 請參見 4-3 。實際的庫使用例 4-22所示

4-3  Observable模組的 API (應用程式設計介面)

   解釋 一下圖 4-22 的程式。首先是 require observer 庫。利用庫的時候,這是必須寫的。

        然後是定義被觀察者類 Tick 。註釋中也寫到,該類每1 秒發 1 次更新通知。它是相當於時鐘的類。 Tick 的發音,好像是時鐘的滴答滴答聲。Tick 是被觀察者,所以要將 Observable 模組 包含 進來。僅一句話就能讓任意類成為被觀察者,這正是Ruby 的威力。

tick 方法是主迴圈。有了這個處理,每隔 1 秒,就迴圈發出更新通知。雖然僅僅sleep  1 秒,但為了 保證 能在整秒發出更新通知, 便 以微秒為單位進行了補正( sleep1.0 - Time... 的部分)

        實際的更新通知只是呼叫 changed 方法設定更新標誌,然後用 notify_observers 方法通知觀察者。他們都寫在 loop (迴圈)內。

        雖然像這樣每次肯定都要定期發出更新通知的情況,把 changed notify_observers 分離開來沒有意義,但是考慮到頻繁變化、每次更新處理的花費 比較大的情況, 還是 將二者分離開了。比如剛才的表編輯的例子中,與每次細微的變化都要更新圖形相比,鍵盤輸入告一段落時集中更新圖形,應該更有效率。


4-22  使用observer 庫的 Ruby 程式 觀察者與被觀察者不相互依存

        後半部分的TextClock 類是觀察者類。依照 Tick 傳送 的通知,在控制畫面上顯示現在時刻。TextClock 類不是特定類的子類或者別的什麼,只是擁有被更新通知呼叫的 update 方法。 update 方法接受Tick notify_observers 方法傳過來的時、分、秒三個整數引數。

        實際顯示用了ANSI 的轉義字元( printf 以下的部分)。用 ESC[8D 將游標移到行首,後面顯示時刻。為避免緩衝問題,每次呼叫 STDOUT.flush

        定義了Tick (被觀察者)與 TextClock (觀察者)兩個類之後就簡單了。 生成Tick 類的物件(圖 4-22 倒數第3 行的部分),然後使之與 TextClock 類相關聯 ,最 後啟動Tick 類的主迴圈(圖 4-22 的末尾部分) 就這麼多。

        執行圖 4-22 的程式,控制畫面上就會顯示出一個數字時鐘。因為是無限迴圈,想要停止時,請按下Ctrl+C

        這次只做了一個觀察Tick 類的物件 TextClock ,如果願意,可以新增任意多個觀察者。比如,對同一個 Tick ,不光能新增文字時鐘,還可以新增圖形時鐘。

        這個程式最應該注意的一點是,Tick 類與 TextClock 之間的關聯,只用一行(圖 4-22 倒數第2 行)就完成了。 Tick 類與 TextClock 類之間,只有 更新以後,呼叫update 方法 ”以及“在 update方法中,傳遞時、分、秒 這種簡單的約定,不存在別的關係。只要是遵守相同約定的類,都可以簡單地進行交換。

        可以看出,使用Observer 模式,顯然能夠降低相互依 賴性 。既可以將觀察者類做成零部件,又可以根據需要更換被觀察者(比如測試用的假程式)。這個性質對於提高軟體的開發效率和測試效率,都是很有用的。

4.2.10  Observer 模式與動態語言

        動態語言的性質在Observer 模式中也很有用。由於 Ruby 的動態性質, observer 庫具有以下的 靈活 性。

        (1 )觀察者類不必是特定類的子類

        (2 )觀察者類不必實現特定的介面(本來在 Ruby 中也沒有介面)

        (3 )觀察者類的更新方法名可以自由決定( Ruby   1.9的功能)

        (4 )觀察者類更新方法的引數可以自由決定

        (5 )被觀察者類不必是特定類的子類

        (6 )對被觀察者類的要求,只是將 Observerable 模組 包含 進來

        我想Java 那種靜態語言也 具有 Ruby observer 同功能的庫。事實上,有幾種DI 容器( Dependency Injection Container )框架,也 有與observer 庫相類似的處理。

        但是,如果編碼太繁雜了,或者需要用XML 檔案代替 Java 來描述類之間關聯的話,我認為 沒有Ruby 這麼好用

 


節選自《松本行弘的程式世界》

 

相關文章