本節透過軟體設計課程中學生實作 HTTP 協定的設計決策,來看資訊隱藏的好例子與壞例子。

HTTP 背景#

  • HTTP 是瀏覽器與 Web 伺服器溝通的協定
  • 使用者點連結或送出表單時,瀏覽器透過 HTTP 把請求送到伺服器
  • 伺服器處理後回傳回應(通常是新的網頁)
  • HTTP 協定規定請求與回應的格式(皆為文字格式)

學生被要求實作一個(或多個)類別,讓 Web 伺服器能方便接收 HTTP 請求並回傳回應。

Figure 5.1: HTTP 協定中的 POST 請求樣本——首行(請求類型、URL、參數、版本)、若干 header(如 Content-Length)、空行、可選 body

POST 請求的構造:第一行(含請求類型、URL、可選參數、HTTP 版本)、若干 header、空行、可選的 body。

例如 header 中的 Content-Length、body 中的 commentpriority 等表單欄位。

反例 1:類別太多#

最常見的錯誤:把程式拆成大量淺類別,造成類別之間資訊洩漏。

某團隊做法

  • 用兩個類別接收 HTTP 請求
  • 第一個類別把請求從網路連線讀進字串
  • 第二個類別解析該字串

這是典型的時序分解(「先讀再解析」),結果:

  • HTTP 請求不解析就讀不完整——例如 Content-Length header 指定 body 長度,必須先解析 header 才能知道整個請求多長
  • 因此兩個類別都得理解 HTTP 請求結構,解析程式碼出現重複
  • 呼叫端要照特定順序呼叫兩個方法 → 介面更複雜

較好的做法:合併成單一類別同時負責讀取與解析。

軟體設計的一個普遍主題:有時讓類別「稍微大一點」,反而能改善資訊隱藏。

兩個原因:

  • 把與某個能力相關的程式碼通通放在一起(例如把所有解析 HTTP 請求的程式碼放在同一個類別)
  • 提高介面層次:把多步驟拆成的多個方法合併成單一方法

合併後的類別比原來的類別都更深。當然這也不能無限上綱(極端就是整個應用一個類別);第 9 章會討論何時該拆。

反例 2:HTTP 參數的處理#

伺服器收到請求後,往往需要存取請求中的某些參數(例如圖中的 photo_id)。

學生做對的兩件事#

  1. 隱藏參數位置:合併首行參數與 body 參數,呼叫端不需要關心參數從哪裡來
  2. 隱藏 URL encoding:解析器返回前就解碼,所以呼叫端拿到的是 "What a cute baby!",而不是 "What+a+cute+baby%21"

但取參數的介面太淺#

多數學生這樣寫:

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

問題:

  • 不是回傳單一參數,而是把內部用來存所有參數的 Map 整個吐出來
  • 內部資料結構的選擇被暴露在介面上:未來改實作(例如為了效能換資料結構),介面就得改,所有呼叫端都得跟著改
  • 呼叫端要先呼叫 getParams,再從 Map 取參數 → 多走一步
  • 呼叫端必須知道「不能修改回傳的 Map」(否則會影響 HTTPRequest 內部狀態)

較好的設計#

public String getParameter(String name) { ... }
public int getIntParameter(String name) { ... }
  • getParameter 直接以字串回傳指定參數值,隱藏內部表示
  • getIntParameter 直接做字串到整數的轉換,把轉換機制也封裝起來
  • 必要時可加 getDoubleParameter 等其他資料型別版本
  • 找不到參數或無法轉型時拋出例外(範例略過例外宣告)

反例 3:HTTP 回應的預設值#

學生在這部分最常犯的錯:預設值給得不夠

不好的做法#

某團隊要求呼叫端建立回應時明確指定 HTTP 版本。但:

  • 回應版本必須對應到請求版本
  • 而且建立回應時就會把請求物件當參數傳入
  • 呼叫端通常不知道要填什麼版本,強行讓呼叫端填,反而造成 HTTP 函式庫與呼叫端之間的資訊洩漏

Date header 也類似:函式庫應自動填合理的預設值。

設計原則:讓常見情況最簡單#

介面設計的核心:讓常見情況盡可能簡單。

預設值是「部分資訊隱藏」的範例:

  • 一般情況下,呼叫端不需要意識到該欄位存在
  • 少數需要覆寫預設值的情況才需要知道,並透過特殊方法修改

能自動「做對的事」的類別,就不該逼使用者明確要求。

Java I/O 的反例(須明確 BufferedInputStream)就是負面教材:緩衝幾乎人人都需要,沒人應該需要明確要求或意識到它存在

最好的功能是「你連它存在都不知道,卻已經受惠於它」。

紅旗:過度暴露(Red Flag: Overexposure)#

如果某個常用功能的 API 強迫使用者先學會其他極少用到的功能,便提高了不需要這些功能的使用者的認知負擔。