上層告知,下層請求——物件導向混搭函式式設計(OO專家Michael Feathers的奇思妙想)

高翌翔發表於2012-04-01

我有個想法,可是好一陣子都猶豫不決,因為我覺得它是錯的。它只是過於通用,以至於令人難以置信,儘管我承認我的觀點略顯抽象,不過我認為……


我有個想法,可是好一陣子都猶豫不決,因為我覺得它是錯的。它只是過於通用,以至於令人難以置信,儘管我承認我的觀點略顯抽象,不過我認為其中另有玄機。就從這裡開始吧!

Michael Feathers

物件導向(object-orientation)更適合更高的系統層次,而函數語言程式設計(functional programming)更適合較低的系統層次。

有趣的想法,不過我們又該如何實現呢?

好吧,對我而言,這就回到了那些方法的基本原理。

函數語言程式設計(functional programming)

functional programming

儘管函數語言程式設計風格五花八門——定義也是千差萬別,但萬變不離其宗:通過減少副作用,以便我們程式設計時更順手。一旦沒有了副作用,你就可以更輕鬆地推理程式碼。你還會擁有引用透明性(referential transparency)——即能夠在程式中把某個表示式從一處貼上到另一處,並確信對於給定的相同輸入,都將產生完全相同的輸出,且不會在其他地方導致討厭的副作用。相應的技術名稱是“純潔性”(purity)。

純潔性(purity)不僅使程式碼理解起來更輕鬆,而且使惰性計算(lazy evaluation)成為可能。以防你從前對此一無所知,因此我們先來掃一下盲,在一些函數語言程式設計語言中沒有“呼叫函式”(calling a function)的說法,而是說“應用此函式”(apply the function)。這不只是命名原則的區別。在Haskell中,表示式 [1..10] 的計算結果為 1,2,3,4,5,6,7,8,9,10。如果把 take 函式應用於該序列,並提供引數 5take 5 [1..10]),那麼你會得到 1,2,3,4,5,即該序列的前5個元素。

當在Haskell中計算 [1..] 時,你覺得結果如何?對極了,你會得到從1開始的無限整數序列。在互動式提示符下,你必須在某一時候鍵入 Ctrl^C 來終止列印。既然如此,那麼下面這個表示式是何結果?

take 5 [1..]

你可能認為,此表示式也會永遠執行下去。畢竟,在把 [1..] 傳遞給 take 函式前,必須先對其進行計算,而且算起來就永遠不停。不過,由於惰性計算(lazy evaluation)的緣故,這種情況並不會發生。之所以說你沒在呼叫 take 函式,是因為你正在通過函式應用產生一個表示式。當你計算整個表示式時,執行庫只執行用於返回首個結果1所需的子表示式的計算,接著對第二個結果進行計算,一直算到上限5為止。

惰性計算(lazy evaluation)可能非常強大,不過關鍵在於:只有獲得純潔性(purity)的支援,惰性計算才能徹底啟用。如果你想對此一探究竟,那麼請想象有個巨大的函式表示式,且副作用深藏其中。究竟何時會出問題呢?確實無可奉告。因為這取決於被計算表示式的上下文。而在理想情況下,你根本無需關心此類問題。

物件導向(object-orientation)

object-orientation

物件導向也有某些類似的可用性。同理,物件導向的定義也是種類繁多,不過我想回到Alan Kay的最初構想上來。因為畢竟是他發明了這個術語。

Alan Kay把物件視為創造複雜系統的一種方式,且此方式非常符合大自然處理生物學複雜性的方式。在有機體中,不僅有許多細胞,而且細胞彼此之間通過傳遞化學資訊進行通訊。Smalltalk使用“訊息傳送”(message send)的說法而非函式呼叫(function call)不僅僅是巧合。物件結構的絕妙之處就在於,它並不突出玩家,而是最大化遊戲本身。正如Kay所暗示的,訊息比物件更重要。在生物系統中,這點會發展到全身冗餘,即你無法通過殺死單個細胞來打垮整個有機體。在軟體領域中,我們所掌握的與此最接近的就是Erlang程式模型,它被比作理想的物件系統。

在21世紀初,Dave Thomas和Andy Hunt曾撰寫過關於被其稱為“告知,不要請求”(Tell, Don’t Ask)的設計指南的文章。其思想是,只需要向物件分派任務,而不是向它們索取資料,然後由你親自加工處理資料,這樣的物件才是最棒的。不僅從封裝(encapsulation)的角度看,此觀點簡直完美之極。而且使物件使用者更簡便地使用物件。一旦你取回了資料,你就必須瞭解它,並對它進行操作。與告知物件運用自身資料做某事比起來,這可不那麼簡單。

如前所述,在生物學中,細胞之間會使用化學訊息。但是,在一個非常重要的方面,它們不同於典型的物件導向——細胞之間的通訊是非同步的。在細胞接收到某個響應以前,其任何內部活動都不會受阻。而物件系統卻通常不是這樣做。我們會給另一物件傳送訊息,然後等待響應。一旦我們得到含有返回值(資料)的響應時,便會如坐鍼氈。因為我們必須對它做點兒什麼,或者選擇忽略它——控制流再次回到我們手中。當你執行同步呼叫時,最終違反“告知,不要請求”簡直是易如反掌。因為同步呼叫通常都有返回值,終究,返回值都是隱式“請求(ask)”的結果

如果我們像細胞一樣來看待物件導向,那麼似乎我們所用的大部分技術都有毛病。在某種程式語言中,我們可能會擁有類和物件,但只要執行同步呼叫,那些部件就無法像它們本來那樣獨立。存在比我們所謂的物件導向更加面嚮物件的技術麼?是的,確實存在。資訊科技架構(IT architectures)中的訊息系統幾乎都獲得了此獨立級別。

混搭

到目前為止,我們已看過物件導向設計和函數語言程式設計。我覺得它們之間真是齊頭並進、互不干擾。

在物件導向中,最好使用告知(tell)。一旦你使用告知(tell)的方式,你就最大限度地實現了實體間的解耦(decoupling)。如果你想防止再次耦合(re-coupling),那麼你就要傳送非同步訊息。告知(tell)模型使這一切成為可能。而在函數語言程式設計中,最好使用請求(ask)。事實上,在純函數語言程式設計中也別無他法可言。不返回任何內容的函式是沒有意義的,除非它有某種副作用,而且我們應該避免此類情況發生。可以採用與物件導向啟用非同步(asynchrony)相同的方式來讓函式的純潔性啟用惰性(laziness)。

現在,如果我們接受這些前提,那麼當我們組織系統時,使用何種方式最有利?我們可以在頂層放置函式層(functional layer),其職責是,當計算表示式時,在內部執行訊息傳送,不過這可能對系統理解造成麻煩。此外,可能會在函式層之外產生副作用,並破壞函式層的純潔性。

反方向會怎麼樣?如果我們將物件層(object layer)置於頂層,並允許物件使用下層函式片段,那麼又會發生什麼情況?毫無疑問可以這樣做,千真萬確!無副作用的函式是處理內部機制的理想方式。而物件導向是高層的絕妙之選,在那裡實現的解耦和資訊隱藏都是至關重要的。

因此,這就是本文的觀點。並且我知道此觀點未必永遠“正確”。像Scala等一些語言允許程式設計師在任何抽象級別上把物件和函式隨意組合。你可以明確擁有那些用於選擇和篩選物件的函式。還有微軟的LINQ技術就是位於物件之上的函式層。儘管如此,我認為我的觀點仍具有一些真實性。物件導向的初衷與實踐中的物件導向可謂大相徑庭。或者換言之,在我們現有的物件導向技術中,至少在現代軟體架構的服務和訊息級別上確實要更接近物件導向的初衷。在服務和訊息的抽象級別上,這似乎是正確的——最好上層告知,下層請求。

檢視英文原文:Tell Above, and Ask Below - Hybridizing OO and Functional Design

作者簡介

enter image description here
Michael Feathers是世界級物件導向技術專家,以豐富的軟體專案開發經驗著稱。目前在世界頂尖的軟體諮詢公司Object Mentor從事敏捷方法/極限程式設計、測試驅動開發、重構、物件導向設計、Java、C#和C++等方面的培訓和專案指導。他是著名測試框架CppUnit和FitCpp的開發者,已經主持了三次物件導向界盛會OOPSLA上的CodeFest比賽。他還是《修改程式碼的藝術》(Working Effectively with Legacy Code)一書的作者。

iTran樂譯

iTran樂譯參加活動,讀好文章!

相關文章