軟件設計的哲學: 第六章 更深的通用模塊


在設計新模塊時,最常見的一個決定就是以通用方式還是特殊方式實現它。有些人可能會說,應該采用通用的方法,即實現一種機制,用於解決廣泛的問題,而不僅僅是當前重要的問題。在這種情況下,新機制可能會在未來發現意想不到的用途,從而節省時間。通用方法似乎與第3章中討論的投資心態一致,即您預先花費更多的時間來節省以后的時間。

另一方面,我們知道很難預測軟件系統的未來需求,所以一個通用的解決方案可能包含一些實際上並不需要的設施。此外,如果您實現了一些過於通用的東西,那么它可能無法很好地解決您現在面臨的特定問題。因此,有些人可能會說,最好關注今天的需求,只構建您知道自己需要的東西,並根據您今天計划使用它的方式進行專門化。如果采用特殊用途的方法,並在以后發現其他用途,則始終可以對其進行重構,使其成為通用用途。專用方法似乎與軟件開發的增量方法一致。

6.1 使類具有一定的通用性

根據我的經驗,最好的方法是以某種通用的方式實現新模塊。短語“某種程度上通用的”意思是模塊的功能應該反映您當前的需求,但是它的接口不應該。相反,接口應該足夠通用,以支持多種用途。該接口應該易於使用,以滿足今天的需要,而不是專門針對他們。“有些”這個詞很重要:不要忘乎所以,不要構建一些通用的東西,因為它很難滿足您當前的需求。

通用方法最重要的(可能也是最令人驚訝的)好處是,它比專用方法產生更簡單、更深入的接口。如果您將該類用於其他目的,那么通用方法還可以在將來為您節省時間。然而,即使該模塊僅用於其原始目的,由於其簡單性,通用目的的方法仍然更好。

6.2 示例:為編輯器存儲文本

讓我們考慮一個來自軟件設計課程的例子,在這個課程中,學生被要求構建簡單的GUI文本編輯器。編輯器必須顯示一個文件,並允許用戶指向、單擊和鍵入來編輯文件。編輯器必須在不同的窗口中支持同一文件的多個同步視圖;他們還必須支持多級撤銷和重做文件的修改。

每個學生項目都包含一個管理文件底層文本的類。文本類通常提供將文件加載到內存、讀取和修改文件文本以及將修改后的文本寫回文件的方法。

許多學生團隊為text類實現了特殊用途的api。他們知道這個類將在交互式編輯器中使用,所以他們考慮了編輯器必須提供的特性,並根據這些特定的特性定制了文本類的API。例如,如果編輯器的用戶鍵入退格鍵,編輯器將立即刪除光標左邊的字符;如果用戶鍵入刪除鍵,編輯器將立即刪除光標右側的字符。了解了這一點,一些團隊在text類中創建了一個方法來支持這些特定的特性:

void backspace(Cursor cursor);
void delete(Cursor cursor);

這些方法中的每一個都以光標的位置作為參數;特殊類型游標表示此位置。編輯器還必須支持可以復制或刪除的選擇。學生們通過定義一個選擇類,並在刪除期間將這個類的對象傳遞給text類來處理這個問題:

void deleteSelection(Selection selection);

學生們可能認為如果text類的方法對應於用戶可見的特性,那么實現用戶界面會更容易。然而,在現實中,這種專門化對用戶界面代碼幾乎沒有什么好處,而且它為用戶界面或文本類的開發人員帶來了很高的認知負荷。text類以大量的淺層方法結束,每個淺層方法只適合一個用戶界面操作。許多方法(如delete)只在一個地方調用。因此,開發用戶界面的開發人員必須了解文本類的大量方法。

這種方法在用戶界面和文本類之間造成了信息泄漏。與用戶界面相關的抽象,如選擇或退格鍵,反映在文本類中;這增加了開發人員處理文本類的認知負荷。每一個新的用戶界面操作都需要在text類中定義一個新方法,因此處理用戶界面的開發人員可能最終也要處理text類。類設計的目標之一是允許獨立地開發每個類,但是專門化的方法將用戶界面和文本類綁定在一起。

6.3更通用的API

更好的方法是使text類更通用。它的API應該只根據基本的文本特性來定義,而不反映將用它實現的高級操作。例如,只需要兩個方法來修改文本:

oid insert(Position position, String newText);
void delete(Position start, Position end);

第一個方法在文本中的任意位置插入任意字符串,第二個方法刪除大於或等於開始但小於結束位置的所有字符。這個API還使用了一個更通用的類型Position而不是游標,它反映了一個特定的用戶界面。text類還應該提供一些通用的工具來處理文本中的位置,例如:

Position changePosition(Position position, int numChars);

此方法返回一個新位置,該位置距離給定位置有一定數量的字符。如果numChars參數為正,則新位置在文件中的時間晚於位置;如果數字是負數,則新位置在位置之前。該方法在必要時自動跳轉到下一行或上一行。使用這些方法,可以用以下代碼實現delete鍵(假設游標變量保存當前游標位置):

text.delete(cursor, text.changePosition(cursor, 1));

同樣,backspace鍵可以實現如下:

text.delete(text.changePosition(cursor, -1), cursor);

使用通用的文本API,實現用戶界面功能(如刪除和退格)的代碼比使用專門的文本API的原始方法要長一些。然而,新代碼比舊代碼更明顯。在用戶界面模塊中工作的開發人員可能關心backspace鍵刪除哪些字符。對於新代碼,這是顯而易見的。使用舊的代碼,開發人員必須轉到text類並閱讀backspace方法的文檔和/或代碼來驗證行為。此外,與專門化方法相比,通用方法總體上的代碼更少,因為它用更少的通用方法替換了文本類中大量的專用方法。

使用通用接口實現的文本類可以用於交互編輯器之外的其他用途。例如,假設您正在構建一個應用程序,該應用程序通過用另一個字符串替換所有特定字符串的出現來修改指定的文件。專門化文本類(如backspace和delete)中的方法對這個應用程序沒有什么價值。但是,通用文本類已經具備了新應用程序所需的大部分功能。唯一缺少的是一個方法來搜索下一個出現的給定字符串,如這個:

Position findNext(Position start, String string);

當然,交互式文本編輯器可能具有搜索和替換的機制,在這種情況下,text類已經包含此方法。

6.4 通用性使得信息隱藏效果更好

通用方法在文本和用戶接口類之間提供了更清晰的分離,從而實現更好的信息隱藏。文本類不需要知道用戶界面的細節,比如如何處理退格鍵;這些細節現在封裝在user interface類中。可以添加新的用戶界面特性,而無需在text類中創建新的支持函數。通用接口還減少了認知負擔:開發人員只需學習一些簡單的方法,這些方法可以用於各種目的。

text類的原始版本中的backspace方法是一個錯誤的抽象。它的目的是隱藏關於刪除哪些字符的信息,但用戶界面模塊確實需要知道這一點;用戶界面開發人員可能會閱讀backspace方法的代碼以確認它的准確行為。將這個方法放到text類中只會讓用戶界面開發人員更難獲得他們需要的信息。軟件設計最重要的元素之一是決定誰需要知道什么,什么時候需要知道。當細節很重要時,最好讓它們盡可能明確和明顯,比如backspace操作的修改實現。將這些信息隱藏在接口后面只會造成不透明性。

6.5 問自己的問題

識別干凈的通用類設計要比創建一個類容易。下面是一些您可以問自己的問題,這些問題將幫助您在接口的一般用途和特殊用途之間找到適當的平衡。

什么是最簡單的接口可以滿足我當前的所有需求?如果您減少了API中的方法數量,而沒有減少它的整體功能,那么您可能正在創建更通用的方法。 特殊用途的文本API至少有三種刪除文本的方法:backspace、delete和deleteSelection。更通用的API只有一個用於刪除文本的方法,這滿足了所有三個目的。只有在每個方法的API保持簡單的情況下,減少方法的數量才有意義;如果為了減少方法的數量,您不得不引入許多額外的參數,那么您可能並沒有真正地簡化事情。

在多少情況下會使用這種方法?如果一個方法是為一個特定的用途而設計的,比如backspace方法,那么這就是一個危險信號,因為它可能太特殊了。看看是否可以將幾個專用方法替換為一個通用方法。

這個API容易用於我當前的需求嗎?這個問題可以幫助您確定什么時候您在使API變得簡單和通用方面做得太過火了。如果您必須編寫大量額外的代碼來使用一個類來滿足當前的需要,那么接口沒有提供正確的功能就是一個危險的信號。例如,text類的一種方法是圍繞單字符操作進行設計:insert插入單個字符,delete刪除單個字符。這個API既簡單又通用。但是,對於文本編輯器來說,它並不特別容易使用:高級代碼將包含許多循環來插入或刪除字符范圍。對於大型操作,單字符方法的效率也很低。因此,文本類最好內置對字符范圍的操作的支持。

6.6 結論

與專用接口相比,通用接口有許多優點。它們往往更簡單,包含更少的方法。它們還提供了類之間更清晰的分離,而特殊用途的接口往往會泄漏類之間的信息。使您的模塊具有一定的通用功能是降低整個系統復雜性的最佳方法之一。


免責聲明!

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



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