元程式設計就是編寫能為你編寫代碼的代碼。但那不就是代碼生成器所做的事情嗎,就像是 rails gem,或者 yeoman?或者甚至是位元組碼編譯器?
是的,但元程式設計一般指的是 ruby 中的另外一些東西。ruby 中的元程式設計指的是能為你動态地編寫代碼的代碼。是在運作時發生的。ruby 是動态元程式設計的首要語言,因為它采用了 類型推斷 并且是強 發射的 – 相比現有的其它語言處在一個更高的級别。這能讓你做一些像使用幾行代碼就可以加入大量功能這樣事情,真的很酷,但是有一個問題:一個不小心,在你提升很多東西的同時,得到卻是幾乎無法閱讀的代碼。這件事情的意思,用 uncle ben 的話說就是 :
uncle ben 說這話的時候,并不是在談論現實生活中的什麼東西,他講的是元程式設計。
假設你想建立一個方法,功能是接受一個字元串參數,隻保留字元串中數字和字母。
這樣就完成了功能,但是好像不“面向對象”,下面我們這樣來修改。
在 ruby 中,即使你不是類的最初的定義者,你也可以像上面的例子一樣向一個已經存在的類中加入你想要擴充的功能(string 是 ruby 的預設字元串類)。但是,在開放類中會有問題,看下面的代碼。
我們寫一個叫 array#replace 的方法,帶兩個參數,第一個是你想在數組中被替換的值,第二個是替換的最終值。
代碼運作正常。為什麼有問題?因為 array#replace 已經存在了,功能是用一個作為參數傳入的數組替換目前數組的全部内容。我們寫的方法直接覆寫了原來的方法,這不太好。因為可能我們的本意并沒有想覆寫原有的功能。
在 ruby 裡這種修改類的處理方法叫 monkeypatching。可能這種做法無可厚非,但是你一定要明确自己在做什麼。
在更深入了解之前,我們需要先讨論一下 ruby 對象模型。
這看起來像一個複雜的圖表,但是它清晰的表示出了 ruby 中對象,類,子產品之間的聯系。有三個要點。
我們還會在第二部分引用這個,現在我們繼續看繼承鍊。
下面這張圖要容易了解一點,隻涉及繼承和子產品包含關系。
當你調用一個方法時,ruby 向右進入的接收者類,然後向上周遊繼承鍊,直到找到被調用方法或者抵達盡頭。在上圖中,對象 b 是 book 類的示例,book 類包含兩個子產品:printable 和 document。book 繼承自 object 類,這是 ruby 中幾乎一切事物的基類。object 包含一個子產品kernel。最後,object 又繼承于 basicobject 類——ruby 中一切對象的絕對父類。
現在我們稍微了解了點這兩個重要的主題——即 ruby 對象模型和繼承鍊——是時候來點代碼了。
在 ruby 中,可以動态的建立方法,也可以動态的調用方法。甚至可以調用一個并不存在的方法,也不會抛出異常。
為什麼會有動态定義方法的需求?可能是為了減少重複的代碼,也可能是為了加上更 cool 的功能。activaterecord(rails 工程的預設 orm 工具)大量的使用了這個特性。看下這個例子。
如果你熟悉 activaterecord,那你會感覺這和普通的 orm 沒有什麼差別。即使我們沒有在 book 類中定義 title 屬性,我們假定 book 類是一個 book 資料庫表的一個 orm 包裝器,并且 title 就是這個表裡的字段,那麼我們就能得到由 b 表示的那個資料庫裡的 title 所指定的那一列。
通常在這個類調用 tilte 方法會引發 nomethoderror 錯誤 - 但是 activerecord 會動态添加這些方法,就和我們手動添加的一樣。activerecord 的源代碼就是如何把元程式設計運用到極緻的一個很好的例子。
讓我們試一下元程式設計,首先建立一些方法
看到這些重複的代碼了嗎? 讓我們用元程式設計來改進一下
上邊這段代碼做的是動态定義了方法 foo,baz 和 bar,然後去調用它們。module#define_method 這個方法我個人經常使用它,它非常非常有用。這裡有個我在之前寫過的 gem 子產品中使用它的例子。
你可以看到我們在這節省了多少代碼 - 尤其是當我們編寫真正的方法時。但是-他的作用大于他帶來的代碼複雜度嗎? 這要由你自己決定了。
這裡有一個既可以通過方法名字元串也可以通過到達名稱符号調用方法的例子。
object#send 方法就是我們來示範如何動态調用的方法示例。通過數字 1 到 5 依次産生 5 個函數,并且通過目前變量的值來确定函數的名字。
由于在 ruby 裡所有的對象都繼承自 object 類,是以你可以在任意對象上調用 send 方法來通路這個對象的其他方法或者屬性,如下,
這個方法的能力(通路其他對象屬性方法的能力)取決于你在調用方法的時候的作用域情況-通常都是基于一個變量值。object 也允許你調用私有方法,但是如果你本意不想這麼做就一定要注意。如果可以,使用 object#public_send 這種方式,作用與直接調用相同,但是會限制通路私有成員和私有方法。
如果我們嘗試執行這樣的代碼會發生什麼?
我們将會得到一個 無此方法的錯誤,因為 book 執行個體不知道如果處理 read 這個方法。但是它也并不是不能處理。讓我們來引入 method_missing 方法。
basicobject#method_missing 為你提供了可以建立一個在無此方法錯誤事件(但在之前是會有錯誤)前被自動觸發調用的處理器的途徑。随後你需要提供你希望調用的方法的名稱,以及其相應的參數作為 method_missing 的參數,和它的代碼塊。在那裡,你可以做任何你想做的事。
這樣的用法看起來很酷,但謹慎使用它除非你有一個明确的好理由,因為:
以上,就是我們在第一部分打算覆寫的全部内容。我們回顧一下,有:開放類、ruby的對象模型、繼承鍊、動态定義方法、動态調用方法,以及魔法方法,但在第二部分中我們将會觸及到更多,包括有:scopes、動态定義類、閉包(塊、procs和lambda表達式)、各種執行(instance_eval,class_eval,和 eval)、編寫多功能子產品。
然而,我們不會講到單例方法和特征類。這些内容涵蓋了 ruby 元程式設計中很好的方面,但在我看來他們也是最令人困惑、最難掌控的内容,而且我也從來沒有遇到過使用他們會讓我的代碼變得更好的場景。是以我把他們完全忽略了,但如果你有興趣想學習更多的話,有大量關于這些方面的論文可參考。
再次感謝閱讀到最後——并且敬請關注 ruby 元程式設計:第二部分。