九步邁向優秀軟件設計

優秀設計背後的核心概念其實並不高深。好比內聚性、鬆耦合、零重複、封裝、可測試性、可讀性以及單一職責。這七條評判代碼質量的原則已經被普遍接受了。然而真正困難的是如何把這些概念付諸實踐。理解力封裝就是隱藏「數據、實現細節、類型、設計或者構造」,這只是設計出良好封裝的代碼的第一步而已。所以,本文接下來是一系列實踐和規則練習,它能夠幫助你將良好的面向對象設計原則變得更加具體,從而在現實世界中應用那些原則。 java

規則 程序員

1.方法只使用以及縮進。 算法

2.拒絕使用else關鍵字。 編程

3.封裝全部的原生類型和字符串。 設計模式

4.一行代碼只有一個「.」 運算符。 app

5.不要使用縮寫。 編程語言

6.保持實體對象簡單清晰。 函數

7.任何類中的實例變量都不要超過兩個。 工具

8.使用一流的集合。 測試

9.不適用任何Getter/Setter/Property。

規則1:方法只使用以及縮進

你是否曾經盯着一個體型巨大的老方法而感到無從下手過。龐大的方法一般缺乏內聚性。一個常見的原則是將方法的行數控制在5行以內,可是若是你的方法已是一個500行的大怪獸了,想要達到這一原則的要求是很是痛苦的。其實,你不妨嘗試讓每一個方法只作一件事——每一個方法只包含一個控制結構或者一個代碼塊。若是你在一個方法中嵌套了多層控制結構,那麼你就要處理多個層次上的抽象,這意味着同事作多件事。

若是每一個方法都只關注一件事,而它們所在的類也只作一件事,那麼你的代碼就開始變化了。因爲應用程序中的每一個單元都變得更小了,代碼的可重用性開始指數增加。一個100行的,肩負五種不一樣職責的方法很難被重用。若是一個很短的方法在設置了上下文後,可以管理一個對象的狀態,那麼它能夠應用在不少不一樣的上下文中。

利用IDE提供的「抽取方法」功能,不斷地抽取方法中的行爲,直到它只有一級縮進爲止。請看下面的實例。

class Board{
      ...
      String board(){
             StringBuffer buf = new StringBuffer();
             for(int i=0; i < 10; i++){
                    for(int j=0; j < 10; j++){
                         buf.append(data[i][j]);
                         buf.append("\n");
                    }             
             } 
             return buf.toString();
      }
}
class Board
{
   ...
   String board(){
         StringBuffer buf = new StringBuffer();
         collectRows(buf);
         Return buf.toString();
   }
   void collectRows(StringBuffer buf){
         for(int i=0; i<10; i++){
             collectRow(buf,i);
         }
   }
   void collectRow(StringBuffer buf, int row){
        for(int i=0; i<10; i++){
            buf.append(data[row][i]);
        }
        buf.append("\n");
   }
}
注意這項重構還能帶來另外一種效果:每一個單獨的方法都變得更簡單了,同時其實現也與其名稱更加匹配。在這樣短小的代碼段中查找bug一般會更加容易。


第一條規則在此接近尾聲了。我還要強調,你越多實踐這條規則,就會越多的嚐到它帶來的甜頭。當你第一次嘗試解決前面展現的那一類問題時,可能不是很是的熟練,也未必能得到不少收穫。可是,應用這些規則的技能是一種藝術,它能將程序提高到一個新的高度。

規則2:拒絕else關鍵字

每一個程序員都熟知if/else結構。幾乎每種語言都支持if/else。簡單的條件判斷對任何人來講都不難理解。不過大多數程序員也見識過使人眩暈的層層嵌套的條件判斷,或者連綿數頁的case語句。更糟糕的是,在現有的判斷條件上加一個新的分支一般是很是容易的,而將它重構爲一個更好的方式的想法卻罕有人去說起。條件判斷結構一般仍是重複代碼的來源。例如,狀態標識常常會帶來這樣的問題。

public static void endMe(){
  if(status == DONE){
       doSomething();
  }else{
       <other code>
  }
}
你有不少種方式重寫這段代碼,去掉else關鍵字。例以下面的代碼。
public static void endMe(){
  if(status == DONE){
     doSomething();
     return;
  }
  <other code>
}

public static Node head(){
  if(isAdvancing()){
     return first;
   }else{
     return last;
   }
}
public static Node head(){
  return isAdvancing()?first:last;
}
在上面的例子中,第二段代碼因爲使用了三元運算符,因此代碼長度從四行壓縮到了一行。須要當心的是,若是過分使用「提早返回」,代碼的清晰度很快會下降《設計模式中》一書中關於策略模式的部分裏有一個實例,演示瞭如何使用多態避免根據狀態進行分支選擇的代碼。若是這種根據狀態進行分支選擇的代碼大量地重複,就應該考慮使用策略模式了。

面向對象編程語言給咱們提供了一種更爲強大的工具——多態。它可以處理更爲複雜的條件判斷。對於簡單的條件判斷,咱們可使用「衛語句」和「提早返回」替換它。而基於多態的設計則更容易閱讀與維護,從而能夠更清晰的表達代碼的內在乎圖。可是,程序員要作出這樣的轉變並非一路順風的.尤爲是你的代碼中可能早已充斥了else。因此,做爲這個練習的一部分,你是不可使用else的。在某些場景下可使用Null Object模式,它會對你有所幫助。另外還有不少工具和技術均可以幫助你甩掉else。試一試,看你能提出多少種方案來?

規則3:封裝全部的原生類型和字符串

整數自身只表明一個數量,沒有任何含義。當方法的參數是整數時,咱們就必須在方法中描述清楚參數的意思。若是此方法使用「Hour」做爲參數,就可以讓程序員更容易地理解它的含義了。像這樣的小對象能夠提升程序的可維護性,由於你不可能給一個參數爲「Hour」的方法傳一個「Year」。若是使用原生變量,編譯器不能幫助你編寫語義正確的程序。若是使用對象,哪怕是很小的對象,它都能給編譯器和其餘程序員提供更多的信息——這個值是什麼,爲何使用它。

像Hour或Money這樣的小對象還提供了放置一類行爲的場所,這些行爲放在其餘的類中都不合適。在你瞭解了關於getter和setter的規則時,這一點會很是明顯,有些值只能被這些小對象來訪問。

規則4:一行代碼中只有一個「.」運算符

有時候咱們很難判斷出一個行爲的職責應該由哪一個對象來承擔。若是你看一看那些包含了多個「.」的代碼,就會從中發現不少沒有被正確放置的職責。若是代碼中每一行都有多個「.」,那麼這個行爲就發生在錯誤的位置了。也許你的對象須要同時與另外兩個對象打交道。在這種狀況下,你的對象只是一箇中間人;它知道太多關於其餘對象的事情了。這時能夠考慮把該行爲移到其它對象之中。

若是這些「.」都是彼此聯繫的,你的對象就已經深深的陷入到另外一個對象之中了。這些過量的「.」說明你破壞了封裝性。嘗試着讓對象爲你作一些事情,而不要窺視對象內部的細節。封裝的主要含義就是,不要讓類的邊界跨入到它不該該知道的類型中。

迪米特法則(The Law of Demeter,「只和身邊的朋友交流」)是一個很好的起點。還能夠這樣思考它:你能夠玩本身的玩具,能夠玩你製造的玩具,還有別人送給你的玩具。可是永遠不要碰別人的玩具。

class Board
{
  ...
  class Piece
  {
     ...
     String representation;
  }
  class Location
  {
     ...
     Piece current;
  }
  String boardRepresentation(){
     StringBuffer buf = new StringBuffer();
     for(Location l: squares()){
         buf.append(l.current.representation.substring(0,1));
     }
     return buf.toString();
  }
}

class Board
{
   ...
   class Piece
   {
      ...
      private String representation;
      String character(){
            return representaion.substring(0,1);
      }
      void addTo(StringBuffer buf){
          buf.append(character());
      }
   }
   class Location
   {
       ...
       private Piece current;
       void addTo(StringBuffer buf){
            current.addTo(buf);
       }
   }
   String boardRepresentation(){
       StringBuffer buf = new StringBuffer();
       for(Location l: squares()){
           l.addTo(buf);
       }
       return buf.toString();
   }
}

注意在這個例子中,算法的實現細節被過分的擴散開了。程序員很難看一眼就理解它。可是在爲Piece轉化成character的行爲建立一個具備名稱的方法後,這個方法的名稱和做用就至關一致了,並且被重用的機會也會很是高——使人費解的representation.substring(0,1)調用能夠所有被這個具備名稱的方法所代替,程序的可讀性又邁進了一大步。在這片新天地裏,方法名取代了註釋,因此,值得花些時間爲方法取一個有意義的名字。理解並寫出這種結構的程序並不困難,你只需使用一些稍微不一樣的手段而已。

規則5:不要使用縮寫

咱們總會不自覺地在類名、方法名或者變量名中使用縮寫。請抵制住這個誘惑。縮寫會使人迷惑,也容易隱藏一些更嚴重的問題。

想想你爲何要使用縮寫。由於你厭倦了一遍又一遍的敲打相同的單詞?若是是這種狀況,也許你的方法調用的過於頻繁,你是否是應該停下來消除一些重複了?由於方法的名字太長?這可能意味着有些職責沒有放在正確的位置或者是有缺失的類。

儘可能保持類名和方法命中只包含一到兩個單詞,避免在名字中重複上下文的信息。好比某個類是Order,那麼方法名就沒必要叫作shipOrder()了,把它簡化爲ship(),客戶端就會調用order.ship()——這可以簡單明瞭的說明代碼的意圖。

在這個練習中,全部實體對象的名稱都只能包含一到兩個單詞,不能使用縮寫。

規則6:保持實體對象簡單清晰

這意味着每一個類的長度都不能超過50行,每一個包所包含的文件不超過10個。

代碼超過50行的類所作的事情一般都不止一件,這會致使它們難以被理解和重用。小於50行代碼的類還有一個妙處:它能夠在一個屏幕內顯示,不須要滾屏,這樣程序員能夠很容易、很快熟悉這個類。

建立這樣小的類會遇到哪些挑戰呢?一般會有不少成組的行爲,它們邏輯上是應該在一塊兒的。這時就須要使用包機制來平衡。隨着類變得愈來愈小,職責愈來愈少,加之包的大小也受到限制,你會逐漸注意到,包中的類愈來愈集中,它們可以協做完成一個相同的目標。包和類同樣,也應該是內聚的,有一個明確的意圖。保證這些包足夠小,就能讓它們有一個真正的標識。

規則7:任何類中的實例變量都不要超過兩個

大多數的類應該只負責處理單一的狀態變量,有些時候也能夠擁有兩個狀態變量。每當爲類添加一個實例變量,就會當即下降類的內聚性。通常而言,編程時若是遵照這些規則,你會發現只有兩種類,一種類負責維護一個實例變量的狀態;另外一種類負責協調兩個獨立的變量。不要讓這兩種職責同時出如今一個類中。

敏銳的讀者可能已經注意到了,規則3和規則7實際上是相同問題的不一樣表述而已。在一般狀況下,對於一個包含不少實例變量的類來講,很難擁有一個內聚的、單一的職責描述。

咱們來仔細分析下面的示例。

class Name                                                                                      {                                                                                                 String first;
  String middle;
  String last;
}
這個類能夠被拆分爲兩個類
class Name
{
  Surname family;
  GivenName given;
}
class Surname
{
   String family;
}
class GivenName
{
   List<String> names;
}

注意思考這裏是如何分離概念的,其中姓氏(family name)是一個關注點(不少法律實體約束中須要用到),它能夠和其餘與其有本質區別的名字分開。GivenName對象包含了一個名字的列表。在新的模型中,名稱容許包含first,middle和其餘名字。一般,對實例變量解耦之後,會加深理解各個相關的實例變量之間的共性。有時,幾個相關的實例變量在一流的集合中會相互關聯。

將一個對象從擁有大量屬性的狀態,解構成爲分層次的、相互關聯的多個對象,會直接產生一個更實用的對象模型。在想到這條規則以前,我曾經浪費過不少時間去追蹤那些大型對象的數據流。雖然咱們能夠理清一個複雜的對象模型,可是理解各組相關的行爲並看到結果是一個很是痛苦的過程。相比而言,不斷應用這條規則,能夠快速將一個複雜的大對象分解成爲大量簡單的小對象。行爲也天然而然地隨着各個實例變量流入到了適當的地方——不然編譯器和封裝法則都不高興的。當你真正開始作的時候,能夠沿着兩個方向進行:其一,能夠將對象的實例變量按照相關性分離在兩個部分中;另外也能夠建立一個新的對象來封裝兩個已有的實例變量。

規則8:使用一流的集合

應用這條規則的方法很是簡單:任何包含集合的類都不能再包含其餘的成員變量。每一個集合都被封裝在本身的類中,這樣,與集合相關的行爲就有了本身的家。你可能會發現做用於這些集合的過濾器將成爲這些新類型中的一部分。或是根據它們自身的狀況包裝爲函數對象。另外這些新的類型還能夠處理其餘任務,好比將兩個集合中的元素拼裝到一塊兒,或者對集合中的元素逐一施加某種規則等。很明顯,這條規則是對前面關於實例變量規則的擴展,不過它自身也有很是重要的意義。集合實際上是一種應用普遍的原生類型。它具備不少行爲,可是對於代碼的讀者和維護者來講,與集合相關的代碼一般都缺乏對語義意圖的解釋。

規則9:不適用任何Getter/Setter/Property

上一條規則的最後一句話幾乎能夠直接通向這條規則。若是你的對象已經封裝了相應的實例變量,可是設計仍然很糟糕的話,那就應該仔細的考察一下其餘對封裝更直接的破壞了。若是能夠從對象以外隨便詢問實例變量的值,那麼行爲與數據就不可能封裝到一處。在嚴格封裝邊界背後,真正的動機是迫使程序員在完成編碼以後,必定要爲這段代碼的行爲找到一個合適的位置,確保它在對象模型中的惟一性。這樣作會有不少好處,好比合一很大程度的減小重複性的錯誤;另外,在實現新特性的時候,也有一個更合適的位置去引入變化。

這條規則一般被描述爲「講述而不要詢問」(「Tell, don't ask」)。

                                                                                                  -----from ThoughtWorks 文集

相關文章
相關標籤/搜索