軟體設計之Deep Module(深模組)
類是不是越小越好?最近在讀John Ousterhout的《 A Philosophy of Software Design 》,感到作者文筆流暢,書中內容具有啟發性。這裡摘要一部分內容,以供參考、學習。
本文連結: https://www.cnblogs.com/hhelibeb/p/10708951.html
轉載請註明
在軟體複雜度的管理當中,最重要的技術之一是通過對系統的設計,使開發者任何在時候都只需要面對整個複雜度中的一小部分。這個過程被稱為 模組化設計 。
1,模組化設計
在模組設計中,軟體系統被分解為相對獨立的模組集合。模組的形式多種多樣,可以是類、子系統、或服務等。在理想的世界中,每個模組都完全獨立於其它模組:開發者在任何模組中工作的時候,都不需要知道有關其它模組的任何知識。在這種理想狀態下,系統複雜度取決於系統中複雜度最高的模組。
當然,實踐與理想不同,系統模組間總會多少有些依賴。當一個模組變化時,其它模組可能也需要隨之而改變。模組化設計的目標就是最小化模組間的依賴。
為了管理依賴,我們可以把模組看成兩部分: 介面 和 實現 。
介面包含了全部的在呼叫該模組時需要的資訊。介面只描述模組 做什麼 ,但不會包含 怎麼做 。
完成介面做出的承諾的程式碼被稱為 實現 。
在一個特定模組內部進行工作的開發者必須知道的資訊是:當前模組的介面和實現+其它被該模組使用的模組的介面。他不需要理解其它模組的實現。
在本文中,包含介面/實現的任何程式碼單元,都是模組。面嚮物件語言中的類是模組,類中的方法也是模組,非面嚮物件語言中的函式也是模組。高層子系統和服務也可以被看作模組,它們的介面也許是多種形式的,比如核心呼叫或HTTP請求。本文中的大部分內容針對的是類,但這些技術和理論對其它型別的模組也有效。
好模組的 介面遠遠比實現更簡單 。這樣的模組有2個優點。首先,簡單的介面最小化了模組施加給系統其餘部分的複雜度。其次,如果修改模組時可以不修改它的介面,那麼其他模組就不會被修改所影響。如果模組的介面遠遠比實現簡單,那麼就更有可能在不改動介面的情況對模組進行修改。
2,接口裡有什麼
介面中包含2種資訊:正式的和非正式的。
正式的資訊在程式碼中被顯式指定,程式語言可以檢查其中的部分正確性。比如,方法的簽名就是正式的資訊,它包含引數的名稱和型別,返回值的型別,異常的資訊。很多程式語言可以保證程式碼中對方法的呼叫提供了與方法定義相匹配的引數值。
接口裡面也包含非正式的元素。非正式部分無法被程式語言理解或強制執行。介面的非正式部分包含一些高層行為,比如函式會根據某個引數的內容刪除具有相應名字的檔案。如果某個類的使用存在某種限制,比如方法的呼叫需要符合特定順序,那這也屬於介面的一部分。凡是開發者在使用模組時需要了解的資訊,都可以算作模組介面的一部分。介面的非正式資訊只能通過註釋等方式描述,程式語言無法確保描述是完整而準確的。大部分介面的非正式資訊都比正式資訊要更多、更復雜。
清晰的介面定義有助於開發者瞭解在使用模組時需要知道的資訊,從而避免一些問題。
3,抽象
抽象這一術語和模組設計思想的關係很近。 抽象是實體的簡化檢視,省略了不重要的細節。 抽象很有用,它可以使對細節的思考和操縱變簡單。
在模組化程式設計中,每個模組通過介面提供其抽象。抽象代表了函式功能的簡化檢視。在函式抽象的立場上,實現的細節是不重要的,所以它們被省略了。
“不重要”這個詞很關鍵。如果沒有忽略掉不重要的細節,那麼抽象會變得複雜,會增加開發者的認知負擔;如果忽略掉了重要的細節,那麼抽象會變得 錯誤 ,失去對實踐的指導意義。設計抽象的關鍵是理解什麼是重要的,並尋找最小化重要資訊的設計。
依賴抽象來管理複雜度不是程式設計的專利,它遍佈在我們的日常生活中。就像車子會提供一個簡單抽象來讓我們駕駛,並不需要我們理解發動機、電池、ABS之類的東西。
4,深模組
最好的模組提供了強大的功能,又有著簡單的介面。術語“ 深 ”可以用於描述這種模組。為了讓深度的概念視覺化,試想每個模組由一個長方形表示,如下圖,
長方形的面積大小和模組實現的功能多少成比例。頂部邊代表模組的介面,邊的長度代表它的複雜度。最好的模組是深的:他們有很多功能隱藏在簡單的介面後。深模組是好的抽象,因為它只把自己內部的一小部分複雜度暴露給了使用者。
淺模組的介面複雜,功能卻少,它沒有隱藏足夠的複雜度。
可以從成本與收益的角度思考模組深度。模組提供的收益是它的功能。模組的成本(從系統複雜度的角度考慮)是它的介面。介面代表了模組施加給系統其餘部分的複雜度。介面越小而簡單,它引入的複雜度就越少。 好的模組就是那些成本低收益高的模組 。
某些語言中的垃圾回收(GC)是深模組的例子之一。這個模組沒有介面,它在需要回收無用記憶體的場景下不可見地工作。在系統中加入垃圾回收縮小了系統的總介面,因為這種做法消除了用於釋放物件的介面。垃圾回收的具體實現是相當複雜的,但這一複雜度在實際使用程式語言的時候被隱藏了。
5,淺模組
相對的,淺模組就是介面相對功能而言很複雜的模組。下面是個可能有些極端的例子,
private void addNullValueForAttribute(String attribute) { data.put(attribute, null); }
從複雜度管理的角度來看,該方法把事情變糟了。它沒有提供抽象,因為所有的功能都是在介面上可見的。思考這一介面並不會比思考它的完整實現更簡單。如果方法有合適的文件,文件也不會比方法的程式碼具有更多資訊。相比於直接操作data,它的長名字甚至會導致開發者敲擊鍵盤的次數變多。這種方法增加了複雜度(引入了一個需要開發者瞭解的新介面),但並沒有提供與之相應的收益。注意:小的模組會更傾向於變淺。
6,Classitis
當今,深模組的價值並沒有被廣為接受。一般常識是 類需要小 ,而不是深。學生們被告知:類設計中最重要的事情是把大類拆分成更小的類。相似的建議還包括:“要把方法行數大於N的方法分成多個方法”,有時候N甚至可以像10這麼小。這會導致大量的淺模組,增加系統的總複雜度。
把極端的“類應該小”是一種綜合症,可以被稱為Classitis。它源於一種錯誤思維:“類是好的,所以越多類越好”。這種思想最終會導致系統層面積累了巨大的複雜度,程式風格也會變得囉嗦。
7,例子
Java類庫可能是Classitis的最明顯例子之一。Java語言本身不需要很多小類,但Classitis文化可能已經在Java語言社群紮了根。比如,為了開啟檔案讀取其中的序列化物件,你必須建立多種物件:
FileInputStream fileStream = new FileInputStream(fileName); BufferedInputStream bufferedStream = new BufferedInputStream(fileStream); ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
FileInputStream物件只提供初步的I/O,它不具備快取I/O的能力,也不能讀寫序列化物件。BufferedInputStream和ObjectInputStream分別提供了後面兩項功能。檔案開啟之後,fileStream和bufferedStream就沒用了,未來的操作只會用到objectStream.。
必須顯式單獨建立BufferedInputStream物件來請求快取,這很煩人而且易出錯。如果開發者忘記建立它,就不會有快取,而且I/O會慢。大概Java開發者會辯解說,不是所有人都需要快取,所以它不應該包含在基本讀寫機制中。他們也許會說讓快取獨立更好,藉此使用者可以選擇是否使用它。提供選擇空間當然很好, 但介面需要設計為對常用場景儘可能簡單 ,幾乎所有檔案I/O使用者都想使用快取,所以就應該預設提供它。對於少數不需要的情況,庫可以提供機制以禁用。禁用快取的機制應該明確地在介面中分離(例如,為FileInputStream提供不同的構造器,或者通過一個方法禁用/替換快取機制),這樣大部分開發者甚至不需要意識到它的存在。
8,結論
通過將模組的介面和實現分離,我們可以對系統的其它部分隱藏實現的複雜度。模組的使用者只需要理解介面提供的抽象。在設計類和其它模組時,最重要的問題是讓它們深,它們要對常見用例有足夠簡單的介面,但還是提供強大的功能。這就最大化地隱藏了複雜度。