設計模式之美學習筆記一

下面是學習極客時間專欄《設計模式之美》的部分筆記,這部分主要是正式開始講解設計模式之前的一些基礎原則,基本設計思想的貫徹,本篇擇取了部分內容,如有問題可在本博客下留言或者直接在個人博客留言。

里氏替換原則

如何理解“裏式替換原則”?

裏式替換原則的英文翻譯是:Liskov Substitution Principle,縮寫爲 LSP 。 子類對象(object of subtype/derived class)能夠替換程序(program)中父類對象(object of base/parent class)出現的任何地方,並且保證原來程序的邏輯行爲(behavior)不變及正確性不被破壞。

雖然從定義描述和代碼實現上來看,多態和裏式替換有點類似,但它們關注的角度是不一樣的。多態是面向對象編程的一大特性,也是面向對象編程語言的一種語法。它是一種代碼實現的思路。而裏式替換是一種設計原則,是用來指導繼承關係中子類該如何設計的,子類的設計要保證在替換父類的時候,不改變原有程序的邏輯以及不破壞原有程序的正確性。

裏式替換原則是用來指導,繼承關係中子類該如何設計的一個原則。理解裏式替換原則,最核心的就是理解“design by contract,按照協議來設計”這幾個字。父類定義了函數的“約定”(或者叫協議),那子類可以改變函數的內部實現邏輯,但不能改變函數原有的“約定”。這裏的約定包括:函數聲明要實現的功能;對輸入、輸出、異常的約定;甚至包括註釋中所羅列的任何特殊說明。

接口隔離原則

接口隔離原則。它對應 SOLID 中的英文字母“I”。對於這個原則,最關鍵就是理解其中“接口”的含義。

不過,你應該已經發現,接口隔離原則跟單一職責原則有點類似,不過稍微還是有點區別。單一職責原則針對的是模塊、類、接口的設計。而接口隔離原則相對於單一職責原則,一方面它更側重於接口的設計,另一方面它的思考的角度不同。它提供了一種判斷接口是否職責單一的標準:通過調用者如何使用接口來間接地判定。如果調用者只使用部分接口或接口的部分功能,那接口的設計就不夠職責單一。

理解“接口隔離原則”的重點是理解其中的“接口”二字。這裏有三種不同的理解。

  • 如果把“接口”理解爲一組接口集合,可以是某個微服務的接口,也可以是某個類庫的接口等。如果部分接口只被部分調用者使用,我們就需要將這部分接口隔離出來,單獨給這部分調用者使用,而不強迫其他調用者也依賴這部分不會被用到的接口。
  • 如果把“接口”理解爲單個 API 接口或函數,部分調用者只需要函數中的部分功能,那我們就需要把函數拆分成粒度更細的多個函數,讓調用者只依賴它需要的那個細粒度函數。
  • 如果把“接口”理解爲 OOP 中的接口,也可以理解爲面向對象編程語言中的接口語法。那接口的設計要儘量單一,不要讓接口的實現類和調用者,依賴不需要的接口函數。

控制反轉、依賴反轉、依賴注入,這三者有何區別和聯繫

依賴反轉原則的英文翻譯是 Dependency Inversion Principle,縮寫爲 DIP

高層模塊(high-level modules)不要依賴低層模塊(low-level)。高層模塊和低層模塊應該通過抽象(abstractions)來互相依賴。除此之外,抽象(abstractions)不要依賴具體實現細節(details),具體實現細節(details)依賴抽象(abstractions)。

所謂高層模塊和低層模塊的劃分,簡單來說就是,在調用鏈上,調用者屬於高層,被調用者屬於低層。在平時的業務代碼開發中,高層模塊依賴底層模塊是沒有任何問題的。實際上,這條原則主要還是用來指導框架層面的設計,跟前面講到的控制反轉類似。

爲何說KISS、YAGNI原則看似簡單,卻經常被用錯

KISS 原則的英文描述有好幾個版本,比如下面這幾個。

  • Keep It Simple and Stupid.
  • Keep It Short and Simple.
  • Keep It Simple and Straightforward.

不過,仔細看你就會發現,它們要表達的意思其實差不多,翻譯成中文就是:儘量保持簡單。

如何寫出滿足 KISS 原則的代碼?

  • 不要使用同事可能不懂的技術來實現代碼。比如前面例子中的正則表達式,還有一些編程語言中過於高級的語法等。
  • 不要重複造輪子,要善於使用已經有的工具類庫。經驗證明,自己去實現這些類庫,出 bug 的概率會更高,維護的成本也比較高。
  • 不要過度優化。不要過度使用一些奇技淫巧(比如,位運算代替算術運算、複雜的條件語句代替 if-else、使用一些過於底層的函數等)來優化代碼,犧牲代碼的可讀性。

我們在做開發的時候,一定不要過度設計,不要覺得簡單的東西就沒有技術含量。實際上,越是能用簡單的方法解決複雜的問題,越能體現一個人的能力。

YAGNI 跟 KISS 說的是一回事嗎?

YAGNI 原則的英文全稱是:You Ain’t Gonna Need It。直譯就是:你不會需要它。這條原則也算是萬金油了。當用在軟件開發中的時候,它的意思是:不要去設計當前用不到的功能;不要去編寫當前用不到的代碼。實際上,這條原則的核心思想就是:不要做過度設計。

YAGNI 原則跟 KISS 原則並非一回事兒。KISS 原則講的是“如何做”的問題(儘量保持簡單),而 YAGNI 原則說的是“要不要做”的問題(當前不需要的就不要做)。

重複的代碼就一定違背DRY嗎?如何提高代碼的複用性?

DRY 原則。它的英文描述爲:Don’t Repeat Yourself。中文直譯爲:不要重複自己。將它應用在編程中,可以理解爲:不要寫重複的代碼。

主要講三種典型的代碼重複情況,它們分別是:實現邏輯重複、功能語義重複和代碼執行重複。這三種代碼重複,有的看似違反 DRY,實際上並不違反;有的看似不違反,實際上卻違反了。

實現邏輯重複

我們先來看下面這樣一段代碼是否違反了 DRY 原則。如果違反了,你覺得應該如何重構,才能讓它滿足 DRY 原則?如果沒有違反,那又是爲什麼呢?

public class UserAuthenticator {
  public void authenticate(String username, String password) {
    if (!isValidUsername(username)) {
      // ...throw InvalidUsernameException...
    }
    if (!isValidPassword(password)) {
      // ...throw InvalidPasswordException...
    }
    //...省略其他代碼...
  }

  private boolean isValidUsername(String username) {
    // check not null, not empty
    if (StringUtils.isBlank(username)) {
      return false;
    }
    // check length: 4~64
    int length = username.length();
    if (length < 4 || length > 64) {
      return false;
    }
    // contains only lowcase characters
    if (!StringUtils.isAllLowerCase(username)) {
      return false;
    }
    // contains only a~z,0~9,dot
    for (int i = 0; i < length; ++i) {
      char c = username.charAt(i);
      if (!(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.') {
        return false;
      }
    }
    return true;
  }

  private boolean isValidPassword(String password) {
    // check not null, not empty
    if (StringUtils.isBlank(password)) {
      return false;
    }
    // check length: 4~64
    int length = password.length();
    if (length < 4 || length > 64) {
      return false;
    }
    // contains only lowcase characters
    if (!StringUtils.isAllLowerCase(password)) {
      return false;
    }
    // contains only a~z,0~9,dot
    for (int i = 0; i < length; ++i) {
      char c = password.charAt(i);
      if (!(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.') {
        return false;
      }
    }
    return true;
  }
}

在代碼中,有兩處非常明顯的重複的代碼片段:isValidUserName() 函數和 isValidPassword() 函數。重複的代碼被敲了兩遍,或者簡單 copy-paste 了一下,看起來明顯違反 DRY 原則。爲了移除重複的代碼,我們對上面的代碼做下重構,將 isValidUserName() 函數和 isValidPassword() 函數,合併爲一個更通用的函數 isValidUserNameOrPassword()。重構後的代碼如下所示:

public class UserAuthenticatorV2 {

  public void authenticate(String userName, String password) {
    if (!isValidUsernameOrPassword(userName)) {
      // ...throw InvalidUsernameException...
    }

    if (!isValidUsernameOrPassword(password)) {
      // ...throw InvalidPasswordException...
    }
  }

  private boolean isValidUsernameOrPassword(String usernameOrPassword) {
    //省略實現邏輯
    //跟原來的isValidUsername()或isValidPassword()的實現邏輯一樣...
    return true;
  }
}

經過重構之後,代碼行數減少了,也沒有重複的代碼了,是不是更好了呢?答案是否定的

單從名字上看,我們就能發現,合併之後的 isValidUserNameOrPassword() 函數,負責兩件事情:驗證用戶名和驗證密碼,違反了“單一職責原則”和“接口隔離原則”。實際上,即便將兩個函數合併成 isValidUserNameOrPassword(),代碼仍然存在問題。

因爲 isValidUserName() 和 isValidPassword() 兩個函數,雖然從代碼實現邏輯上看起來是重複的,但是從語義上並不重複。所謂“語義不重複”指的是:從功能上來看,這兩個函數乾的是完全不重複的兩件事情,一個是校驗用戶名,另一個是校驗密碼。儘管在目前的設計中,兩個校驗邏輯是完全一樣的,但如果按照第二種寫法,將兩個函數的合併,那就會存在潛在的問題。在未來的某一天,如果我們修改了密碼的校驗邏輯,比如,允許密碼包含大寫字符,允許密碼的長度爲 8 到 64 個字符,那這個時候,isValidUserName() 和 isValidPassword() 的實現邏輯就會不相同。我們就要把合併後的函數,重新拆成合並前的那兩個函數。

儘管代碼的實現邏輯是相同的,但語義不同,我們判定它並不違反 DRY 原則。對於包含重複代碼的問題,我們可以通過抽象成更細粒度函數的方式來解決。比如將校驗只包含 az、09、dot 的邏輯封裝成 boolean onlyContains(String str, String charlist); 函數。

功能語義重複

兩段代碼的實現邏輯不重複,但語義重複,也就是功能重複,我們認爲它違反了 DRY 原則。

代碼執行重複

再來看第三個例子。其中,UserService 中 login() 函數用來校驗用戶登錄是否成功。如果失敗,就返回異常;如果成功,就返回用戶信息。具體代碼如下所示:

public class UserService {
  private UserRepo userRepo;//通過依賴注入或者IOC框架注入

  public User login(String email, String password) {
    boolean existed = userRepo.checkIfUserExisted(email, password);
    if (!existed) {
      // ... throw AuthenticationFailureException...
    }
    User user = userRepo.getUserByEmail(email);
    return user;
  }
}

public class UserRepo {
  public boolean checkIfUserExisted(String email, String password) {
    if (!EmailValidation.validate(email)) {
      // ... throw InvalidEmailException...
    }

    if (!PasswordValidation.validate(password)) {
      // ... throw InvalidPasswordException...
    }

    //...query db to check if email&password exists...
  }

  public User getUserByEmail(String email) {
    if (!EmailValidation.validate(email)) {
      // ... throw InvalidEmailException...
    }
    //...query db to get user by email...
  }
}

上面這段代碼,既沒有邏輯重複,也沒有語義重複,但仍然違反了 DRY 原則。這是因爲代碼中存在“執行重複” , 重複執行最明顯的一個地方,就是在 login() 函數中,email 的校驗邏輯被執行了兩次。一次是在調用 checkIfUserExisted() 函數的時候,另一次是調用 getUserByEmail() 函數的時候。

除此之外,代碼中還有一處比較隱蔽的執行重複,不知道你發現了沒有?實際上,login() 函數並不需要調用 checkIfUserExisted() 函數,只需要調用一次 getUserByEmail() 函數,從數據庫中獲取到用戶的 email、password 等信息,然後跟用戶輸入的 email、password 信息做對比,依次判斷是否登錄成功。

實際上,這樣的優化是很有必要的。因爲 checkIfUserExisted() 函數和 getUserByEmail() 函數都需要查詢數據庫,而數據庫這類的 I/O 操作是比較耗時的。我們在寫代碼的時候,應當儘量減少這類 I/O 操作。

按照剛剛的修改思路,我們把代碼重構一下,移除“重複執行”的代碼,只校驗一次 email 和 password,並且只查詢一次數據庫。重構之後的代碼如下所示:

public class UserService {
  private UserRepo userRepo;//通過依賴注入或者IOC框架注入

  public User login(String email, String password) {
    if (!EmailValidation.validate(email)) {
      // ... throw InvalidEmailException...
    }
    if (!PasswordValidation.validate(password)) {
      // ... throw InvalidPasswordException...
    }
    User user = userRepo.getUserByEmail(email);
    if (user == null || !password.equals(user.getPassword()) {
      // ... throw AuthenticationFailureException...
    }
    return user;
  }
}

public class UserRepo {
  public boolean checkIfUserExisted(String email, String password) {
    //...query db to check if email&password exists
  }

  public User getUserByEmail(String email) {
    //...query db to get user by email...
  }
}

代碼複用性(Code Reusability)

我們首先來區分三個概念:代碼複用性(Code Reusability)代碼複用(Code Resue)DRY 原則

  • 代碼的可複用性表示一段代碼可被複用的特性或能力:我們在編寫代碼的時候,讓代碼儘量可複用。

  • 代碼複用表示一種行爲:我們在開發新功能的時候,儘量複用已經存在的代碼。

  • DRY 原則是一條原則:不要寫重複的代碼。從定義描述上,它們好像有點類似,但深究起來,三者的區別還是蠻大的。

首先,“不重複”並不代表“可複用”。在一個項目代碼中,可能不存在任何重複的代碼,但也並不表示裏面有可複用的代碼,不重複和可複用完全是兩個概念。所以,從這個角度來說,DRY 原則跟代碼的可複用性講的是兩回事。

其次,“複用”和“可複用性”關注角度不同。代碼“可複用性”是從代碼開發者的角度來講的,“複用”是從代碼使用者的角度來講的。比如,A 同事編寫了一個 UrlUtils 類,代碼的“可複用性”很好。B 同事在開發新功能的時候,直接“複用”A 同事編寫的 UrlUtils 類。

儘管複用、可複用性、DRY 原則這三者從理解上有所區別,但實際上要達到的目的都是類似的,都是爲了減少代碼量,提高代碼的可讀性、可維護性。除此之外,複用已經經過測試的老代碼,bug 會比從零重新開發要少。

“複用”這個概念不僅可以指導細粒度的模塊、類、函數的設計開發,實際上,一些框架、類庫、組件等的產生也都是爲了達到複用的目的。比如,Spring 框架、Google Guava 類庫、UI 組件等等。

怎麼提高代碼複用性?

  • 減少代碼耦合

    對於高度耦合的代碼,當我們希望複用其中的一個功能,想把這個功能的代碼抽取出來成爲一個獨立的模塊、類或者函數的時候,往往會發現牽一髮而動全身。移動一點代碼,就要牽連到很多其他相關的代碼。所以,高度耦合的代碼會影響到代碼的複用性,我們要儘量減少代碼耦合。

  • 滿足單一職責原則

    我們前面講過,如果職責不夠單一,模塊、類設計得大而全,那依賴它的代碼或者它依賴的代碼就會比較多,進而增加了代碼的耦合。根據上一點,也就會影響到代碼的複用性。相反,越細粒度的代碼,代碼的通用性會越好,越容易被複用。

  • 模塊化

    這裏的“模塊”,不單單指一組類構成的模塊,還可以理解爲單個類、函數。我們要善於將功能獨立的代碼,封裝成模塊。獨立的模塊就像一塊一塊的積木,更加容易複用,可以直接拿來搭建更加複雜的系統。

  • 業務與非業務邏輯分離

    越是跟業務無關的代碼越是容易複用,越是針對特定業務的代碼越難複用。所以,爲了複用跟業務無關的代碼,我們將業務和非業務邏輯代碼分離,抽取成一些通用的框架、類庫、組件等。

  • 通用代碼下沉

    從分層的角度來看,越底層的代碼越通用、會被越多的模塊調用,越應該設計得足夠可複用。一般情況下,在代碼分層之後,爲了避免交叉調用導致調用關係混亂,我們只允許上層代碼調用下層代碼及同層代碼之間的調用,杜絕下層代碼調用上層代碼。所以,通用的代碼我們儘量下沉到更下層。

  • 繼承、多態、抽象、封裝

    在講面向對象特性的時候,我們講到,利用繼承,可以將公共的代碼抽取到父類,子類複用父類的屬性和方法。利用多態,我們可以動態地替換一段代碼的部分邏輯,讓這段代碼可複用。除此之外,抽象和封裝,從更加廣義的層面、而非狹義的面向對象特性的層面來理解的話,越抽象、越不依賴具體的實現,越容易複用。代碼封裝成模塊,隱藏可變的細節、暴露不變的接口,就越容易複用。

  • 應用模板等設計模式

    一些設計模式,也能提高代碼的複用性。比如,模板模式利用了多態來實現,可以靈活地替換其中的部分代碼,整個流程模板代碼可複用。關於應用設計模式提高代碼複用性這一部分,我們留在後面慢慢來講解。除了剛剛我們講到的幾點,還有一些跟編程語言相關的特性,也能提高代碼的複用性,比如泛型編程等。實際上,除了上面講到的這些方法之外,複用意識也非常重要。在寫代碼的時候,我們要多去思考一下,這個部分代碼是否可以抽取出來,作爲一個獨立的模塊、類或者函數供多處使用。在設計每個模塊、類、函數的時候,要 像設計一個外部 API 那樣,去思考它的複用性。

辯證思考和靈活應用

實際上,編寫可複用的代碼並不簡單。如果我們在編寫代碼的時候,已經有複用的需求場景,那根據複用的需求去開發可複用的代碼,可能還不算難。但是,如果當下並沒有複用的需求,我們只是希望現在編寫的代碼具有可複用的特點,能在未來某個同事開發某個新功能的時候複用得上。在這種沒有具體複用需求的情況下,我們就需要去預測將來代碼會如何複用,這就比較有挑戰了。

實際上,除非有非常明確的複用需求,否則,爲了暫時用不到的複用需求,花費太多的時間、精力,投入太多的開發成本,並不是一個值得推薦的做法。這也違反我們之前講到的 YAGNI 原則。

除此之外,有一個著名的原則,叫作“Rule of Three”。這條原則可以用在很多行業和場景中,你可以自己去研究一下。如果把這個原則用在這裏,那就是說,我們在第一次寫代碼的時候,如果當下沒有複用的需求,而未來的複用需求也不是特別明確,並且開發可複用代碼的成本比較高,那我們就不需要考慮代碼的複用性。在之後我們開發新的功能的時候,發現可以複用之前寫的這段代碼,那我們就重構這段代碼,讓其變得更加可複用。

也就是說,第一次編寫代碼的時候,我們不考慮複用性;第二次遇到複用場景的時候,再進行重構使其複用。需要注意的是,“Rule of Three”中的“Three”並不是真的就指確切的“三”,這裏就是指“二”。

總結

  • DRY 原則

我們今天講了三種代碼重複的情況:實現邏輯重複、功能語義重複、代碼執行重複。實現邏輯重複,但功能語義不重複的代碼,並不違反 DRY 原則。實現邏輯不重複,但功能語義重複的代碼,也算是違反 DRY 原則。除此之外,代碼執行重複也算是違反 DRY 原則。

  • 代碼複用性

    提高代碼可複用性的一些方法,有以下 7 點。

    • 減少代碼耦合
    • 滿足單一職責原則
    • 模塊化
    • 業務與非業務邏輯分離
    • 通用代碼下沉
    • 繼承、多態、抽象、封裝
    • 應用模板等設計模式

如何用迪米特法則(LOD)實現“高內聚、松耦合”?

到底什麼是“高內聚”呢?

所謂高內聚,就是指相近的功能應該放到同一個類中,不相近的功能不要放到同一個類中。相近的功能往往會被同時修改,放到同一個類中,修改會比較集中,代碼容易維護。實際上,單一職責原則是實現代碼高內聚非常有效的設計原則

什麼是“松耦合”?

所謂松耦合是說,在代碼中,類與類之間的依賴關係簡單清晰。即使兩個類有依賴關係,一個類的代碼改動不會或者很少導致依賴類的代碼改動。實際上,我們前面講的依賴注入、接口隔離、基於接口而非實現編程,以及今天講的迪米特法則,都是爲了實現代碼的松耦合。

迪米特法則

迪米特法則的英文翻譯是:Law of Demeter,縮寫是 LOD。單從這個名字上來看,我們完全猜不出這個原則講的是什麼。不過,它還有另外一個更加達意的名字,叫作最小知識原則,英文翻譯爲:The Least Knowledge Principle。

每個模塊(unit)只應該瞭解那些與它關係密切的模塊(units: only units “closely” related to the current unit)的有限知識(knowledge)。或者說,每個模塊只和自己的朋友“說話”(talk),不和陌生人“說話”(talk)。

針對業務系統的開發,如何做需求分析和設計?

假設平臺需要開發一個積分系統,大致涉及到功能點:

籠統地來講,積分系統無外乎就兩個大的功能點,一個是賺取積分,另一個是消費積分。賺取積分功能包括積分賺取渠道,比如下訂單、每日簽到、評論等;還包括積分兌換規則,比如訂單金額與積分的兌換比例,每日簽到贈送多少積分等。消費積分功能包括積分消費渠道,比如抵扣訂單金額、兌換優惠券、積分換購、參與活動扣積分等;還包括積分兌換規則,比如多少積分可以換算成抵扣訂單的多少金額,一張優惠券需要多少積分來兌換等等。

合理地將功能劃分到不同模塊

對於前面羅列的所有功能點,我們有下面三種模塊劃分方法。

第一種劃分方式是:積分賺取渠道及兌換規則、消費渠道及兌換規則的管理和維護(增刪改查),不劃分到積分系統中,而是放到更上層的營銷系統中。這樣積分系統就會變得非常簡單,只需要負責增加積分、減少積分、查詢積分、查詢積分明細等這幾個工作。

舉個例子解釋一下。比如,用戶通過下訂單賺取積分。訂單系統通過異步發送消息或者同步調用接口的方式,告知營銷系統訂單交易成功。營銷系統根據拿到的訂單信息,查詢訂單對應的積分兌換規則(兌換比例、有效期等),計算得到訂單可兌換的積分數量,然後調用積分系統的接口給用戶增加積分。

第二種劃分方式是:積分賺取渠道及兌換規則、消費渠道及兌換規則的管理和維護,分散在各個相關業務系統中,比如訂單系統、評論系統、簽到系統、換購商城、優惠券系統等。還是剛剛那個下訂單賺取積分的例子,在這種情況下,用戶下訂單成功之後,訂單系統根據商品對應的積分兌換比例,計算所能兌換的積分數量,然後直接調用積分系統給用戶增加積分。

第三種劃分方式是:所有的功能都劃分到積分系統中,包括積分賺取渠道及兌換規則、消費渠道及兌換規則的管理和維護。還是同樣的例子,用戶下訂單成功之後,訂單系統直接告知積分系統訂單交易成功,積分系統根據訂單信息查詢積分兌換規則,給用戶增加積分。

怎麼判斷哪種模塊劃分合理呢?實際上,我們可以反過來通過看它是否符合高內聚、低耦合特性來判斷。如果一個功能的修改或添加,經常要跨團隊、跨項目、跨系統才能完成,那說明模塊劃分的不夠合理,職責不夠清晰,耦合過於嚴重。

除此之外,爲了避免業務知識的耦合,讓下層系統更加通用,一般來講,我們不希望下層系統(也就是被調用的系統)包含太多上層系統(也就是調用系統)的業務信息,但是,可以接受上層系統包含下層系統的業務信息。比如,訂單系統、優惠券系統、換購商城等作爲調用積分系統的上層系統,可以包含一些積分相關的業務信息。但是,反過來,積分系統中最好不要包含太多跟訂單、優惠券、換購等相關的信息。

所以,綜合考慮,我們更傾向於第一種和第二種模塊劃分方式。但是,不管選擇這兩種中的哪一種,積分系統所負責的工作是一樣的,只包含積分的增、減、查詢,以及積分明細的記錄和查詢。

爲什麼要分層開發

1.分層開發代碼可以複用,比如同一個dao層代碼可以被多個service層代碼調用,同一個service方法可以被多個controller層代碼調用

2.分層能起到隔離變化的作用

什麼情況下要重構?到底重構什麼?又該如何重構?

軟件設計大師 Martin Fowler 是這樣定義重構的:“重構是一種對軟件內部結構的改善,目的是在不改變軟件的可見行爲的情況下,使其更易理解,修改成本更低。”

爲什麼要進行代碼重構?

重構是時刻保證代碼質量的一個極其有效的手段;

其次,優秀的代碼或架構不是一開始就能完全設計好的,就像優秀的公司和產品也都是迭代出來的。我們無法 100% 遇見未來的需求,也沒有足夠的精力、時間、資源爲遙遠的未來買單,所以,隨着系統的演進,重構代碼也是不可避免的

最後,重構是避免過度設計的有效手段。在我們維護代碼的過程中,真正遇到問題的時候,再對代碼進行重構,能有效避免前期投入太多時間做過度的設計,做到有的放矢。

重構的時機:什麼時候重構(when)?

平時沒有事情的時候,你可以看看項目中有哪些寫得不夠好的、可以優化的代碼,主動去重構一下。或者,在修改、添加某個功能代碼的時候,你也可以順手把不符合編碼規範、不好的設計重構一下。總之,就像把單元測試、Code Review 作爲開發的一部分,我們如果能把持續重構也作爲開發的一部分,成爲一種開發習慣,對項目、對自己都會很有好處。

那些看到別人代碼有點瑕疵就一頓亂罵,或者花盡心思去構思一個完美設計的人,往往都是因爲沒有樹立正確的代碼質量觀,沒有持續重構意識。

編碼規範

命名長度適中

對於作用域小的局部變量,簡短爲先;對於類名,接口名介意見名知意,除通用縮寫外儘量少用縮寫;總體來說表達含義的前提下,越短越好

利用上下文簡化命名

命名要可讀,可搜索

除非是本類內部使用的,否則不要用布爾類型或者參數是否爲null控制代碼邏輯

代碼不要有過深的嵌套層次:去掉多餘的if或者else;使用break,continue,return等語句提前退出嵌套;調整執行順序減少嵌套;將部分嵌套邏輯封裝成函數調用

如何發現代碼質量問題

常規checklist

  • 目錄設置是否合理,模塊劃分是否清晰、代碼結構是否滿足高內聚,低耦合
  • 是否遵循經典的設計原則和設計思想(SOLID、DRY、KISS、YAGNI、LOD等)?
  • 設計模式是否應用得當?是否過度設計?
  • 代碼是否容易擴展?如果要添加新的功能,是否容易實現?
  • 代碼是否可以複用?是否可以複用已有的項目代碼或類庫?是否有重複造輪子?
  • 代碼是否容易測試?單元測試是否全面覆蓋各種正常和異常的情況?
  • 代碼是否容易讀?是否符合編碼規範(比如命名和註釋是否恰當、代碼風格是否一致等)

業務需求checklist

  • 代碼是否實現了預期的業務需求
  • 邏輯是否正確?是否處理了各種異常情況
  • 日誌打印是否得當?是否方便debug排查問題?
  • 接口是否易用,是否支持冪等、事務等?
  • 代碼是否存在併發問題?是否線程安全?
  • 性能是否有優化空間,比如,SQL,算法是否可以優化
  • 是否有安全漏洞?比如輸入輸出校驗是否全面

階段性總結:

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章