類是否是越小越好?最近在讀John Ousterhout的《A Philosophy of Software Design》,感到做者文筆流暢,書中內容具備啓發性。這裏摘要一部份內容,以供開發工做中的參考、學習。html
本文連接:http://www.javashuo.com/article/p-yihkqyly-bq.html編程
轉載請註明緩存
在軟件複雜度的管理當中,最重要的技術之一是經過對系統的設計,使開發者任何在時候都只須要面對總體複雜度中的一小部分。這個過程被稱爲模塊化設計。模塊化
複雜度是什麼?在本文中,複雜度的定義是:和軟件系統結構有關的、會致使理解和修改系統變困難的東西。函數
在模塊設計中,軟件系統被分解爲相對獨立的模塊集合。模塊的形式多種多樣,能夠是類、子系統、或服務等。在理想的世界中,每一個模塊都徹底獨立於其它模塊:開發者在任何模塊中工做的時候,都不須要知道有關其它模塊的任何知識。在這種理想狀態下,系統複雜度取決於系統中複雜度最高的模塊。學習
固然,實踐與理想不一樣,系統模塊間總會多少有些依賴。當一個模塊變化時,其它模塊可能也須要隨之而改變。模塊化設計的目標就是最小化模塊間的依賴。spa
爲了管理依賴,咱們能夠把模塊當作兩部分:接口和實現。設計
接口包含了所有的在調用該模塊時須要的信息。接口只描述模塊作什麼,但不會包含怎麼作。code
完成接口所作出的承諾的代碼被稱爲實現。htm
在一個特定模塊內部進行工做的開發者必須知道的信息是:當前模塊的接口和實現+其它被該模塊使用的模塊的接口。他不須要理解其它模塊的實現。
在本文中,包含接口/實現的任何代碼單元,都是模塊。面嚮對象語言中的類是模塊,類中的方法也是模塊,非面嚮對象語言中的函數也是模塊。高層的子系統和服務也能夠被看做模塊,它們的接口也許是多種形式的,好比內核調用或HTTP請求。本文中的大部份內容針對的是類,但這些技術和理論對其它類型的模塊也有效。
好模塊的接口遠遠比實現更簡單。這樣的模塊有2個優勢。首先,簡單的接口最小化了模塊施加給系統其他部分的複雜度。其次,若是修改模塊時能夠不修改它的接口,那麼其餘模塊就不會被修改所影響。若是模塊的接口遠遠比實現簡單,那麼就更有可能在不改動接口的狀況對模塊進行修改。
接口中包含2種信息:正式的和非正式的。
正式的信息在代碼中被顯式指定,程序語言能夠檢查其中的部分正確性。好比,方法的簽名就是正式的信息,它包含參數的名稱和類型,返回值的類型,異常的信息。不少程序語言能夠保證代碼中對方法的調用提供了與方法定義相匹配的參數值。
接口裏面也包含非正式的元素。非正式部分沒法被程序語言理解或強制執行。接口的非正式部分包含一些高層行爲,好比函數會根據某個參數的內容刪除具備相應名字的文件。若是某個類的使用存在某種限制,好比方法的調用須要符合特定順序,那這也屬於接口的一部分。凡是開發者在使用模塊時須要瞭解的信息,均可以算做模塊接口的一部分。接口的非正式信息只能經過註釋等方式描述,程序語言沒法確保描述是完整而準確的。大部分接口的非正式信息都比正式信息要更多、更復雜。
清晰的接口定義有助於開發者瞭解在使用模塊時須要知道的信息,從而避免一些問題。
抽象這一術語和模塊設計思想的關係很近。抽象是實體的簡化視圖,省略了不重要的細節。抽象頗有用,它可使咱們對細節的思考和操縱變簡單。
在模塊化編程中,每一個模塊經過接口提供其抽象。抽象表明了函數功能的簡化視圖。在函數抽象的立場上,實現的細節是不重要的,因此它們被省略了。
「不重要」這個詞很關鍵。若是沒有忽略掉不重要的細節,那麼抽象會變得複雜,會增長開發者的認知負擔;若是忽略掉了重要的細節,那麼抽象會變得錯誤,失去對實踐的指導意義。設計抽象的關鍵是理解什麼是重要的,並尋找最小化重要信息的設計。
依賴抽象來管理複雜度不是編程的專利,它遍及在咱們的平常生活中。就像車子會提供一個簡單抽象來讓咱們駕駛,並不須要咱們理解發動機、電池、ABS之類的東西。
最好的模塊提供了強大的功能,又有着簡單的接口。術語「深」能夠用於描述這種模塊。爲了讓深度的概念可視化,試想每一個模塊由一個長方形表示,以下圖,
長方形的面積大小和模塊實現的功能多少成比例。頂部邊表明模塊的接口,邊的長度表明它的複雜度。最好的模塊是深的:他們有不少功能隱藏在簡單的接口後。深模塊是好的抽象,由於它只把本身內部的一小部分複雜度暴露給了用戶。
淺模塊的接口複雜,功能卻少,它沒有隱藏足夠的複雜度。
能夠從成本與收益的角度思考模塊深度。模塊提供的收益是它的功能。模塊的成本(從系統複雜度的角度考慮)是它的接口。接口表明了模塊施加給系統其他部分的複雜度。接口越小而簡單,它引入的複雜度就越少。好的模塊就是那些成本低收益高的模塊。
某些語言中的垃圾回收(GC)是深模塊的例子之一。這個模塊沒有接口,它在須要回收無用內存的場景下不可見地工做。在系統中加入垃圾回收的作法,縮小了系統的總接口,由於這種作法消除了用於釋放對象的接口。垃圾回收的具體實現是至關複雜的,但這一複雜度在實際使用程序語言的時候是隱藏的。
相對的,淺模塊就是接口相對功能而言很複雜的模塊。下面是個可能有些極端的例子,
private void addNullValueForAttribute(String attribute) { data.put(attribute, null); }
從複雜度管理的角度來看,該方法把事情變糟了。它沒有提供抽象,由於全部的功能都是在接口上可見的。思考這一接口並不會比思考它的完整實現更簡單。若是方法有合適的文檔,文檔也不會比方法的代碼具備更多信息。相比於直接操做data,它的長名字甚至會致使開發者敲擊鍵盤的次數變多。這種方法增長了複雜度(引入了一個須要開發者瞭解的新接口),但並無提供與之相應的收益。注意:小的模塊會更傾向於變淺。
當今,深模塊的價值並無被廣爲接受。通常常識是類須要小,而不是深。學生們被告知:類設計中最重要的事情是把大類拆分紅更小的類。類似的建議還包括:「要把方法行數大於N的方法分紅多個方法」,有時候N甚至只有10這麼小。這會致使大量的淺模塊,增長系統的總複雜度。
極端的「類應該小」的作法是一種綜合症的表現,這種症狀能夠被稱爲Classitis。它源於一種錯誤思惟:「類是好的,因此越多類越好」。這種思想最終會致使系統層面積累了巨大的複雜度,程序風格也會變得囉嗦。
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提供不一樣的構造器,或者經過一個方法禁用/替換緩存機制),這樣大部分開發者甚至不須要意識到它的存在。
經過將模塊的接口和實現分離,咱們能夠對系統的其它部分隱藏實現的複雜度。模塊的使用者只須要理解接口提供的抽象。在設計類和其它模塊時,最重要的問題是讓它們深,它們要對常見用例有足夠簡單的接口,但同時依然提供強大的功能。這就最大化地隱藏了複雜度。