這篇文章將與大家一起聊一聊,書寫整潔類的一些法則。 一、引言 以下引言的內容,有必要伴隨這個系列的每一次更新,這次也不例外。 《代碼整潔之道》這本書提出了一個觀點:代碼質量與其整潔度成正比,干凈的代碼,既在質量上可靠,也為后期維護、升級奠定了良好基礎。書中介紹的規則均來自作者多年的實踐經驗,涵蓋從命名到重構的多個編程方面,雖為一“家”之言,然誠有可資借鑒的價值。 但我們知道,很多時候,理想很豐滿,現實很骨感,也知道人在江湖,身不由己。因為項目的緊迫性,需求的多樣性,我們無法時時刻刻都寫出整潔的代碼,保持自己輸出的都是高質量、優雅的代碼。 但若我們理解了代碼整潔之道的精髓,我們會知道怎樣讓自己的代碼更加優雅、整潔、易讀、易擴展,知道真正整潔的代碼應該是怎么樣的,也許就會漸漸養成持續輸出整潔代碼的習慣。 而且或許你會發現,若你一直保持輸出整潔代碼的習慣,長期來看,會讓你的整體效率和代碼質量大大提升。 二、本文涉及知識點思維導圖 國際慣例,先放出這篇文章所涉及內容知識點的一張思維導圖,就開始正文。大家若是疲于閱讀文章正文,直接看這張圖,也是可以Get到本文的主要知識點的大概。 三、整潔類的書寫準則 1 合理地分布類中的代碼 一般情況下,我們遵循變量列表在前,函數在后的原則。 類應該從一組變量列表開始。若有公有靜態常量,應該最先出現,然后是私有靜態變量,以及公有變量,私有變量。盡可能少的出現公有變量。 公共函數應該出現在變量列表之后。我們喜歡把由某個公共函數調用的私有工具函數緊跟在公共函數后面。 這樣是符合自定向下的原則,讓程序讀起來像一篇報紙文章。 2 盡可能保持類的封裝 我們喜歡保持變量和工具函數的私有性,但不執著于此。有時,我們需要用到protected變量或者工具,比如讓測試可以訪問到。然而,我們會盡可能使函數或變量保持私有,不對外暴露太多細節。放松封裝,總是下策。 3 類應該短小 正如之前關于函數書寫的論調。類的一條規則是短小,第二條規則還是要短小。 和函數一樣,馬上有個問題要出現,那就是,多小合適呢? 對于函數,我們通過計算代碼行數來衡量大小,對于類,我們采用不同的衡量方法,那就是權責(responsibility)。 3.1 單一權責原則 單一權責(Single Responsibility Principle,SRP)認為,類或模塊應有且只有一條加以修改的理由。 舉個栗子,下面這個類足夠短小了嗎? [cpp] view plain copy print? public class SuperDashboard extends JFrameimplements MetaDataUser { public Component getLastFocusedComponent() public void setLastFocused(Component lastFocused) public int getMajorVersionNumber() public int getMinorVersionNumber() public int getBuildNumber() } 答案是否定的,這個類不夠“短小”。5個方法不算多,但是這個類雖方法少,但還是擁有太多權責。這個貌似很小的SuperDashboard類,卻有兩條關聯度并不大的加以修改的理由: 第一, 它跟蹤會隨著軟件每次發布而更新的版本信息(含有getMajorVersionNumber等方法)。 第二,它還在管理組件(含有getLastFocusedComponent方法)。 其實,鑒別權責(修改的理由)常常幫助我們在代碼中認識到并創建出更好的抽象。 我們可以輕易地將SuperDashboard拆解成名為Version的類中,而這個名為Version的類,極可能在其他應用程序中得到復用: [cpp] view plain copy print? public class Version { public int getMajorVersionNumber() public int getMinorVersionNumber() public int getBuildNumber() } 這樣,這個類就大致做到了單一權責。 4 合理提高類的內聚性 我們希望類的內聚性保持在較高的水平。 何為類的內聚性?類的內聚性就是類中變量與方法之間的依賴關系。類中方法操作的變量越多,就越黏聚到類上,就代表類的內聚性高。 類應該只有少量的實體變量,類中的每個方法都應該操作一個或者多個這種變量。通常而言,如果一個類中的每個變量都被每個方法所使用,則該類具有最大的內聚性。一般來說,創建這種極大化的內聚類不可取,也不可能。 我們只希望內聚性保持在較高的水平。內聚性高,表示類中方法和變量相互依賴,相互結合成一個邏輯整體。 舉個高內聚的例子: [cpp] view plain copy print? public class Stack { private int topOfStack = 0; List elements = new LinkedList(); public int size() { return topOfStack; } public void push(int element) { topOfStack++; elements.add(element); } public int pop() throws PoppedWhenEmpty { if (topOfStack == 0) throw new PoppedWhenEmpty(); int element = elements.get(--topOfStack); elements.remove(topOfStack); return element; } } 這個類非常內聚,在三個方法中,僅有size()方法沒有使用所有的兩個變量。 注意,保持函數和參數短小的策略,有時候會導致為一組子集方法所用的實體變量增加。我們應該嘗試將這些方法拆分到兩個或者多個類中,讓新的類更為內聚。 5 有效地隔離修改 需求會改變,所以代碼也會改變。在面向對象入門知識中我們學習到,具體類包含實現細節(代碼),而抽象類則呈現概念。依賴于具體細節的客戶類,當細節改變時,就會有風險。我們可以借助接口和抽象類來隔離這些細節帶來的影響。 舉個栗子,在一個設計場景下,我們以其設計直接依賴于TokyoStockExchange的Protfolio類,不如創建StockExchange接口,里面只聲明一個方法: [cpp] view plain copy print? public interface StockExchange { MoneycurrentPrice(String symbol); } 接著設計TokyoStockExchange類來實現這個接口: [cpp] view plain copy print? public class TokyoStockExchange extends StockExchange { //… } 我們還要確保Portfolio的構造器接受作為參數StickExchange引用: [cpp] view plain copy print? public Portfolio { private StockExchange exchange; public Portfolio(StockExchange exchange) { this.exchange = exchange; } // ... } 那么現在就可以為StockExchange接口創建可以測試的實現了,例如返回固定的股票現值。比如測試購買5股微軟股票,我們下面的實現代碼返回100美元的現值,然后再實現一個總投資價值為500美元的測試,那么大概代碼則是: [cpp] view plain copy print? public class PortfolioTest { privateFixedStockExchangeStub exchange; privatePortfolio portfolio; @Before protected void setUp() throws Exception { exchange = new FixedStockExchangeStub(); exchange.fix("MSFT", 100); portfolio = new Portfolio(exchange); } @Test public void GivenFiveMSFTTotalShouldBe500() throws Exception { portfolio.add(5, "MSFT"); Assert.assertEquals(500,portfolio.value()); } } 如果系統解耦到足以這樣測試的程度,也就更加靈活,更加可復用。部件之間的解耦代表著系統中的元素相互隔離得很好。隔離也讓對系統每個元素的理解變得更加容易。 我們的Portfolio類不再是依賴于TokyoStockExchange類的實現細節,而是依賴于StockExchange接口這個抽象的概念,這樣就隔離了特定的細節。而其實我們的類就遵循了另一條類的設計原則,依賴倒置原則(Dependency Inversion Principle , DIP),因為依賴倒置原則的本質,實際上就是認為類應該依賴于抽象,而不是依賴于具體細節。 四、一些思考與總結 讓軟件能夠保持工作和讓軟件代碼整潔,是兩種截然不同的工作。我們中大多數人腦力有限,只能更多把更多精力放在讓代碼能夠工作上,而不是放在保持代碼有組織和整潔上。 問題是太多人在程序能夠正常工作時就以為萬事大吉了。我們沒能把思維轉向有關代碼組織和整潔的部分,我們只是一直在做新的需求,而不是回頭將臃腫的類切分為只有單一權責的去耦式單元。 與此同時,許多開發者害怕數量巨大的短小單一目的的類會導致難以一目了然抓住全局。他們認為,要搞清楚一件較大的工作如果完成,就得在類與類之間找來找去。其實,有大量短小的類的系統并不比有少量龐大類的系統更難掌控。問題是:你是想把工具歸置于有許多抽屜、每個抽屜中裝有定義和標記的良好組件的工具箱中呢,還是想要少數幾個能隨便把所有東西都扔進去的抽屜呢?大概我們都更趨向于選擇前者。 每個達到一定規模的系統都包含大量邏輯和復雜性。管理這種復雜性的首要目標就是加以組織,以便開發者能知道在哪里找到需要的內容,專注于當下工作直接相關的具體模塊。反之,擁有巨大、多目的類的系統,總是讓我們在目前并不需要了解的一大堆東西中艱難跋涉。 最終再強調一下:系統應該由許多短小的類而不是少量巨大的類組成。每個小類封裝一個權責,只有一個修改的原因,并與少數其他類一起協同達成期望的系統行為。 五、本文涉及知識點提煉整理 原則一:合理地分布類中的代碼。 類中代碼的分布順序大致是: 1. 公有靜態常量 2. 私有靜態變量 3. 公有普通變量 4. 私有普通變量 5. 公共函數 6. 私有函數 原則二:盡可能地保持類的封裝。盡可能使函數或變量保持私有,不對外暴露太多細節。 原則三:類應該短小,盡量保持單一權責原則。類或模塊應有且只有一條加以修改的理由。 原則四:合理提高類的內聚性。我們希望類的內聚性保持在較高的水平。內聚性高,表示類中方法和變量相互依賴,相互結合成一個邏輯整體。 原則五:有效地隔離修改。類應該依賴于抽象,而不是依賴于具體細節。盡量對設計解耦,做好系統中的元素的相互隔離,做到更加靈活與可復用。 本文就此結束。 |