軟件設計的哲學 第五章 隱藏信息


第四章論述了模塊的深度。本章以及隨后的幾章將討論創建深度模塊的技術。

5.1 信息隱藏

實現深度模塊最重要的技術是信息隱藏。這種技術首先由David Parnas描述。基本思想是每個模塊應該封裝一些知識,這些知識表示設計決策。該知識嵌入到模塊的實現中,但不出現在其接口中,因此其他模塊無法看到它。

模塊中隱藏的信息通常包含如何實現某種機制的細節。以下是一些可能隱藏在模塊中的信息示例:

  • 如何在b樹中存儲信息,以及如何有效地訪問它。
  • 如何識別文件中每個邏輯塊對應的物理磁盤塊。
  • 如何實現TCP網絡協議。
  • 如何調度多核處理器上的線程。
  • 如何解析JSON文檔。

隱藏信息包括與該機制相關的數據結構和算法。它還可以包括低級的細節,如頁面的大小,還可以包括更抽象的高級概念,如大多數文件都很小的假設。

信息隱藏在兩個方面降低了復雜性。首先,它將接口簡化為模塊。 接口反映了模塊功能的更簡單、更抽象的視圖,並隱藏了細節;這減少了使用該模塊的開發人員的認知負擔。例如,使用B-tree類的開發人員不必擔心樹中節點的理想扇出,也不必擔心如何保持樹的平衡。其次,信息隱藏使系統更容易演化。 如果隱藏了一段信息,那么在包含該信息的模塊之外就不存在對該信息的依賴,因此與該信息相關的設計更改將只影響一個模塊。例如,如果TCP協議發生了變化(例如,為了引入一種新的擁塞控制機制),協議的實現就必須進行修改,但是在使用TCP發送和接收數據的高級代碼中不需要進行任何修改。

在設計新模塊時,您應該仔細考慮哪些信息可以隱藏在該模塊中。如果您可以隱藏更多的信息,您還應該能夠簡化模塊的接口,這使得模塊更加深入。

注意:通過將變量和方法聲明為私有來隱藏它們與信息隱藏是不同的。私有元素可以幫助信息隱藏,因為它們使項不可能從類外部直接訪問。但是,關於私有項的信息仍然可以通過公共方法(如getter和setter方法)公開。當這種情況發生時,變量的性質和用法就像公開變量一樣公開。

信息隱藏的最佳形式是將信息完全隱藏在模塊中,這樣模塊的用戶就不會看到這些信息。 然而,部分信息隱藏也有其價值。例如,如果某個特定的特性或信息片段只由少數類用戶需要,並且通過單獨的方法訪問它,因此在最常見的用例中不可見,那么該信息大部分是隱藏的。這樣的信息創建的依賴關系要比每個類用戶可見的信息少。

5.2 信息泄漏

信息隱藏的反面是信息泄露。當一個設計決策反映在多個模塊中時,就會發生信息泄漏。這在模塊之間創建了一個依賴關系:對設計決策的任何更改都需要對所有相關模塊進行更改。如果一段信息反映在模塊的接口中,那么根據定義,它已經被泄露了;因此,更簡單的接口往往與更好的信息隱藏相關。然而,即使信息沒有出現在模塊的接口中,它也可能被泄露。假設兩個類都知道某種特定的文件格式(可能一個類讀取這種格式的文件,另一個類寫入這些文件)。即使這兩個類都沒有在其接口中公開該信息,它們也都依賴於文件格式:如果格式更改,則需要修改這兩個類。像這樣的后門泄漏比通過接口泄漏更有害,因為它並不明顯。

信息泄漏是軟件設計中最重要的危險信號之一。 作為一個軟件設計師,你能學到的最好的技能之一就是對信息泄露的高度敏感性。如果您在類之間遇到信息泄漏,請自問“我如何才能重新組織這些類,使這些特定的知識只影響一個類?”如果受影響的類相對較小,並且與泄漏的信息緊密相關,那么將它們合並到一個類中是有意義的。另一種可能的方法是從所有受影響的類中提取信息,並創建一個只封裝這些信息的新類。但是,這種方法只有在您能夠找到一個從細節中抽象出來的簡單接口時才有效;如果新類通過其接口公開了大部分知識,那么它就不會提供太多的價值(您只是用通過接口的泄漏替換了后門泄漏)。

危險信號:信息泄漏
當在多個地方使用相同的知識時,例如兩個不同的類都理解特定類型文件的格式,就會發生信息泄漏。

5.3 時間分解

信息泄漏的一個常見原因是我稱之為時間分解的設計風格。在時間分解中,系統的結構與操作發生的時間順序相對應。考慮這樣一個應用程序,它以特定的格式讀取文件,修改文件的內容,然后再次將文件寫出來。使用臨時分解,這個應用程序可以分為三個類:一個用於讀取文件,另一個用於執行修改,第三個用於寫出新版本。文件的讀取和寫入步驟都需要了解文件的格式,從而導致信息泄漏。解決方案是將讀寫文件的核心機制合並到一個類中。這個類將在應用程序的讀寫階段使用。很容易陷入時間分解的陷阱,因為在編寫代碼時,必須考慮操作發生的順序。然而,在應用程序的生命周期中,大多數設計決策都會在幾個不同的時間出現;因此,時間分解常常導致信息泄漏。

順序通常很重要,所以它將反映在應用程序的某個地方。但是,它不應該反映在模塊結構中,除非該結構與信息隱藏一致(可能不同的階段使用完全不同的信息)。在設計模塊時,要關注執行每個任務所需的知識,而不是任務發生的順序。

危險信號:時間分解
在時間分解中,執行順序反映在代碼結構中:在不同時間發生的操作位於不同的方法或類中。如果在不同的執行點使用相同的知識,它就會被編碼到多個地方,從而導致信息泄漏。

5.4示例:HTTP服務器

為了說明信息隱藏中的問題,讓我們考慮一下在軟件設計課程中實現HTTP協議的學生所做的設計決策。看到他們做得好的地方和有問題的地方是很有用的。

HTTP是Web瀏覽器用來與Web服務器通信的一種機制。當用戶單擊Web瀏覽器中的鏈接或提交表單時,瀏覽器使用HTTP通過網絡向Web服務器發送請求。一旦服務器處理了請求,它會向瀏覽器發送一個響應;響應通常包含要顯示的新Web頁面。HTTP協議指定請求和響應的格式,兩者都以文本形式表示。圖5.1顯示了一個描述表單提交的示例HTTP請求。課程要求學生實現一個或多個類,以便Web服務器更容易地接收傳入的HTTP請求並發送響應。

圖5.1:HTTP協議中的POST請求由通過TCP套接字發送的文本組成。每個請求包含一個初始行、一個以空行結尾的報頭集合和一個可選的主體。初始行包含請求類型(POST用於提交表單數據)、指示操作(/comments/create)的URL和可選參數(photo_id的值為246),以及發送方使用的HTTP協議版本。每個標題行由一個名稱組成,例如Content-Length后跟它的值。對於這個請求,主體包含額外的參數(注釋和優先級)。

5.5 示例:類太多

學生最常犯的錯誤是把代碼分成大量的淺層類,導致類與類之間的信息泄露。一個團隊使用兩個不同的類來接收HTTP請求;第一個類將來自網絡連接的請求讀入一個字符串,第二個類解析該字符串。這是一個時間分解的例子(“首先我們讀取請求,然后我們解析它”)。信息泄漏是由於HTTP請求在不解析大部分消息的情況下無法讀取;例如,Content-Length報頭指定請求體的長度,因此必須解析報頭才能計算總請求長度。因此,這兩個類都需要理解HTTP請求的大部分結構,解析代碼在這兩個類中都是重復的。這種方法還為調用者增加了額外的復雜性,他們必須以特定的順序調用不同類中的兩個方法才能接收請求。

因為這些類共享了如此多的信息,所以最好將它們合並到一個處理請求讀取和解析的類中。這提供了更好的信息隱藏,因為它將所有關於請求格式的知識隔離在一個類中,而且它還為調用者提供了更簡單的接口(只需調用一個方法)。

這個例子說明了軟件設計中的一個普遍主題:信息隱藏通常可以通過使類稍微大一點來改進。這樣做的一個原因是將與特定功能相關的所有代碼(比如解析HTTP請求)放在一起,這樣得到的類就包含了與該功能相關的所有內容。增加類大小的第二個原因是提高接口的級別;例如,不是為一個計算的三個步驟中的每個步驟使用單獨的方法,而是使用單個方法來執行整個計算。這可以導致一個更簡單的接口。這兩個優點都適用於前一段的示例:組合這些類將所有與解析HTTP請求相關的代碼放在一起,並將兩個外部可見的方法替換為一個。合並的類比原來的類更深。

當然,大類的概念可能太過寬泛(例如對於整個應用程序只有一個類)。第9章將討論在什么情況下將代碼分成多個更小的類是有意義的。

5.6 示例:HTTP參數處理

服務器接收到HTTP請求后,服務器需要訪問請求中的一些信息。處理圖5.1中的請求的代碼可能需要知道photo_id參數的值。參數可以在請求的第一行中指定(圖5.1中的photo_id),有時也可以在正文中指定(圖5.1中的注釋和優先級)。每個參數都有一個名稱和一個值。參數的值使用一種稱為URL編碼的特殊編碼;例如,在圖5.1中的comment值中,使用“+”表示空格字符,使用“%21”代替“!”。為了處理請求,服務器將需要一些參數的值,並希望它們以未編碼的形式出現。

大多數學生項目在參數處理方面做出了兩個很好的選擇。首先,他們認識到服務器應用程序並不關心參數是在頭行還是請求體中指定的,所以他們對調用者隱藏了這種區別,並將來自兩個位置的參數合並在一起。其次,他們隱藏了URL編碼的知識:HTTP解析器在將參數值返回到Web服務器之前對其進行解碼,這樣圖5.1中的comment參數值將被返回為“多么可愛的嬰兒啊!””,而不是“+ +可愛+嬰兒% 21”)。在這兩種情況下,信息隱藏導致使用HTTP模塊的代碼使用更簡單的api。

然而,大多數學生使用的接口返回的參數太淺,這導致了信息隱藏機會的丟失。大多數項目使用HTTPRequest類型的對象來保存解析后的HTTP請求,而HTTPRequest類只有一個像下面這樣的方法來返回參數:

public Map<String, String> getParams() {     
    return this.params;
}

該方法不是返回單個參數,而是返回內部用於存儲所有參數的映射的引用。這個方法是淺層的,它公開HTTPRequest類用來存儲參數的內部表示。對該表示的任何更改都將導致接口的更改,這將需要對所有調用者進行修改。在修改實現時,更改通常涉及關鍵數據結構表示的更改(例如,為了提高性能)。因此,盡量避免暴露內部數據結構是很重要的。這種方法還為調用者提供了更多的工作:調用者必須首先調用getParams,然后必須調用另一個方法從映射中檢索特定的參數。最后,調用者必須意識到他們不應該修改getParams返回的映射,因為這會影響HTTPRequest的內部狀態。

這里是一個更好的接口檢索參數值:

public String getParameter(String name) { ... }
public int getIntParameter(String name) { ... }

getParameter以字符串的形式返回參數值。它提供了比上面的getParams稍微深一些的接口;更重要的是,它隱藏了參數的內部表示。getIntParameter將參數的值從HTTP請求中的字符串形式轉換為整數(例如,圖5.1中的photo_id參數)。這將使調用者不必分別請求字符串到整數的轉換,並向調用者隱藏該機制。如果需要,可以定義其他數據類型的其他方法,如getDoubleParameter。(如果所需的參數不存在,或者不能轉換為請求的類型,所有這些方法都會拋出異常;上面的代碼中省略了異常聲明)。

5.7 示例:HTTP響應中的默認值

HTTP項目還必須為生成HTTP響應提供支持。學生們在這方面最常犯的錯誤就是不充分的違約。每個HTTP響應必須指定一個HTTP協議版本;一個團隊要求調用者在創建響應對象時顯式地指定此版本。但是,響應版本必須與請求對象中的響應版本對應,並且在發送響應時必須已經將請求作為參數傳遞(它指示將響應發送到何處)。因此,HTTP類自動提供響應版本更有意義。調用者不太可能知道要指定什么版本,如果調用者指定了一個值,可能會導致HTTP庫和調用者之間的信息泄漏。HTTP響應還包括一個日期標頭,指定發送響應的時間;HTTP庫也應該為此提供一個合理的缺省值。

默認值說明了這樣一個原則,即接口的設計應該盡可能使普通情況變得簡單。它們也是部分信息隱藏的一個例子:在正常情況下,調用者不需要知道缺省項的存在。在調用者需要覆蓋默認值的少數情況下,它必須知道這個值,並且可以調用一個特殊的方法來修改它。

只要可能,類應該“做正確的事情”,而不是被顯式地詢問。默認值就是一個例子。第26頁的Java I/O示例以一種消極的方式說明了這一點。文件I/O中的緩沖是普遍需要的,因此沒有人需要顯式地請求它,甚至不需要知道它的存在;I/O類應該做正確的事情並自動提供它。最好的功能是那些你甚至不知道它們存在的功能。

危險信號:過度曝光
如果一個常用特性的API迫使用戶了解很少使用的其他特性,這將增加不需要這些很少使用的特性的用戶的認知負擔。

5.8 隱藏在類中的信息

本章中的示例主要關注與類的外部可見api相關的信息隱藏,但是信息隱藏也可以應用於系統中的其他級別,比如類。嘗試在類中設計私有方法,以便每個方法都封裝一些信息或功能,並對類的其他部分隱藏這些信息或功能。此外,盡量減少使用每個實例變量的數量。有些變量可能需要在整個類中廣泛地訪問,但其他變量可能只需要在少數地方訪問;如果可以減少使用變量的位置數量,就可以消除類中的依賴性並降低其復雜性。

5.9 不要過度隱藏

信息隱藏只有在被隱藏的信息在其模塊之外不需要時才有意義。如果需要模塊之外的信息,則不能隱藏它。假設某個模塊的性能受到某些配置參數的影響,並且模塊的不同使用需要不同的參數設置。在這種情況下,在模塊的接口中公開參數是很重要的,這樣就可以對它們進行適當的轉換。作為一名軟件設計師,您的目標應該是最小化模塊外所需的信息量;例如,如果模塊可以自動調整其配置,這比公開配置參數要好。但是,重要的是要識別模塊外部需要哪些信息,並確保它是公開的。

5.10 結論

信息隱藏與深度模塊密切相關。如果一個模塊隱藏了很多信息,就會增加模塊提供的功能,同時也減少了它的接口。這使得模塊更深入。相反,如果一個模塊沒有隱藏很多信息,那么要么它沒有太多的功能,要么它有一個復雜的接口;不管怎樣,這個模塊都是淺層的。

在將系統分解成模塊時,盡量不受運行時操作發生順序的影響;這將引導您進入時間分解的路徑,這將導致信息泄漏和淺模塊。相反,請考慮執行應用程序任務所需的不同知識片段,並設計每個模塊來封裝這些知識片段中的一個或幾個。這將產生一個干凈和簡單的設計與深模塊。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM