如何設計優雅的類結構

TheAlchemist發表於2016-07-20

注:正文中的引用是直接引用作者作者的話,兩條橫線中間的段落的是我自己的觀點,其他大約都可以算是筆記了。

「Clean Code」這本書從這一章開始文風有些變化,感覺比較亂,很多概念在之前的章節也提到過,因為這本書的某些章節是不同的人編寫的,所以這種情況也難免,所以可能會有些小節我會幾句話簡單帶過。

本章講的是類的組織結構,其實很多這些概念我們在學校裡學習OOP時可能都有學到過,有些人可能會覺得講得比較虛,但文中確實有些細節還是解開了一些之前的疑惑,姑且當做複習物件導向的概念也好。

在前面的章節中詳細討論了命名、方法和資料結構等等這些概念,它們能夠幫助我們更好地理解在程式碼行或者程式碼塊的級別裡如何寫出簡潔優雅。在此基礎上,我們還是要在更高的層面上去探究程式碼簡潔之道。在現代的高階語言程式設計世界裡,類是系統的基本組成部分,這章就著重討論一下如何寫出好的類。

類的組織結構

對於類的程式碼結構,Java中有一套不成文的約定:

  • 一個類應該以一系列的常量和變數定義作為開始
  • 如果有公共靜態常量,它們應該放在最前邊
  • 接下來是私有的靜態常量
  • 接下來是私有的例項變數
  • 類中不應該有公共的變數
  • 緊接著是公共的方法
  • 一些私有的方法應該緊接著它被呼叫的共有方法後邊

封裝

在OOP中很多概念都是相通的,封裝作為OOP的一個基本概念突出了「開閉原則」的重要性,它很好地解決了一些擴充套件性的問題,它使得介面的提供者可以遮蔽此介面的具體實現,從而可以在一個較自由的範圍內去修改自己的實現。

按照封裝的概念,一個類中的所有變數都應該是私有的,但同時也不要對此概念太過執著,前邊的章節也提到過測試的重要性,有時候測試需要某些類的變數可訪問,那麼可以考慮給它們賦予protected屬性。但應該儘可能的保證封裝的特性。

類應該儘可能的「小」

在函式的那一章我們提到過方法應該設計的儘可能的小,我們衡量函式使用程式碼行數,在這裡我們衡量類使用「職責」。

一個類的職責應該是唯一的,這才符合OOP對現實世界的模擬的概念。職責往往是和程式碼的行數正相關,但它們並不是完全正相關的,如程式碼9-1中所示:

程式碼9-1
public class SuperDashboard extends JFrame implements MetaDataUser{
  public Component getLastFocusedComponent()
  public void setLastFocused(Component lastFocused)
  public int getMajorVersionNumber()
  public int getMinorVersionNumber()
  public int getBuildNumber()
}

這個類只包含了5個函式,那麼它是不是已經足夠小了呢?非也,它包含了兩個不同的職責——它同時管理「版本號」與「某個JFrame元件」。

單職責原則(SRP)

SRP的意思是說一個類(或者一個模組)應該有且只有一個要修改它的原因(職責)。

比如程式碼9-1中所示那樣,我們可能有兩個原因要去修改類SuperDashboard,一個是版本號改變了,另一個是獲取元件的方法變了。誠然,當我們修改了獲取lastFocus元件的方法時,往往是要修改版本號的,但是反過來就不一定了。

SRP是OOP中最重要的設計理念之一,但同時也是最常被違反的理念之一。「使軟體可以工作」和「使軟體簡潔優雅」是兩個截然不同的的工作,我們常常沒有時間也沒有精力同時關注這兩者,然後就只關注前者了。

在中國當下的現實環境是,很多程式碼的需求方(往往是老闆)根本不在乎程式碼的可維護性、可擴充套件性甚至健壯性,他們的要求常常是軟體快速上線,而不是簡潔優雅但要延期上線的軟體。此外,還有有很多程式碼寫完之後可能永遠也不會被維護和修改,所以「使軟體簡潔優雅」慢慢地塊要變成一種個人追求,變成了「大家都說這樣做更好,但真正這麼做的卻很少」。

我倒是覺得儘管在實際的程式設計工作中不得不不斷地進行妥協,但是隻要把clean code的理念放在心中,並用它來審視自己的程式碼,我們總是會寫出越來越好的程式碼。程式設計是如此,人生何嘗不是如此。

問題是很多人認為軟體「可以工作」的那一刻,我們的工作就結束了。我們接下來要做的是解決下一個問題而不是回過頭來把這些超級類分解成解耦的小單元。與此同時,還有很多人很害怕看到「大量的小而職責單一的類」,覺得那樣會使他們很難去從大方向上理解整個系統。事實則恰恰相反,大量的小的職責單一的解耦的類往往帶來更多的好處。

我們的目標是這樣的:我們的系統由大量的小的職責單一的類組成,而不是少數幾個超級大類。每一個類都只與少數幾個其他類進行互動(這點有點像迪米特法則)。

內聚

內聚的概念是這樣的:一個類應該只有少數的幾個例項變數,這個類的每個方法都應該操作這個類中的一個或多個例項變數。通常一個方法操作的例項變數越多,那麼這個方法對於這個類來說聚合性就越高。一個類中的所有方法都操作了這個類中的所有例項變數,那麼這個類就是聚合型最高的。

但是,通常來說這樣的超級內聚的類不太可能出現,也不建議去建立這樣的類。但我們還是想要一個類的內聚性是高的,這表明這個類中的幾個組成部分是互相依賴不可分割的。如程式碼9-2中所示的一個堆疊的簡單實現,就是一個內聚性很高的例子:

程式碼9-2
public class Stack {
    private int topOfStack = 0;
    List<Integer> elements = new LinkedList<Integer>();

    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()之外,其他幾個方法都同時使用到了這個類中的兩個變數。所以寫出高內聚的類的訣竅就是,保持類中的變數個數很少,方法很小。如果一個你程式碼中某個類的內聚性很低,那麼你就要考慮一下,是否要把它拆分成幾個更小的類了。

維護類的高內聚往往會帶來更小的類

只要你不斷的將大的方法拆分成小的方法,最直接的結果就是你會看到越來越多的類。

舉例來說,我們經常會碰到一個場景,我們想把一個超級方法中的某一個邏輯(可能是幾行程式碼)抽出來重構成為一個新的方法,然後抽取之後的新方法需要傳入4個在這個超級方法中定義的變數,這種情形下,最好就是把這4個變數程式設計類級別的變數,這樣我們抽取的這個新方法就不需要傳入任何的引數了。

但是,這樣做之後這個類的內聚性就降低了——這4個變數只在兩個方法中被呼叫——但是這種「有幾個方法想要分享幾個變數」的行為不正是類定義的由來嗎。所以,一旦你的類的內聚性降低時,就去著手把它拆分為更小的類吧。

所以,拆分類可以從拆分超級方法開始,這樣往往能給我們帶來一個更清晰的類的組織結構。

為了變化而設計

對於大多數的系統,變化是持續發生的。每次發生改變,都可能對我們的現有系統造成威脅,那麼我們設計系統中「類的組織結構」時就要儘可能降低這種風險。

然後在這個小節作者舉了個使用abstract類來解決對類的修改的問題。「對擴充套件開放,對修改關閉」最好的一個實現就是使用抽象類,因為對於此抽象概念增加時只需要多寫一個此抽象類的實現類,而不是去修改現有的實現類。

隔離變化

需求會不斷地變化,所以我們的程式碼實現也會不斷地變化。使用抽象類可以很大程度上地隔離這種變化。

這一小節的宗旨就是說要使用面向介面程式設計,使得此介面的呼叫者對介面依賴而不是對實現依賴,這樣就實現了「隔離變化」。

相關文章