第 6 章 通用的模組是更深的
設計新模組時,您將面臨的最常見的決定之一就是以通用還是專用方式實現它。有人可能會爭辯說,您應該採用通用方式,在這種方式中,您將實現一種可用於解決廣泛問題的機制,而不僅是當前重要的問題。在這種情況下,該機制可能會在將來發現意外用途,從而節省時間。通用方式似乎與第 3 章中討論的投資思維一致,您花了更多時間在前面,以節省以後的時間。
另一方面,我們知道很難預測軟體系統的未來需求,因此通用解決方案可能包含從未真正需要的功能。此外,如果您實現的東西過於通用,那麼可能無法很好地解決您今天遇到的特定問題。結果,有些人可能會爭辯說,最好只關注當今的需求,構建您所知道的需求,並針對您今天打算使用的方式進行專門化處理。如果您採用專用的方式並在以後發現要支援更多用途,您總是可以對其進行重構以使其通用。專用方式似乎與軟體開發的增量方式一致。
6.1 使類的介面足夠通用
以我的經驗,最有效的辦法是以有點通用的方式實現新模組。這裡的短語“有點通用”表示該模組的功能應反映您當前的需求,但其介面則不應該。相反,該介面應該足夠通用以支援多種用途。該介面應易於使用,以滿足當今的需求,而不必專門與它們綁在一起。“有點”這個詞很重要:不要忘乎所以,建立一些太過通用的東西,以至於很難滿足你當前的需求。
通用方式最重要的(也許是令人驚訝的)好處是,與專用方式相比,它導致更簡單、更深的介面。如果您將該類用於其他目的,則通用方式還可以節省將來的時間。但是,即使該模組僅用於其原始用途,由於其簡單性,通用方式仍然更好。
6.2 示例:為編輯器儲存文字
讓我們考慮一個軟體設計課程的示例,其中要求學生構建簡單的圖形介面文字編輯器。編輯器必須顯示一個檔案,並允許使用者指向、點選並輸入以編輯該檔案。編輯器必須支援同一檔案在不同視窗中的多個並行檢視;他們還必須支援檔案修改的多級撤銷和重做。
每個學生專案都包括一個管理檔案內的文字的類。文字類通常提供以下方法:將檔案載入到記憶體、讀取和修改檔案的文字以及將修改後的文字寫回到檔案。
許多學生團隊為文字類實現了專用的 API。他們知道該類將在互動式編輯器中使用,因此他們考慮了編輯器必須提供的功能,並針對這些特定功能定製了文字類的 API。例如,如果編輯器的使用者輸入了退格鍵,則編輯器會立即刪除游標左側的字元;如果使用者鍵入了刪除鍵,則編輯器會立即刪除游標右側的字元。知道這一點後,一些團隊在文字類中針對每個特定功能都建立了一個方法:
void backspace(Cursor cursor);
void delete(Cursor cursor);
這些方法中的每一個都以游標位置作為引數,並用專用的型別 Cursor
來表示。編輯器還必須支援複製或刪除一個選擇的區域。學生透過定義 Selection
類並在刪除過程中將該類的物件傳遞給文字類來解決此問題:
void deleteSelection(Selection selection);
學生們可能認為,如果文字類的方法與使用者可見的功能相對應,則將更易於實現使用者介面。但是,實際上,這種專業化對使用者介面程式碼幾乎沒有好處,並且為使用者介面或文字類的開發人員帶來了很高的認知負荷。文字類最終包含了大量的淺方法,每個淺方法僅適用於一個使用者介面操作。許多方法(例如 delete
)僅在單個位置被呼叫。結果,使用者介面的開發人員必須學習文字類的大量方法。
這種方式在使用者介面和文字類之間造成了資訊洩露。與使用者介面有關的抽象(例如區域選擇或退格鍵)反映在文字類中;這增加了文字類的開發人員的認知負荷。每個新的使用者介面操作都需要在文字類中定義一個新方法,因此該使用者介面的開發人員最終可能也要處理這個文字類。類設計的目標之一是允許每個類獨立開發,但是專用方式將使用者介面和文字類繫結在了一起。
6.3 更通用的 API
更好的方法是使文字類更通用。其 API 應僅根據基本的文字功能進行定義,而不應反映用其實現的更高層級的操作。例如,只需提供兩個方法即可修改文字:
void insert(Position position, String newText);
void delete(Position start, Position end);
前一個方法在文字內的任意位置插入任意字串,後一個方法刪除大於或等於開始位置但小於結束位置的所有字元。此 API 還使用了更通用的 Position
型別來代替 Cursor
,後者則是特別針對使用者介面的。文字類還應該提供用於操縱文字中位置的通用方法,例如:
Position changePosition(Position position, int numChars);
此方法返回一個新位置,該位置與給定位置相距給定的字元數。如果 numChars
引數為正,則新位置在檔案中給定位置的後面;如果 numChars
為負,則新位置在給定位置之前。必要時,該方法會自動跳到下一行或上一行。使用這些方法,可以使用以下程式碼來實現刪除鍵(假定 cursor
變數儲存了當前游標的位置):
text.delete(cursor, text.changePosition(cursor, 1));
類似的,可以按以下方式實現退格鍵:
text.delete(text.changePosition(cursor, -1), cursor);
使用通用文字 API,實現使用者介面功能(如刪除和退格)的程式碼比使用專用文字 API 的原始方法要長一些。但是,新程式碼比舊程式碼更易理解。使用者介面模組的開發人員可能會關心退格鍵會刪除哪些字元。使用新程式碼,這是顯而易見的。使用舊程式碼,開發人員必須去文字類中閱讀退格方法的文件或程式碼以驗證該行為。此外,通用方法總體上比專用方法具有更少的程式碼,因為它用較少數量的通用方法代替了文字類中的大量專用方法。
使用通用介面實現的文字類除互動式編輯器外,還可以用於其他目的。作為一個示例,假設您正在構建一個應用程式,該應用程式透過將所有出現的特定字串替換為另一個字串來修改指定檔案。專用文字類中的方法(例如 backspace
和 delete
)對於此應用程式幾乎沒有價值。但是,通用文字類已經具有新應用程式所需的大多數功能。缺少的只是一個搜尋給定字串的下一個匹配項的方法,例如:
Position findNext(Position start, String string);
當然,互動式文字編輯器可能具有搜尋和替換的機制,在這種情況下,文字類將已經包含此方法。
6.4 通用性可以更好地隱藏資訊
通用方法在文字類和使用者介面類之間提供了更清晰的分隔,從而可以更好地隱藏資訊。文字類不需要知道使用者介面的詳細資訊,例如如何處理退格鍵。這些細節現在封裝在使用者介面類中。在新增新的使用者介面功能時,也無需在文字類中建立新的支援方法。通用介面還減輕了認知負荷:使用者介面的開發人員只需要學習幾個簡單的方法,就可以將其複用於各種目的。
文字類原始版本中的 backspace
方法是錯誤的抽象。它旨在隱藏有關刪除哪些字元的資訊,但是使用者介面模組確實需要知道這一點。使用者介面開發人員可能會需要閱讀 backspace
方法的程式碼以確認其精確的行為。將方法放在文字類中只會使使用者介面開發人員更難獲得所需的資訊。軟體設計最重要的元素之一就是確定誰需要知道什麼以及何時需要知道。當細節很重要時,最好使它們明確且儘可能明顯,例如修訂的退格鍵操作實現。將這些資訊隱藏在介面後面只會產生模糊性。
6.5 問自己一些問題
識別乾淨的通用類設計要比建立它更簡單。您可以問自己一些問題,這將幫助您在介面的通用和專用之間找到適當的平衡。
滿足我當前所有需求的最簡單的介面是什麼? 如果能減少 API 中的方法數量而不降低其整體功能,那您可能正在建立更通用的方法。專用的文字 API 至少具有三個刪除文字的方法:backspace
、delete
和 deleteSelection
。而更通用的 API 只有一個刪除文字的方法,它可以同時滿足所有三個目的。僅在每個方法的 API 都保持簡單的前提下,減少方法的數量才有意義。如果您必須引入許多額外的引數以減少方法數量,那麼您可能並沒有真正簡化事情。
這個方法會在多少種情況下被使用? 如果一個方法是為特定用途而設計的,例如 backspace
方法,那就是一個表明它可能過於專用的危險訊號。看看是否可以用一個通用方法替換幾個專用方法。
這個 API 對於我當前的需求來說是否易於使用? 這個問題可以幫助確定當你在讓一個 API 變得簡單和通用時是否走得太遠了。如果您必須編寫許多其他程式碼才能將類用於當前的用途,那麼這是一個介面沒有提供正確功能的危險訊號。例如,針對文字類的一種方式是圍繞單字元操作進行設計:用於插入單個字元的 insert
方法和用於刪除單個字元的 delete
方法。該 API 既簡單又通用。但是,對於文字編輯器來說並不是特別容易使用:更高層級的程式碼將包含許多用於插入或刪除字元範圍的迴圈。單字元方法對於大型操作也將是低效的。因此,文字類最好內建對字元範圍操作的支援。
6.6 結論
通用介面比專用介面具有許多優點。它們往往更簡單,提供的方法更少也更深。它們還提供了類之間的更清晰的分隔,而專用介面則傾向於在類之間洩露資訊。使模組的介面具有一定程度的通用性是降低整體系統複雜性的最佳方法之一。