在設計新模塊時,最多見的一個決定就是以通用方式仍是特殊方式實現它。有些人可能會說,應該採用通用的方法,即實現一種機制,用於解決普遍的問題,而不只僅是當前重要的問題。在這種狀況下,新機制可能會在將來發現意想不到的用途,從而節省時間。通用方法彷佛與第3章中討論的投資心態一致,即您預先花費更多的時間來節省之後的時間。編輯器
另外一方面,咱們知道很難預測軟件系統的將來需求,因此一個通用的解決方案可能包含一些實際上並不須要的設施。此外,若是您實現了一些過於通用的東西,那麼它可能沒法很好地解決您如今面臨的特定問題。所以,有些人可能會說,最好關注今天的需求,只構建您知道本身須要的東西,並根據您今天計劃使用它的方式進行專門化。若是採用特殊用途的方法,並在之後發現其餘用途,則始終能夠對其進行重構,使其成爲通用用途。專用方法彷佛與軟件開發的增量方法一致。函數
根據個人經驗,最好的方法是以某種通用的方式實現新模塊。短語「某種程度上通用的」意思是模塊的功能應該反映您當前的需求,可是它的接口不該該。相反,接口應該足夠通用,以支持多種用途。該接口應該易於使用,以知足今天的須要,而不是專門針對他們。「有些」這個詞很重要:不要得意忘形,不要構建一些通用的東西,由於它很難知足您當前的需求。工具
通用方法最重要的(可能也是最使人驚訝的)好處是,它比專用方法產生更簡單、更深刻的接口。若是您將該類用於其餘目的,那麼通用方法還能夠在未來爲您節省時間。然而,即便該模塊僅用於其原始目的,因爲其簡單性,通用目的的方法仍然更好。學習
讓咱們考慮一個來自軟件設計課程的例子,在這個課程中,學生被要求構建簡單的GUI文本編輯器。編輯器必須顯示一個文件,並容許用戶指向、單擊和鍵入來編輯文件。編輯器必須在不一樣的窗口中支持同一文件的多個同步視圖;他們還必須支持多級撤銷和重作文件的修改。spa
每一個學生項目都包含一個管理文件底層文本的類。文本類一般提供將文件加載到內存、讀取和修改文件文本以及將修改後的文本寫回文件的方法。設計
許多學生團隊爲text類實現了特殊用途的api。他們知道這個類將在交互式編輯器中使用,因此他們考慮了編輯器必須提供的特性,並根據這些特定的特性定製了文本類的API。例如,若是編輯器的用戶鍵入退格鍵,編輯器將當即刪除光標左邊的字符;若是用戶鍵入刪除鍵,編輯器將當即刪除光標右側的字符。瞭解了這一點,一些團隊在text類中建立了一個方法來支持這些特定的特性:code
void backspace(Cursor cursor); void delete(Cursor cursor);
這些方法中的每個都以光標的位置做爲參數;特殊類型遊標表示此位置。編輯器還必須支持能夠複製或刪除的選擇。學生們經過定義一個選擇類,並在刪除期間將這個類的對象傳遞給text類來處理這個問題:對象
void deleteSelection(Selection selection);
學生們可能認爲若是text類的方法對應於用戶可見的特性,那麼實現用戶界面會更容易。然而,在現實中,這種專門化對用戶界面代碼幾乎沒有什麼好處,並且它爲用戶界面或文本類的開發人員帶來了很高的認知負荷。text類以大量的淺層方法結束,每一個淺層方法只適合一個用戶界面操做。許多方法(如delete)只在一個地方調用。所以,開發用戶界面的開發人員必須瞭解文本類的大量方法。接口
這種方法在用戶界面和文本類之間形成了信息泄漏。與用戶界面相關的抽象,如選擇或退格鍵,反映在文本類中;這增長了開發人員處理文本類的認知負荷。每個新的用戶界面操做都須要在text類中定義一個新方法,所以處理用戶界面的開發人員可能最終也要處理text類。類設計的目標之一是容許獨立地開發每一個類,可是專門化的方法將用戶界面和文本類綁定在一塊兒。
更好的方法是使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類已經包含此方法。
通用方法在文本和用戶接口類之間提供了更清晰的分離,從而實現更好的信息隱藏。文本類不須要知道用戶界面的細節,好比如何處理退格鍵;這些細節如今封裝在user interface類中。能夠添加新的用戶界面特性,而無需在text類中建立新的支持函數。通用接口還減小了認知負擔:開發人員只需學習一些簡單的方法,這些方法能夠用於各類目的。
text類的原始版本中的backspace方法是一個錯誤的抽象。它的目的是隱藏關於刪除哪些字符的信息,但用戶界面模塊確實須要知道這一點;用戶界面開發人員可能會閱讀backspace方法的代碼以確認它的準確行爲。將這個方法放到text類中只會讓用戶界面開發人員更難得到他們須要的信息。軟件設計最重要的元素之一是決定誰須要知道什麼,何時須要知道。當細節很重要時,最好讓它們儘量明確和明顯,好比backspace操做的修改實現。將這些信息隱藏在接口後面只會形成不透明性。
識別乾淨的通用類設計要比建立一個類容易。下面是一些您能夠問本身的問題,這些問題將幫助您在接口的通常用途和特殊用途之間找到適當的平衡。
什麼是最簡單的接口能夠知足我當前的全部需求?若是您減小了API中的方法數量,而沒有減小它的總體功能,那麼您可能正在建立更通用的方法。 特殊用途的文本API至少有三種刪除文本的方法:backspace、delete和deleteSelection。更通用的API只有一個用於刪除文本的方法,這知足了全部三個目的。只有在每一個方法的API保持簡單的狀況下,減小方法的數量纔有意義;若是爲了減小方法的數量,您不得不引入許多額外的參數,那麼您可能並無真正地簡化事情。
在多少狀況下會使用這種方法?若是一個方法是爲一個特定的用途而設計的,好比backspace方法,那麼這就是一個危險信號,由於它可能太特殊了。看看是否能夠將幾個專用方法替換爲一個通用方法。
這個API容易用於我當前的需求嗎?這個問題能夠幫助您肯定何時您在使API變得簡單和通用方面作得太過火了。若是您必須編寫大量額外的代碼來使用一個類來知足當前的須要,那麼接口沒有提供正確的功能就是一個危險的信號。例如,text類的一種方法是圍繞單字符操做進行設計:insert插入單個字符,delete刪除單個字符。這個API既簡單又通用。可是,對於文本編輯器來講,它並不特別容易使用:高級代碼將包含許多循環來插入或刪除字符範圍。對於大型操做,單字符方法的效率也很低。所以,文本類最好內置對字符範圍的操做的支持。
與專用接口相比,通用接口有許多優勢。它們每每更簡單,包含更少的方法。它們還提供了類之間更清晰的分離,而特殊用途的接口每每會泄漏類之間的信息。使您的模塊具備必定的通用功能是下降整個系統複雜性的最佳方法之一。