<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          常見代碼重構(gòu)技巧(非常實用)

          共 31509字,需瀏覽 64分鐘

           ·

          2021-06-27 18:35

          來源:juejin.cn/post/6954378167947624484

          • 關(guān)于重構(gòu)
            • 為什么要重構(gòu)
            • 什么是重構(gòu)
          • 代碼的壞味道
            • 壞代碼的問題
            • 什么是好代碼
          • 如何重構(gòu)
            • SOLID原則
            • 設(shè)計模式
            • 代碼分層
            • 命名規(guī)范
            • 重構(gòu)技巧
          • 質(zhì)量如何保證
            • 測試驅(qū)動開發(fā)
            • TDD的開發(fā)周期
            • 兩個基本的原則
            • 分層測試點

          關(guān)于重構(gòu)

          為什么要重構(gòu)

          項目在不斷演進過程中,代碼不停地在堆砌。如果沒有人為代碼的質(zhì)量負責(zé),代碼總是會往越來越混亂的方向演進。當(dāng)混亂到一定程度之后,量變引起質(zhì)變,項目的維護成本已經(jīng)高過重新開發(fā)一套新代碼的成本,想要再去重構(gòu),已經(jīng)沒有人能做到了。

          造成這樣的原因往往有以下幾點:

          1. 編碼之前缺乏有效的設(shè)計
          2. 成本上的考慮,在原功能堆砌式編程
          3. 缺乏有效代碼質(zhì)量監(jiān)督機制

          對于此類問題,業(yè)界已有有很好的解決思路:通過持續(xù)不斷的重構(gòu)將代碼中的“壞味道”清除掉。

          什么是重構(gòu)

          重構(gòu)一書的作者Martin Fowler對重構(gòu)的定義:

          重構(gòu)(名詞):對軟件內(nèi)部結(jié)構(gòu)的一種調(diào)整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本。重構(gòu)(動詞):使用一系列重構(gòu)手法,在不改變軟件可觀察行為的前提下,調(diào)整其結(jié)構(gòu)。

          根據(jù)重構(gòu)的規(guī)模可以大致分為大型重構(gòu)和小型重構(gòu):

          大型重構(gòu) :對頂層代碼設(shè)計的重構(gòu),包括:系統(tǒng)、模塊、代碼結(jié)構(gòu)、類與類之間的關(guān)系等的重構(gòu),重構(gòu)的手段有:分層、模塊化、解耦、抽象可復(fù)用組件等等。這類重構(gòu)的工具就是我們學(xué)習(xí)過的那些設(shè)計思想、原則和模式。這類重構(gòu)涉及的代碼改動會比較多,影響面會比較大,所以難度也較大,耗時會比較長,引入bug的風(fēng)險也會相對比較大。

          小型重構(gòu) :對代碼細節(jié)的重構(gòu),主要是針對類、函數(shù)、變量等代碼級別的重構(gòu),比如規(guī)范命名和注釋、消除超大類或函數(shù)、提取重復(fù)代碼等等。小型重構(gòu)更多的是使用統(tǒng)一的編碼規(guī)范。這類重構(gòu)要修改的地方比較集中,比較簡單,可操作性較強,耗時會比較短,引入bug的風(fēng)險相對來說也會比較小。什么時候重構(gòu) 新功能開發(fā)、修bug或者代碼review中出現(xiàn)“代碼壞味道”,我們就應(yīng)該及時進行重構(gòu)。持續(xù)在日常開發(fā)中進行小重構(gòu),能夠降低重構(gòu)和測試的成本。

          代碼的壞味道

          代碼重復(fù)

          • 實現(xiàn)邏輯相同、執(zhí)行流程相同

          方法過長

          • 方法中的語句不在同一個抽象層級
          • 邏輯難以理解,需要大量的注釋
          • 面向過程編程而非面向?qū)ο?/section>

          過大的類

          • 類做了太多的事情
          • 包含過多的實例變量和方法
          • 類的命名不足以描述所做的事情

          邏輯分散

          • 發(fā)散式變化:某個類經(jīng)常因為不同的原因在不同的方向上發(fā)生變化
          • 散彈式修改:發(fā)生某種變化時,需要在多個類中做修改

          嚴重的情結(jié)依戀

          • 某個類的方法過多的使用其他類的成員

          數(shù)據(jù)泥團/基本類型偏執(zhí)

          • 兩個類、方法簽名中包含相同的字段或參數(shù)
          • 應(yīng)該使用類但使用基本類型,比如表示數(shù)值與幣種的Money類、起始值與結(jié)束值的Range類

          不合理的繼承體系

          • 繼承打破了封裝性,子類依賴其父類中特定功能的實現(xiàn)細節(jié)
          • 子類必須跟著其父類的更新而演變,除非父類是專門為了擴展而設(shè)計,并且有很好的文檔說明

          過多的條件判斷

          過長的參數(shù)列

          臨時變量過多

          令人迷惑的暫時字段

          • 某個實例變量僅為某種特定情況而設(shè)置
          • 將實例變量與相應(yīng)的方法提取到新的類中

          純數(shù)據(jù)類

          • 僅包含字段和訪問(讀寫)這些字段的方法
          • 此類被稱為數(shù)據(jù)容器,應(yīng)保持最小可變性

          不恰當(dāng)?shù)拿?/span>

          • 命名無法準確描述做的事情
          • 命名不符合約定俗稱的慣例

          過多的注釋

          壞代碼的問題

          • 難以復(fù)用
          • 系統(tǒng)關(guān)聯(lián)性過多,導(dǎo)致很難分離可重用部分
          • 難于變化
          • 一處變化導(dǎo)致其他很多部分的修改,不利于系統(tǒng)穩(wěn)定
          • 難于理解
          • 命名雜亂,結(jié)構(gòu)混亂,難于閱讀和理解
          • 難以測試
          • 分支、依賴較多,難以覆蓋全面

          什么是好代碼

          代碼質(zhì)量的評價有很強的主觀性,描述代碼質(zhì)量的詞匯也有很多,比如可讀性、可維護性、靈活、優(yōu)雅、簡潔。這些詞匯是從不同的維度去評價代碼質(zhì)量的。其中,可維護性、可讀性、可擴展性又是提到最多的、最重要的三個評價標準。

          要寫出高質(zhì)量代碼,我們就需要掌握一些更加細化、更加能落地的編程方法論,這就包含面向?qū)ο笤O(shè)計思想、設(shè)計原則、設(shè)計模式、編碼規(guī)范、重構(gòu)技巧等。

          如何重構(gòu)

          SOLID原則

          4_SOLID原則.png

          單一職責(zé)原則

          一個類只負責(zé)完成一個職責(zé)或者功能,不要存在多于一種導(dǎo)致類變更的原因。

          單一職責(zé)原則通過避免設(shè)計大而全的類,避免將不相關(guān)的功能耦合在一起,來提高類的內(nèi)聚性。同時,類職責(zé)單一,類依賴的和被依賴的其他類也會變少,減少了代碼的耦合性,以此來實現(xiàn)代碼的高內(nèi)聚、松耦合。但是,如果拆分得過細,實際上會適得其反,反倒會降低內(nèi)聚性,也會影響代碼的可維護性。

          開放-關(guān)閉原則

          添加一個新的功能,應(yīng)該是通過在已有代碼基礎(chǔ)上擴展代碼(新增模塊、類、方法、屬性等),而非修改已有代碼(修改模塊、類、方法、屬性等)的方式來完成。

          開閉原則并不是說完全杜絕修改,而是以最小的修改代碼的代價來完成新功能的開發(fā)。

          很多設(shè)計原則、設(shè)計思想、設(shè)計模式,都是以提高代碼的擴展性為最終目的的。特別是 23 種經(jīng)典設(shè)計模式,大部分都是為了解決代碼的擴展性問題而總結(jié)出來的,都是以開閉原則為指導(dǎo)原則的。最常用來提高代碼擴展性的方法有:多態(tài)、依賴注入、基于接口而非實現(xiàn)編程,以及大部分的設(shè)計模式(比如,裝飾、策略、模板、職責(zé)鏈、狀態(tài))。

          里氏替換原則

          子類對象(object of subtype/derived class)能夠替換程序(program)中父類對象(object of base/parent class)出現(xiàn)的任何地方,并且保證原來程序的邏輯行為(behavior)不變及正確性不被破壞。

          子類可以擴展父類的功能,但不能改變父類原有的功能

          父類中凡是已經(jīng)實現(xiàn)好的方法(相對于抽象方法而言),實際上是在設(shè)定一系列的規(guī)范和契約,雖然它不強制要求所有的子類必須遵從這些契約,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。

          接口隔離原則

          調(diào)用方不應(yīng)該依賴它不需要的接口;一個類對另一個類的依賴應(yīng)該建立在最小的接口上。接口隔離原則提供了一種判斷接口的職責(zé)是否單一的標準:通過調(diào)用者如何使用接口來間接地判定。如果調(diào)用者只使用部分接口或接口的部分功能,那接口的設(shè)計就不夠職責(zé)單一。

          依賴反轉(zhuǎn)原則

          高層模塊不應(yīng)該依賴低層模塊,二者都應(yīng)該依賴其抽象;抽象不應(yīng)該依賴細節(jié),細節(jié)應(yīng)該依賴抽象。

          迪米特法則

          一個對象應(yīng)該對其他對象保持最少的了解

          合成復(fù)用原則

          盡量使用合成/聚合的方式,而不是使用繼承。

          單一職責(zé)原則告訴我們實現(xiàn)類要職責(zé)單一;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向接口編程;接口隔離原則告訴我們在設(shè)計接口的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱,告訴我們要對擴展開放,對修改關(guān)閉。

          設(shè)計模式

          設(shè)計模式:軟件開發(fā)人員在軟件開發(fā)過程中面臨的一般問題的解決方案。這些解決方案是眾多軟件開發(fā)人員經(jīng)過相當(dāng)長的一段時間的試驗和錯誤總結(jié)出來的。每種模式都描述了一個在我們周圍不斷重復(fù)發(fā)生的問題,以及該問題的核心解決方案。

          • 創(chuàng)建型 :主要解決對象的創(chuàng)建問題,封裝復(fù)雜的創(chuàng)建過程,解耦對象的創(chuàng)建代碼和使用代碼
          • 結(jié)構(gòu)型 :主要通過類或?qū)ο蟮牟煌M合,解耦不同功能的耦合
          • 行為型 :主要解決的是類或?qū)ο笾g的交互行為的耦合

          代碼分層

          模塊結(jié)構(gòu)說明

          • server_main:配置層,負責(zé)整個項目的module管理,maven配置管理、資源管理等;
          • server_application:應(yīng)用接入層,承接外部流量入口,例如:RPC接口實現(xiàn)、消息處理、定時任務(wù)等;不要在此包含業(yè)務(wù)邏輯;
          • server_biz:核心業(yè)務(wù)層,用例服務(wù)、領(lǐng)域?qū)嶓w、領(lǐng)域事件等
          • server_irepository:資源接口層,負責(zé)資源接口的暴露
          • server_repository:資源層,負責(zé)資源的proxy訪問,統(tǒng)一外部資源訪問,隔離變化。注意:這里強調(diào)的是弱業(yè)務(wù)性,強數(shù)據(jù)性;
          • server_common:公共層,vo、工具等

          代碼開發(fā)要遵守各層的規(guī)范,并注意層級之間的依賴關(guān)系。

          命名規(guī)范

          一個好的命名應(yīng)該要滿足以下兩個約束:

          • 準確描述所做得事情
          • 格式符合通用的慣例

          如果你覺得一個類或方法難以命名的時候,可能是其承載的功能太多了,需要進一步拆分。

          約定俗稱的慣例

          類命名

          類名使用大駝峰命名形式,類命通常使用名詞或名詞短語。接口名除了用名詞和名詞短語以外,還可以使用形容詞或形容詞短語,如 Cloneable,Callable 等,表示實現(xiàn)該接口的類有某種功能或能力。

          方法命名

          方法命名采用小駝峰的形式,首字小寫,往后的每個單詞首字母都要大寫。和類名不同的是,方法命名一般為動詞或動詞短語,與參數(shù)或參數(shù)名共同組成動賓短語,即動詞 + 名詞。一個好的函數(shù)名一般能通過名字直接獲知該函數(shù)實現(xiàn)什么樣的功能。

          重構(gòu)技巧

          提煉方法

          多個方法代碼重復(fù)、方法中代碼過長或者方法中的語句不在一個抽象層級。方法是代碼復(fù)用的最小粒度,方法過長不利于復(fù)用,可讀性低,提煉方法往往是重構(gòu)工作的第一步。

          意圖導(dǎo)向編程 :把處理某件事的流程和具體做事的實現(xiàn)方式分開。

          • 把一個問題分解為一系列功能性步驟,并假定這些功能步驟已經(jīng)實現(xiàn)
          • 我們只需把把各個函數(shù)組織在一起即可解決這一問題
          • 在組織好整個功能后,我們在分別實現(xiàn)各個方法函數(shù)
          /**
            * 1、交易信息開始于一串標準ASCII字符串。
            * 2、這個信息字符串必須轉(zhuǎn)換成一個字符串的數(shù)組,數(shù)組存放的此次交易的領(lǐng)域語言中所包含的詞匯元素(token)。
            * 3、每一個詞匯必須標準化。
            * 4、包含超過150個詞匯元素的交易,應(yīng)該采用不同于小型交易的方式(不同的算法)來提交,以提高效率。
            * 5、如果提交成功,API返回”true”;失敗,則返回”false”。
            */

          public class Transaction {
            public Boolean commit(String command) {
              Boolean result = true;
              String[] tokens = tokenize(command);
              normalizeTokens(tokens);
              if (isALargeTransaction(tokens)) {
                result = processLargeTransaction(tokens);
              } else {
                result = processSmallTransaction(tokens);
              }
              return result;
            }
          }

          以函數(shù)對象取代函數(shù)

          將函數(shù)放進一個單獨對象中,如此一來局部變量就變成了對象內(nèi)的字段。然后你可以在同一個對象中將這個大型函數(shù)分解為多個小型函數(shù)。

          引入?yún)?shù)對象

          方法參數(shù)比較多時,將參數(shù)封裝為參數(shù)對象

          移除對參數(shù)的賦值

          public int discount(int inputVal, int quantity, int yearToDate) {
            if (inputVal > 50) inputVal -= 2;
            if (quantity > 100) inputVal -= 1;
            if (yearToDate > 10000) inputVal -= 4;
            return inputVal;
          }

          public int discount(int inputVal, int quantity, int yearToDate) {
            int result = inputVal;
            if (inputVal > 50) result -= 2;
            if (quantity > 100) result -= 1;
            if (yearToDate > 10000) result -= 4;
            return result;
          }

          將查詢與修改分離

          任何有返回值的方法,都不應(yīng)該有副作用

          • 不要在convert中調(diào)用寫操作,避免副作用
          • 常見的例外:將查詢結(jié)果緩存到本地

          移除不必要臨時變量

          臨時變量僅使用一次或者取值邏輯成本很低的情況下

          引入解釋性變量

          將復(fù)雜表達式(或其中一部分)的結(jié)果放進一個臨時變量,以此變量名稱來解釋表達式用途

          if ((platform.toUpperCase().indexOf("MAC") > -1)
              && (browser.toUpperCase().indexOf("IE") > -1) && wasInitialized() && resize > 0) {
            // do something
          }

          final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
          final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
          final boolean wasResized = resize > 0;
          if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
            // do something
          }

          使用衛(wèi)語句替代嵌套條件判斷

          把復(fù)雜的條件表達式拆分成多個條件表達式,減少嵌套。嵌套了好幾層的if - then-else語句,轉(zhuǎn)換為多個if語句

          //未使用衛(wèi)語句
          public void getHello(int type) {
              if (type == 1) {
                  return;
              } else {
                  if (type == 2) {
                      return;
                  } else {
                      if (type == 3) {
                          return;
                      } else {
                          setHello();
                      }
                  }
              }
          }

          //使用衛(wèi)語句
          public void getHello(int type) {
              if (type == 1) {
                  return;
              }
              if (type == 2) {
                  return;
              }
              if (type == 3) {
                  return;
              }
              setHello();
          }

          使用多態(tài)替代條件判斷斷

          當(dāng)存在這樣一類條件表達式,它根據(jù)對象類型的不同選擇不同的行為。可以將這種表達式的每個分支放進一個子類內(nèi)的復(fù)寫函數(shù)中,然后將原始函數(shù)聲明為抽象函數(shù)。

          public int calculate(int a, int b, String operator) {
              int result = Integer.MIN_VALUE;

              if ("add".equals(operator)) {
                  result = a + b;
              } else if ("multiply".equals(operator)) {
                  result = a * b;
              } else if ("divide".equals(operator)) {
                  result = a / b;
              } else if ("subtract".equals(operator)) {
                  result = a - b;
              }
              return result;
          }

          當(dāng)出現(xiàn)大量類型檢查和判斷時,if else(或switch)語句的體積會比較臃腫,這無疑降低了代碼的可讀性。另外,if else(或switch)本身就是一個“變化點”,當(dāng)需要擴展新的類型時,我們不得不追加if else(或switch)語句塊,以及相應(yīng)的邏輯,這無疑降低了程序的可擴展性,也違反了面向?qū)ο蟮拈_閉原則。

          基于這種場景,我們可以考慮使用“多態(tài)”來代替冗長的條件判斷,將if else(或switch)中的“變化點”封裝到子類中。這樣,就不需要使用if else(或switch)語句了,取而代之的是子類多態(tài)的實例,從而使得提高代碼的可讀性和可擴展性。很多設(shè)計模式使用都是這種套路,比如策略模式、狀態(tài)模式。

          public interface Operation {
            int apply(int a, int b);
          }

          public class Addition implements Operation {
            @Override
            public int apply(int a, int b) {
              return a + b;
            }
          }

          public class OperatorFactory {
              private final static Map<String, Operation> operationMap = new HashMap<>();
              static {
                  operationMap.put("add"new Addition());
                  operationMap.put("divide"new Division());
                  // more operators
              }

              public static Operation getOperation(String operator) {
                  return operationMap.get(operator);
              }
          }

          public int calculate(int a, int b, String operator) {
              if (OperatorFactory .getOperation == null) {
                 throw new IllegalArgumentException("Invalid Operator");
              }
              return OperatorFactory .getOperation(operator).apply(a, b);
          }

          使用異常替代返回錯誤碼

          非正常業(yè)務(wù)狀態(tài)的處理,使用拋出異常的方式代替返回錯誤碼

          • 不要使用異常處理用于正常的業(yè)務(wù)流程控制


            • 異常處理的性能成本非常高
          • 盡量使用標準異常

          • 避免在finally語句塊中拋出異常


            • 如果同時拋出兩個異常,則第一個異常的調(diào)用棧會丟失
          • finally塊中應(yīng)只做關(guān)閉資源這類的事情

          //使用錯誤碼
          public boolean withdraw(int amount) {
              if (balance < amount) {
                  return false;
              } else {
                  balance -= amount;
                  return true;
              }
          }

          //使用異常
          public void withdraw(int amount) {
              if (amount > balance) {
                  throw new IllegalArgumentException("amount too large");
              }
              balance -= amount;
          }

          引入斷言

          某一段代碼需要對程序狀態(tài)做出某種假設(shè),以斷言明確表現(xiàn)這種假設(shè)。

          • 不要濫用斷言,不要使用它來檢查“應(yīng)該為真”的條件,只使用它來檢查“一定必須為真”的條件
          • 如果斷言所指示的約束條件不能滿足,代碼是否仍能正常運行?如果可以就去掉斷言

          引入Null對象或特殊對象

          當(dāng)使用一個方法返回的對象時,而這個對象可能為空,這個時候需要對這個對象進行操作前,需要進行判空,否則就會報空指針。當(dāng)這種判斷頻繁的出現(xiàn)在各處代碼之中,就會影響代碼的美觀程度和可讀性,甚至增加Bug的幾率。

          空引用的問題在Java中無法避免,但可以通過代碼編程技巧(引入空對象)來改善這一問題。

          //空對象的例子
          public class OperatorFactory {
            static Map<String, Operation> operationMap = new HashMap<>();
            static {
              operationMap.put("add"new Addition());
              operationMap.put("divide"new Division());
              // more operators
            }
            public static Optional<Operation> getOperation(String operator) {
              return Optional.ofNullable(operationMap.get(operator));
            }
          }
          public int calculate(int a, int b, String operator) {
            Operation targetOperation = OperatorFactory.getOperation(operator)
               .orElseThrow(() -> new IllegalArgumentException("Invalid Operator"));
            return targetOperation.apply(a, b);
          }

          //特殊對象的例子
          public class InvalidOp implements Operation {
            @Override
            public int apply(int a, int b)  {
              throw new IllegalArgumentException("Invalid Operator");
            }
          }

          提煉類

          根據(jù)單一職責(zé)原則,一個類應(yīng)該有明確的責(zé)任邊界。但在實際工作中,類會不斷的擴展。當(dāng)給某個類添加一項新責(zé)任時,你會覺得不值得分離出一個單獨的類。于是,隨著責(zé)任不斷增加,這個類包含了大量的數(shù)據(jù)和函數(shù),邏輯復(fù)雜不易理解。

          此時你需要考慮將哪些部分分離到一個單獨的類中,可以依據(jù)高內(nèi)聚低耦合的原則。如果某些數(shù)據(jù)和方法總是一起出現(xiàn),或者某些數(shù)據(jù)經(jīng)常同時變化,這就表明它們應(yīng)該放到一個類中。另一種信號是類的子類化方式:如果你發(fā)現(xiàn)子類化只影響類的部分特性,或者類的特性需要以不同方式來子類化,這就意味著你需要分解原來的類。

          //原始類
          public class Person {
              private String name;
              private String officeAreaCode;
              private String officeNumber;

              public String getName() {
                  return name;
              }

              public String getTelephoneNumber() {
                  return ("(" + officeAreaCode + ")" + officeNumber);
              }

              public String getOfficeAreaCode() {
                  return officeAreaCode;
              }

              public void setOfficeAreaCode(String arg) {
                  officeAreaCode = arg;
              }

              public String getOfficeNumber() {
                  return officeNumber;
              }

              public void setOfficeNumber(String arg) {
                  officeNumber = arg;
              }
          }

          //新提煉的類(以對象替換數(shù)據(jù)值)
          public class TelephoneNumber {
              private String areaCode;
              private String number;

              public String getTelephnoeNumber() {
                  return ("(" + getAreaCode() + ")" + number);
              }

              String getAreaCode() {
                  return areaCode;
              }

              void setAreaCode(String arg) {
                  areaCode = arg;
              }

              String getNumber() {
                  return number;
              }

              void setNumber(String arg) {
                  number = arg;
              }
          }

          組合優(yōu)先于繼承

          繼承使實現(xiàn)代碼重用的有力手段,但這并非總是完成這項工作的最佳工具,使用不當(dāng)會導(dǎo)致軟件變得很脆弱。與方法調(diào)用不同的是,繼承打破了封裝性。子類依賴于其父類中特定功能的實現(xiàn)細節(jié),如果父類的實現(xiàn)隨著發(fā)行版本的不同而變化,子類可能會遭到破壞,即使他的代碼完全沒有改變。

          舉例說明,假設(shè)有一個程序使用HashSet,為了調(diào)優(yōu)該程序的性能,需要統(tǒng)計HashSet自從它創(chuàng)建以來添加了多少個元素。為了提供該功能,我們編寫一個HashSet的變體。

          // Inappropriate use of inheritance!
          public class InstrumentedHashSet<Eextends HashSet<E{
              // The number of attempted element insertions
              private int addCount = 0;

              public InstrumentedHashSet() { }

              public InstrumentedHashSet(int initCap, float loadFactor) {
                  super(initCap, loadFactor);
              }

              @Override
              public boolean add(E e) {
                  addCount++;
                  return super.add(e);
              }

              @Override
              public boolean addAll(Collection<? extends E> c) {
                  addCount += c.size();
                  return super.addAll(c);
              }

              public int getAddCount() {
                  return addCount;
              }
          }

          通過在新的類中增加一個私有域,它引用現(xiàn)有類的一個實例,這種設(shè)計被稱為組合,因為現(xiàn)有的類變成了新類的一個組件。這樣得到的類將會非常穩(wěn)固,它不依賴現(xiàn)有類的實現(xiàn)細節(jié)。即使現(xiàn)有的類添加了新的方法,也不會影響新的類。許多設(shè)計模式使用就是這種套路,比如代理模式、裝飾者模式

          // Reusable forwarding class
          public class ForwardingSet<Eimplements Set<E{
              private final Set<E> s;
              public ForwardingSet(Set<E> s) this.s = s; }

              @Override
              public int size() return s.size(); }
              @Override
              public boolean isEmpty() return s.isEmpty(); }
              @Override
              public boolean contains(Object o) return s.contains(o); }
              @Override
              public Iterator<E> iterator() return s.iterator(); }
              @Override
              public Object[] toArray() { return s.toArray(); }
              @Override
              public <T> T[] toArray(T[] a) { return s.toArray(a); }
              @Override
              public boolean add(E e) return s.add(e); }
              @Override
              public boolean remove(Object o) return s.remove(o); }
              @Override
              public boolean containsAll(Collection<?> c) return s.containsAll(c); }
              @Override
              public boolean addAll(Collection<? extends E> c) return s.addAll(c); }
              @Override
              public boolean retainAll(Collection<?> c) return s.retainAll(c); }
              @Override
              public boolean removeAll(Collection<?> c) return s.removeAll(c); }
              @Override
              public void clear() { s.clear(); }
          }

          // Wrappter class - uses composition in place of inheritance
          public class InstrumentedHashSet<Eextends ForwardingSet<E{
              private int addCount = 0;

              public InstrumentedHashSet1(Set<E> s) {
                  super(s);
              }

              @Override
              public boolean add(E e) {
                  addCount++;
                  return super.add(e);
              }

              @Override
              public boolean addAll(Collection<? extends E> c) {
                  addCount += c.size();
                  return super.addAll(c);
              }

              public int getAddCount() {
                  return addCount;
              }
          }

          繼承與組合如何取舍

          • 只有當(dāng)子類真正是父類的子類型時,才適合繼承。對于兩個類A和B,只有兩者之間確實存在“is-a”關(guān)系的時候,類B才應(yīng)該繼承A;
          • 在包的內(nèi)部使用繼承是非常安全的,子類和父類的實現(xiàn)都處在同一個程序員的控制之下;
          • 對于專門為了繼承而設(shè)計并且具有很好的文檔說明的類來說,使用繼承也是非常安全的;
          • 其他情況就應(yīng)該優(yōu)先考慮組合的方式來實現(xiàn)

          接口優(yōu)于抽象類

          Java提供了兩種機制,可以用來定義允許多個實現(xiàn)的類型:接口和抽象類。自從Java8為接口增加缺省方法(default method),這兩種機制都允許為實例方法提供實現(xiàn)。主要區(qū)別在于,為了實現(xiàn)由抽象類定義的類型,類必須稱為抽象類的一個子類。因為Java只允許單繼承,所以用抽象類作為類型定義受到了限制。

          接口相比于抽象類的優(yōu)勢:

          • 現(xiàn)有的類可以很容易被更新,以實現(xiàn)新的接口。
          • 接口是定義混合類型(比如Comparable)的理想選擇。
          • 接口允許構(gòu)造非層次結(jié)構(gòu)的類型框架。

          接口雖然提供了缺省方法,但接口仍有有以下局限性:

          • 接口的變量修飾符只能是public static final的
          • 接口的方法修飾符只能是public的
          • 接口不存在構(gòu)造函數(shù),也不存在this
          • 可以給現(xiàn)有接口增加缺省方法,但不能確保這些方法在之前存在的實現(xiàn)中都能良好運行。
          • 因為這些默認方法是被注入到現(xiàn)有實現(xiàn)中的,它們的實現(xiàn)者并不知道,也沒有許可

          接口缺省方法的設(shè)計目的和優(yōu)勢在于:

          為了接口的演化

          • Java 8 之前我們知道,一個接口的所有方法其子類必須實現(xiàn)(當(dāng)然,這個子類不是一個抽象類),但是 java 8 之后接口的默認方法可以選擇不實現(xiàn),如上的操作是可以通過編譯期編譯的。這樣就避免了由 Java 7 升級到 Java 8 時項目編譯報錯了。Java8在核心集合接口中增加了許多新的缺省方法,主要是為了便于使用lambda。

          可以減少第三方工具類的創(chuàng)建

          • 例如在 List 等集合接口中都有一些默認方法,List 接口中默認提供 replaceAll(UnaryOperator)、sort(Comparator)、、spliterator()等默認方法,這些方法在接口內(nèi)部創(chuàng)建,避免了為了這些方法而專門去創(chuàng)建相應(yīng)的工具類。

          可以避免創(chuàng)建基類

          • 在 Java 8 之前我們可能需要創(chuàng)建一個基類來實現(xiàn)代碼復(fù)用,而默認方法的出現(xiàn),可以不必要去創(chuàng)建基類。

          由于接口的局限性和設(shè)計目的的不同,接口并不能完全替換抽象類。但是通過對接口提供一個抽象的骨架實現(xiàn)類,可以把接口和抽象類的優(yōu)點結(jié)合起來。 接口負責(zé)定義類型,或許還提供一些缺省方法,而骨架實現(xiàn)類則負責(zé)實現(xiàn)除基本類型接口方法之外,剩下的非基本類型接口方法。擴展骨架實現(xiàn)占了實現(xiàn)接口之外的大部分工作。這就是模板方法(Template Method)設(shè)計模式。

          接口Protocol:定義了RPC協(xié)議層兩個主要的方法,export暴露服務(wù)和refer引用服務(wù)

          抽象類AbstractProtocol:封裝了暴露服務(wù)之后的Exporter和引用服務(wù)之后的Invoker實例,并實現(xiàn)了服務(wù)銷毀的邏輯

          具體實現(xiàn)類XxxProtocol:實現(xiàn)export暴露服務(wù)和refer引用服務(wù)具體邏輯

          優(yōu)先考慮泛型

          聲明中具有一個或者多個類型參數(shù)(type parameter)的類或者接口,就是泛型(generic)類或者接口。泛型類和接口統(tǒng)稱為泛型(generic type)。泛型從Java 5引入,提供了編譯時類型安全檢測機制。泛型的本質(zhì)是參數(shù)化類型,通過一個參數(shù)來表示所操作的數(shù)據(jù)類型,并且可以限制這個參數(shù)的類型范圍。泛型的好處就是編譯期類型檢測,避免類型轉(zhuǎn)換。

          // 比較三個值并返回最大值
          public static <T extends Comparable<T>> maximum(T x, T y, T z) {
            T max = x;
            // 假設(shè)x是初始最大值
            if ( y.compareTo( max ) > 0 ) {
              max = y; //y 更大
            }   if ( z.compareTo( max ) > 0 ) {
              max = z; // 現(xiàn)在 z 更大
            }   return max; // 返回最大對象
          }

          public static void main( String args[] ) {
            System.out.printf( "%d, %d 和 %d 中最大的數(shù)為 %d\n\n",  345, maximum( 345 ));
            System.out.printf( "%.1f, %.1f 和 %.1f 中最大的數(shù)為 %.1f\n\n",  6.68.87.7,  maximum( 6.68.87.7 ));
            System.out.printf( "%s, %s 和 %s 中最大的數(shù)為 %s\n","pear""apple""orange", maximum( "pear""apple""orange" ) );
          }

          不要使用原生態(tài)類型

          由于為了保持Java代碼的兼容性,支持和原生態(tài)類型轉(zhuǎn)換,并使用擦除機制實現(xiàn)的泛型。但是使用原生態(tài)類型就會失去泛型的優(yōu)勢,會受到編譯器警告。

          要盡可能地消除每一個非受檢警告

          每一條警告都表示可能在運行時拋出ClassCastException異常。要盡最大的努力去消除這些警告。如果無法消除但是可以證明引起警告的代碼是安全的,就可以在盡可能小的范圍中,使用@SuppressWarnings("unchecked")注解來禁止警告,但是要把禁止的原因記錄下來。

          利用有限制通配符來提升API的靈活性

          參數(shù)化類型不支持協(xié)變的,即對于任何兩個不同的類型Type1和Type2而言,List既不是List的子類型,也不是它的超類。為了解決這個問題,提高靈活性,Java提供了一種特殊的參數(shù)化類型,稱作有限制的通配符類型,即List<? extends E>和List<? super E>。使用原則是producer-extends,consumer-super(PECS)。如果即是生產(chǎn)者,又是消費者,就沒有必要使用通配符了。

          還有一種特殊的無限制通配符List<?>,表示某種類型但不確定。常用作泛型的引用,不可向其添加除Null以外的任何對象。

          //List<? extends E>
          // Number 可以認為 是Number 的 "子類"
          List<? extends Number> numberArray = new ArrayList<Number>();
          // Integer 是 Number 的子類
          List<? extends Number> numberArray = new ArrayList<Integer>();
          // Double 是 Number 的子類
          List<? extends Number> numberArray = new ArrayList<Double>();

          //List<? super E>
          // Integer 可以認為是 Integer 的 "父類"
          List<? super Integer> array = new ArrayList<Integer>();、
          // Number 是 Integer 的 父類
          List<? super Integer> array = new ArrayList<Number>();
          // Object 是 Integer 的 父類
          List<? super Integer> array = new ArrayList<Object>();

          public static <T> void copy(List<? super T> dest, List<? extends T> src) {
            int srcSize = src.size();
            if (srcSize > dest.size())
             throw new IndexOutOfBoundsException("Source does not fit in dest");
            if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) {
              for (int i=0; i<srcSize; i++)
              dest.set(i, src.get(i));
            } else {
              ListIterator<? super T> di=dest.listIterator();
              ListIterator<? extends T> si=src.listIterator();
              for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
              }
            }
          }

          靜態(tài)成員類優(yōu)于非靜態(tài)成員類

          嵌套類(nested class)是指定義在另一個類的內(nèi)部的類。嵌套類存在的目的只是為了它的外部類提供服務(wù),如果其他的環(huán)境也會用到的話,應(yīng)該成為一個頂層類(top-level class)。 嵌套類有四種:靜態(tài)成員類(static member class)、非靜態(tài)成員類(nonstatic member class)、匿名類(anonymous class)和 局部類(local class)。除了第一種之外,其他三種都稱為內(nèi)部類(inner class)。

          匿名類(anonymous class)

          沒有名字,聲明的同時進行實例化,只能使用一次。當(dāng)出現(xiàn)在非靜態(tài)的環(huán)境中,會持有外部類實例的引用。通常用于創(chuàng)建函數(shù)對象和過程對象,不過現(xiàn)在會優(yōu)先考慮lambda。

          局部類(local class)

          任何可以聲明局部變量的地方都可以聲明局部類,同時遵循同樣的作用域規(guī)則。跟匿名類不同的是,有名字可以重復(fù)使用。不過實際很少使用局部類。

          靜態(tài)成員類(static member class)

          最簡單的一種嵌套類,聲明在另一個類的內(nèi)部,是這個類的靜態(tài)成員,遵循同樣的可訪問性規(guī)則。常見的用法是作為公有的輔助類,只有與它的外部類一起使用才有意義。

          非靜態(tài)成員類(nonstatic member class)

          盡管語法上,跟靜態(tài)成員類的唯一區(qū)別就是類的聲明不包含static,但兩者有很大的不同。非靜態(tài)成員類的每個實例都隱含地與外部類的實例相關(guān)聯(lián),可以訪問外部類的成員屬性和方法。另外必須先創(chuàng)建外部類的實例之后才能創(chuàng)建非靜態(tài)成員類的實例。

          總而言之,這四種嵌套類都有自己的用途。假設(shè)這個嵌套類屬于一個方法的內(nèi)部,如果只需要在一個地方創(chuàng)建實例,并且已經(jīng)有了一個預(yù)置的類型可以說明這個類的特征,就要把它做成匿名類。如果一個嵌套類需要在單個方法之外仍然可見,或者它太長了,不適合放在方法內(nèi)部,就應(yīng)該使用成員類。如果成員類的每個實例都需要一個指向其外圍實例的引用,就要把成員類做成非靜態(tài)的,否則就做成靜態(tài)的。

          優(yōu)先使用模板/工具類

          通過對常見場景的代碼邏輯進行抽象封裝,形成相應(yīng)的模板工具類,可以大大減少重復(fù)代碼,專注于業(yè)務(wù)邏輯,提高代碼質(zhì)量。

          分離對象的創(chuàng)建與使用

          面向?qū)ο缶幊滔鄬τ诿嫦蜻^程,多了實例化這一步,而對象的創(chuàng)建必須要指定具體類型。我們常見的做法是“哪里用到,就在哪里創(chuàng)建”,使用實例和創(chuàng)建實例的是同一段代碼。這似乎使代碼更具有可讀性,但是某些情況下造成了不必要的耦合。

          public class BusinessObject {
           public void actionMethond {
               //Other things
               Service myServiceObj = new Service();
                 myServiceObj.doService();
                 //Other things
              }
          }

          public class BusinessObject {
           public void actionMethond {
               //Other things
               Service myServiceObj = new ServiceImpl();
                 myServiceObj.doService();
                 //Other things
              }
          }

          public class BusinessObject {
             private Service myServiceObj;
             public BusinessObject(Service aService) {
                 myServiceObj = aService;
              }
           public void actionMethond {
               //Other things
                 myServiceObj.doService();
                 //Other things
              }
          }

          public class BusinessObject {
             private Service myServiceObj;
             public BusinessObject() {
                 myServiceObj = ServiceFactory;
              }
           public void actionMethond {
               //Other things
                 myServiceObj.doService();
                 //Other things
              }
          }

          對象的創(chuàng)建者耦合的是對象的具體類型,而對象的使用者耦合的是對象的接口。也就是說,創(chuàng)建者關(guān)心的是這個對象是什么,而使用者關(guān)心的是它能干什么。這兩者應(yīng)該視為獨立的考量,它們往往會因為不同的原因而改變。

          當(dāng)對象的類型涉及多態(tài)、對象創(chuàng)建復(fù)雜(依賴較多)可以考慮將對象的創(chuàng)建過程分離出來,使得使用者不用關(guān)注對象的創(chuàng)建細節(jié)。設(shè)計模式中創(chuàng)建型模式的出發(fā)點就是如此,實際項目中可以使用工廠模式、構(gòu)建器、依賴注入的方式。

          可訪問性最小化

          區(qū)分一個組件設(shè)計得好不好,一個很重要的因素在于,它對于外部組件而言,是否隱藏了其內(nèi)部數(shù)據(jù)和實現(xiàn)細節(jié)。Java提供了訪問控制機制來決定類、接口和成員的可訪問性。實體的可訪問性由該實體聲明所在的位置,以及該實體聲明中所出現(xiàn)的訪問修飾符(private、protected、public)共同決定的。

          對于頂層的(非嵌套的)類和接口,只有兩種的訪問級別:包級私有的(沒有public修飾)和公有的(public修飾)。

          對于成員(實例/域、方法、嵌套類和嵌套接口)由四種的訪問級別,可訪問性如下遞增:

          • 私有的(private修飾)--只有在聲明該成員的頂層類內(nèi)部才可以訪問這個成員;
          • 包級私有的(默認)--聲明該成員的包內(nèi)部的任何類都可以訪問這個成員;
          • 受保護的(protected修飾)--聲明該成員的類的子類可以訪問這個成員,并且聲明該成員的包內(nèi)部的任何類也可以訪問這個成員;
          • 公有的(public修飾)--在任何地方都可以訪問該成員;

          正確地使用這些修飾符對于實現(xiàn)信息隱藏是非常關(guān)鍵的,原則就是:盡可能地使每個類和成員不被外界訪問(私有或包級私有)。這樣好處就是在以后的發(fā)行版本中,可以對它進行修改、替換或者刪除,而無須擔(dān)心會影響現(xiàn)有的客戶端程序。

          • 如果類或接口能夠做成包級私有的,它就應(yīng)該被做成包級私有的;
          • 如果一個包級私有的頂層類或接口只是在某一個類的內(nèi)部被用到,就應(yīng)該考慮使它成為那個類的私有嵌套類;
          • 公有類不應(yīng)直接暴露實例域,應(yīng)該提供相應(yīng)的方法以保留將來改變該類的內(nèi)部表示法的靈活性;
          • 當(dāng)確定了類的公有API之后,應(yīng)該把其他的成員都變成私有的;
          • 如果同一個包下的類之間存在比較多的訪問時,就要考慮重新設(shè)計以減少這種耦合;

          可變性最小化

          不可變類是指其實例不能被修改的類。每個實例中包含的所有信息都必須在創(chuàng)建該實例時提供,并在對象的整個生命周期內(nèi)固定不變。不可變類好處就是簡單易用、線程安全、可自由共享而不容易出錯。Java平臺類庫中包含許多不可變的類,比如String、基本類型包裝類、BigDecimal等。

          為了使類成為不可變,要遵循下面五條規(guī)則:

          • 聲明所有的域都是私有的

          • 聲明所有的域都是final的


            • 如果一個指向新創(chuàng)建實例的引用在缺乏同步機制的情況下,從一個線程被傳遞到另一個線程,就必須確保正確的行為
          • 不提供任何會修改對象狀態(tài)的方法

          • 保證類不會被擴展(防止子類化,類聲明為final)


            • 防止粗心或者惡意的子類假裝對象的狀態(tài)已經(jīng)改變,從而破壞該類的不可變行為
          • 確保對任何可變組件的互斥訪問


            • 如果類具有指向可變對象的域,則必須確保該類的客戶端無法獲得指向這些對象的引用。并且,永遠不要用客戶端提供的對象引用來初始化這樣的域,也不要從任何訪問方法中返回該對象引用。在構(gòu)造器、訪問方法和readObject 方法中使用保護性拷貝技術(shù)

          可變性最小化的一些建議:

          • 除非有很好的理由要讓類成為可變的類,否則它就應(yīng)該是不可變的;
          • 如果類不能被做成不可變的,仍然應(yīng)該盡可能地限制它的可變性;
          • 除非有令人信服的理由要使域變成非final的,否則要使每個域都是private final的;
          • 構(gòu)造器應(yīng)該創(chuàng)建完全初始化的對象,并建立起所有的約束關(guān)系;

          質(zhì)量如何保證

          測試驅(qū)動開發(fā)

          測試驅(qū)動開發(fā)(TDD)要求以測試作為開發(fā)過程的中心,要求在編寫任何代碼之前,首先編寫用于產(chǎn)碼行為的測試,而編寫的代碼又要以使測試通過為目標。TDD要求測試可以完全自動化地運行,并在對代碼重構(gòu)前后必須運行測試。

          TDD的最終目標是整潔可用的代碼(clean code that works)。大多數(shù)的開發(fā)者大部分時間無法得到整潔可用的代碼。辦法是分而治之。首先解決目標中的“可用”問題,然后再解決“代碼的整潔”問題。這與體系結(jié)構(gòu)驅(qū)動(architecture-driven)的開發(fā)相反。

          采用TDD另一個好處就是讓我們擁有一套伴隨代碼產(chǎn)生的詳盡的自動化測試集。將來無論出于任何原因(需求、重構(gòu)、性能改進)需要對代碼進行維護時,在這套測試集的驅(qū)動下工作,我們代碼將會一直是健壯的。

          TDD的開發(fā)周期

          添加一個測試 -> 運行所有測試并檢查測試結(jié)果 -> 編寫代碼以通過測試 -> 運行所有測試且全部通過 -> 重構(gòu)代碼,以消除重復(fù)設(shè)計,優(yōu)化設(shè)計結(jié)構(gòu)

          兩個基本的原則

          • 僅在測試失敗時才編寫代碼并且只編寫剛好使測試通過的代碼
          • 編寫下一個測試之前消除現(xiàn)有的重復(fù)設(shè)計,優(yōu)化設(shè)計結(jié)構(gòu)

          關(guān)注點分離是這兩條規(guī)則隱含的另一個非常重要的原則。其表達的含義指在編碼階段先達到代碼“可用”的目標,在重構(gòu)階段再追求“整潔”目標,每次只關(guān)注一件事!

          分層測試點



          歡迎添加程序汪個人微信 itwang007  進粉絲群或圍觀朋友圈



          往期資源  需要請自取

          Java項目分享 最新整理全集,找項目不累啦 03版

          臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載

          字節(jié)跳動總結(jié)的設(shè)計模式 PDF 火了,完整版開放下載!


          堪稱神級的Spring Boot手冊,從基礎(chǔ)入門到實戰(zhàn)進階


          臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開放下載!

          喜歡就"在看"唄^_^

          瀏覽 13
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  水蜜桃一区 | xxxxx在线视频 | 国产女人18水真多18精品 | 三级黄色毛片 | 真的可以看 波多野结衣 一区 |