Ruby中的設計模式
繼續 上 節講述過的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 這麼好用 了 。
節選自《松本行弘的程式世界》
相關文章
- Ruby中的設計模式——《松本行弘的程式世界》設計模式
- 【設計模式】漢堡中的設計模式——策略模式設計模式
- 【設計模式】漢堡中的設計模式——觀察者模式設計模式
- Mybatis中的設計模式MyBatis設計模式
- php中的設計模式PHP設計模式
- javascript中的設計模式JavaScript設計模式
- OO設計模式中的工廠模式設計模式
- 設計模式中的觀察者模式設計模式
- 設計模式(七)Android中的代理模式設計模式Android
- 設計模式(三)Animation中的策略模式設計模式
- 原始碼中的設計模式--模板方法模式原始碼設計模式
- 原始碼中的設計模式--工廠模式原始碼設計模式
- 設計模式 - ASM 中的訪問者模式設計模式ASM
- Android 中的設計模式:觀察者模式Android設計模式
- 設計模式應用場景之Model設計中可以用到的設計模式設計模式
- Java中的設計模式詳解Java設計模式
- 原始碼中的設計模式--裝飾器模式原始碼設計模式
- Python 中的設計模式詳解之:策略模式Python設計模式
- Java中的設計模式(一):觀察者模式Java設計模式
- 設計模式(八)Context中的裝飾者模式設計模式Context
- 設計模式在 TypeScript 中的應用 – 策略模式設計模式TypeScript
- Logstash中的ruby
- Java併發程式設計中的設計模式解析(一)Java程式設計設計模式
- javascript中的設計模式之釋出-訂閱模式JavaScript設計模式
- Java中的設計模式和原則Java設計模式
- Laravel 中設計模式的實戰分享Laravel設計模式
- 微服務中的Sidecar設計模式解析微服務IDE設計模式
- Ruby中的陣列陣列
- 設計模式中的黃金搭檔:命令模式+觀察者模式設計模式
- Swift 中的設計模式 #3 外觀模式與介面卡模式Swift設計模式
- Spring中如何使用設計模式Spring設計模式
- 16種JavaScript設計模式(中)JavaScript設計模式
- 原始碼中的設計模式--模板方法模式(鉤子方法)原始碼設計模式
- 設計模式之--策略模式及其在JDK中的應用設計模式JDK
- java23中設計模式–代理模式ProxyJava設計模式
- java23中設計模式--代理模式ProxyJava設計模式
- 【設計模式】最常用的設計模式之一的觀察者模式設計模式
- 設計模式(三)——JDK中的那些單例設計模式JDK單例