天天看點

《設計模式沉思錄》—第2章2.1節基礎

本節書摘來自異步社群《設計模式沉思錄》一書中的第2章,第2.1節基礎,作者【美】john vlissides,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

第2章 運用模式進行設計

設計模式沉思錄

如果想體驗一下運用模式的感覺,那麼最好的方法就是運用它們。對我來說,最大的挑戰在于找到一個所有人都能夠了解的示例。人們對自己的問題最感興趣,如果某些人對某個示例越感興趣,這個示例往往就越具體。問題在于,這樣的示例所涉及的問題往往太過晦澀,對于沒有相關領域背景的人來說難以了解。

層級檔案系統(hierarchical file system)是每個計算機使用者都熟悉的東西,就讓我們來看看該如何設計它。我們不會關心諸如i/o緩沖和磁盤扇區管理之類的底層實作問題,我們要關心的是設計一個讓應用程式開發人員使用的程式設計模型——檔案系統的api。在大多數的作業系統中,這樣的api通常包含大量的過程調用和一些資料結構,但對擴充性的支援卻很少或根本沒有。我們的設計将完全是面向對象的而且是可擴充的。

我們首先集中讨論這個設計最重要的兩方面,以及用來對這兩方面進行處理的模式。然後我會在這個示例的基礎上展示其他模式是如何解決設計問題的。本章的目的并不是要為如何應用模式規定一個嚴格的流程,也不是要展示設計檔案系統的最佳方法,而是要鼓勵讀者自己應用模式。用得越多,看得越多,你在處理模式的時候就會感到越輕松。最終,你将慢慢地學會應用模式所需的精湛技藝:屬于你自己的技藝。

2.1 基礎

從使用者的角度來看,無論檔案有多大,目錄結構有多複雜,檔案系統都應該能夠對它們進行處理。檔案系統不應該對目錄結構的廣度或深度施加任何限制。從程式員的角度來看,檔案結構的表示方法不僅應該容易處理,而且應該容易擴充。

假設我們正在實作一個用來列出一個目錄中檔案的指令。我們編寫的用來得到一個目錄的名字的代碼與用來得到一個檔案的名字的代碼相比,應該沒有差別,也就是說,同樣的代碼應該能夠同時處理這兩種情況。換句話說,在請求目錄的名字和檔案的名字時,應該能夠以相同的方式處理。這樣得到的代碼将會更易于編寫和維護。我們還想在不重新實作部分系統的前提下,加入新的檔案類型(比如符号化連結)。

是以,一開始有兩件事情非常清楚:一是檔案和目錄是這個問題域(problem domain)的關鍵元素,二是我們需要一種方式,能夠讓我們在完成設計之後再為這些元素引入特别的版本。一種顯而易見的設計方法是用對象來表示這些元素。

我們如何實作圖2-1所示的結構呢?有兩種對象,這意味着我們需要兩個類——一個用來表示檔案,另一個用來表示目錄。我們還想以同樣的方式處理檔案和目錄,這意味着它們必須有一個共同的接口。更進一步說,這意味着這兩個類必須派生自一個共同的(抽象)基類,我們稱之為node。最後,我們還知道目錄中包含檔案。

《設計模式沉思錄》—第2章2.1節基礎

所有這些限制基本上已經替我們把類的層次結構定義出來了。

另一個需要斟酌的問題與共同接口的組成有關。哪些操作既能夠适用于檔案又能夠适用于目錄呢?

檔案和目錄有各種各樣的共同屬性,比如名字、大小、保護屬性等。每個屬性可以有相應的操作來通路和修改它的值。以相同的方式來處理那些對檔案和目錄都有明确意義的操作是很簡單的事情。但是,想以相同的方式來處理那些不能明确适用于兩者的操作時,問題就随之而來。

舉個例子,使用者經常執行的一項操作就是列出一個目錄中的所有檔案。這意味着directory需要一個接口來枚舉它的子節點。下面這個簡單的接口用來傳回第n個子節點。

由于一個目錄既可能包含file對象,也可能包含directory對象,是以getchild必須傳回一個node*。這個傳回值的類型衍生出一個重要的結果:它強制我們不僅要在directory類中定義getchild,而且還要在node類中定義該接口。為什麼?因為我們想要能夠列出子目錄的子節點。實際上,使用者經常想要通路檔案系統結構的下一層。除非不用強制轉換就能用getchild的傳回值來調用getchild,否則是無法通過一種靜态的、類型安全的方式來完成這個操作的。是以,和屬性操作一樣,getchild是我們想要同時用在檔案和目錄上的操作。

同時,getchild也是允許我們以遞歸的方式來定義directory的操作的關鍵。假設node聲明了一個size操作,這個操作傳回該目錄樹(及其子樹)所占用的總位元組數。directory可以這樣定義自己的這個操作:依次調用它所有子節點的size操作,将所有的傳回值相加,得到的總和就是自己的傳回值。

目錄和檔案的例子說明了composite模式最關鍵的幾個方面:它産生的樹結構可以支援任意的複雜度,它還規定了如何以統一的方式來處理這些樹結構中的對象。composite模式的意圖部分對這些方面進行了描述:

将對象組織成一個樹結構來表示“部分—整體”的層次結構,給客戶一種統一的方式來處理這些對象,無論這些對象是内部節點(internal node)還是葉節點(leaf)。

适用性部分描述了我們應該在以下場合使用composite模式。

我們想要表示對象的“部分—整體”層次結構。

我們想讓使用者能夠忽略複合對象和單個對象之間的差別。使用者将以統一的方式來處理複合結構中的所有對象。

該模式的結構部分用一個經過修改的omt①圖的形式,描繪了典型的composite類結構。之是以說它是典型,我的意思隻是它代表了我們(gof)所見過的類的最為常見的組織方式。它并不能代表最終得到的各個類及其關系,這是因為有時候受到某種設計或實作的影響,我們必須采取一些折中,這種情況下得到的接口可能會有所不同。(composite模式同樣對這些内容進行了闡述。)

圖2-2展示了composite模式涉及的各個類,以及這些類之間的靜态關系。我們的node類相當于component,它是一個抽象基類。file類相當于子類leaf,而directory類則相當于子類composite。從composite指向component的箭頭線表明composite包含了component類型的執行個體。箭頭前面的實心圓圈表示多于一個執行個體;如果沒有實心圓圈,則表示有且僅有一個執行個體。箭頭線尾部的菱形表示composite聚合了它的子執行個體,這也意味着删除一個composite會同樣删除它的子執行個體。它還意味着所有的component沒有被共享,是以確定了樹結構。composite模式的參與者和協作部分對各個類之間的靜态關系和動态關系分别進行了解釋。

《設計模式沉思錄》—第2章2.1節基礎

composite的效果部分總結了使用該模式的好處和壞處。好處是,composite支援任意複雜度的樹結構。這個特性産生的直接結果就是對客戶代碼隐藏了節點的複雜度:他們無法辨識出他們正在處理的component到底是一個leaf還是一個composite,事實上他們也沒有必要去辨識,這使得客戶代碼更加獨立于component的代碼。客戶代碼也變得更加簡單,因為它能夠以統一的方式來處理leaf和composite。客戶代碼再也不需要根據component的實際類型來決定要執行許多代碼分支中的哪一個分支。最好的是,我們可以添加新的component類型而無須修改已有的代碼。

但是composite的壞處在于它可能會産生這樣的系統:系統中每個對象的類與其他對象的類看起來都差不多。由于顯著的差別隻有在運作的時候才會顯現出來,是以這會使代碼難以了解,即便我們知道類的具體實作也無濟于事。此外,如果在一個比較低的層次運用該模式,或者運用該模式時的粒度太細,那麼對象的數量可能會多得讓系統負擔不起。

正如讀者可能已經猜到的那樣,composite模式的實作部分讨論了在實作該模式時會面臨的許多問題。

為了提高性能,應該在何時以及何處對資訊進行高速緩存;

component類應該配置設定多少存儲空間;

在存儲子節點的時候,應該使用什麼資料結構;

是否應該在component類中聲明那些用來添加和删除子節點的操作;

等等。

在開發我們的檔案系統時,我們将努力解決這些問題中的一部分,以及許多其他問題。

繼續閱讀