設計模式總結(理論篇)

oliver-l發表於2020-10-05

如何才能寫出高質量的程式碼?

問如何寫出高質量的程式碼,也就等同於在問,如何寫出易維護、易讀、易擴充套件、靈活、簡潔、可複用、可測試的程式碼。

要寫出滿足這些評價標準的高質量程式碼,我們需要掌握一些更加細化、更加能落地的程式設計方法論,包括物件導向設計思想、設計原則、設計模式、編碼規範、重構技巧等。而所有這些程式設計方法論的最終目的都是為了編寫出高質量的程式碼。

比如,物件導向中的繼承、多型能讓我們寫出可複用的程式碼;編碼規範能讓我們寫出可讀性好的程式碼;設計原則中的單一職責、DRY、基於介面而非實現、裡式替換原則等,可以讓我們寫出可複用、靈活、可讀性好、易擴充套件、易維護的程式碼;設計模式可以讓我們寫出易擴充套件的程式碼;持續重構可以時刻保持程式碼的可維護性等等。

什麼是物件導向程式設計和麵向物件程式語言?

物件導向程式設計的英文縮寫是 OOP,全稱是 Object Oriented Programming。對應地,物件導向程式語言的英文縮寫是 OOPL,全稱是 Object Oriented Programming Language。

物件導向程式設計中有兩個非常重要、非常基礎的概念,那就是類(class)和物件(object)。

  • 物件導向程式設計是一種程式設計正規化或程式設計風格。它以類或物件作為組織程式碼的基本單元,並將封裝、抽象、繼承、多型四個特性,作為程式碼設計和實現的基石 。

  • 物件導向程式語言是支援類或物件的語法機制,並有現成的語法機制,能方便地實現物件導向程式設計四大特性(封裝、抽象、繼承、多型)的程式語言。

物件導向

  • 物件導向的四大特性:封裝、抽象、繼承、多型

封裝(Encapsulation)

封裝也叫作資訊隱藏或者資料訪問保護。類通過暴露有限的訪問介面,授權外部僅能通過類提供的方式(或者叫函式)來訪問內部資訊或者資料。

對於封裝這個特性,我們需要程式語言本身提供一定的語法機制來支援。這個語法機制就是訪問許可權控制。如public,private,protected。

類僅僅通過有限的方法暴露必要的操作,也能提高類的易用性。如果我們把類屬性都暴露給類的呼叫者,呼叫者想要正確地操作這些屬性,就勢必要對業務細節有足夠的瞭解。而這對於呼叫者來說也是一種負擔。相反,如果我們將屬性封裝起來,暴露少許的幾個必要的方法給呼叫者使用,呼叫者就不需要了解太多背後的業務細節,用錯的概率就減少很多。

抽象(Abstraction)

類的方法是通過程式語言中的“函式”這一語法機制來實現的。通過函式包裹具體的實現邏輯,這本身就是一種抽象。呼叫者在使用函式的時候,並不需要去研究函式內部的實現邏輯,只需要通過函式的命名、註釋或者文件,瞭解其提供了什麼功能,就可以直接使用了。

抽象這個概念是一個非常通用的設計思想,並不單單用在物件導向程式設計中,也可以用來指導架構設計等。而且這個特性也並不需要程式語言提供特殊的語法機制來支援,只需要提供“函式”這一非常基礎的語法機制,就可以實現抽象特性、所以,它沒有很強的“特異性”,有時候並不被看作物件導向程式設計的特性之一。

繼承(Inheritance)

繼承是用來表示類之間的 is-a 關係。從繼承關係上來講,繼承可以分為兩種模式,單繼承和多繼承。PHP只支援單繼承,不支援多重繼承。

繼承最大的一個好處就是程式碼複用。假如兩個類有一些相同的屬性和方法,我們就可以將這些相同的部分,抽取到父類中,讓兩個子類繼承父類。這樣,兩個子類就可以重用父類中的程式碼,避免程式碼重複寫多遍。

多型(Polymorphism)

多型是指,子類可以替換父類。只要兩個類具有相同的方法,就可以實現多型,並不要求兩個類之間有任何關係。多型可以提高程式碼的擴充套件性和複用性,是很多設計模式、設計原則、程式設計技巧的程式碼實現基礎。

物件導向程式設計與程式導向程式設計的區別和聯絡

物件導向程式設計是一種程式設計正規化或程式設計風格。它以類或物件作為組織程式碼的基本單元,並將封裝、抽象、繼承、多型四個特性,作為程式碼設計和實現的基石 。

程式導向程式設計也是一種程式設計正規化或程式設計風格。它以過程(可以理解為方法、函式、操作)作為組織程式碼的基本單元,以資料(可以理解為成員變數、屬性)與方法相分離為最主要的特點。程式導向風格是一種流程化的程式設計風格,通過拼接一組順序執行的方法來運算元據完成一項功能。

程式導向和麵向物件最基本的區別就是,程式碼的組織方式不同。程式導向風格的程式碼被組織成了一組方法集合及其資料結構(struct User),方法和資料結構的定義是分開的。物件導向風格的程式碼被組織成一組類,方法和資料結構被繫結一起,定義在類中。

以PHP為例,假設我們有一個記錄了使用者資訊的文字檔案 users.txt,每行文字的格式是 name&age&gender(比如,小王 &28& 男)。我們希望寫一個程式,從 users.txt 檔案中逐行讀取使用者資訊,然後格式化成 name\tage\tgender(其中,\t 是分隔符)這種文字格式,並且按照 age 從小到大排序之後,重新寫入到另一個文字檔案 formatted_users.txt 中。

//程式導向程式設計

function parse_to_user($text)
{
    // 將text(“小王&28&男”)解析成陣列
    return $user;
}

function format_to_text($user)
{
    // 將$user格式化成文字("小王\t28\t男")
    return $user;
}

function sort_users_by_age($user)  
{
    // 按照年齡從小到大排序users
    return $user
}

function format_user_file($origin_file_path, $new_file_path)  {

    // open files...

    $count = 0;
    $user = array();
    while(1) { // read until the file is empty

        $user[] = parse_to_user(line);

    }

    sort_users_by_age(users);

    for (int i = 0; i < count($user); ++i) {

        $text = format_to_text(users[i]);

        // write to new file...

    }

    // close files...

}
format_user_file("user.txt", "formatted_users.txt");
//物件導向程式設計
public class User(){
    private $name;
    private $age;
    private $sex;
    public static User praseFrom($userInfoText)  {
        // 將text(“小王&28&男”)解析成類User
        $this->name=$name;
        $this->age=$age;
        $this->sex=$sex;
    }

    public String formatToText()  {
        // 將類User變數格式化成文字("小王\t28\t男")
        return $formatUser;
    }
}

public  class  UserFileFormatter  {
    public  function  format(String userFile, String formattedUserFile)  {
        // Open files...
        $userArr = array();
        while (1) { // read until file is empty
            // read from file into userText...
            $user = new User();
            $user->praseFrom($line);
            $userArr[] = $user
        }
        // sort users by age...
        for (int i = 0; i < count($userArr); ++i) {
            $formattedUserText = $userArr[$i].formatToText();
            // write to new file...
        }
        // close files...
    }
}
$userFileFormatter = new UserFileFormatter();
$userFileFormatter->format("user.txt", "formatted_users.txt");

1.對於大規模複雜程式的開發,程式的處理流程並非單一的一條主線,而是錯綜複雜的網狀結構。物件導向程式設計比起程式導向程式設計,更能應對這種複雜型別的程式開發。

2.物件導向程式設計相比程式導向程式設計,具有更加豐富的特性(封裝、抽象、繼承、多型)。利用這些特性編寫出來的程式碼,更加易擴充套件、易複用、易維護。

3.從程式語言跟機器打交道的方式的演進規律中,物件導向程式語言比起程式導向程式語言,更加人性化、更加高階、更加智慧。

在物件導向程式設計中,為什麼容易寫出程式導向風格的程式碼?

你可以聯想一下,在生活中,你去完成一個任務,你一般都會思考,應該先做什麼、後做什麼,如何一步一步地順序執行一系列操作,最後完成整個任務。程式導向程式設計風格恰恰符合人的這種流程化思維方式。而物件導向程式設計風格正好相反。它是一種自底向上的思考方式。它不是先去按照執行流程來分解任務,而是將任務翻譯成一個一個的小的模組(也就是類),設計類之間的互動,最後按照流程將類組裝起來,完成整個任務。

除此之外,物件導向程式設計要比程式導向程式設計難一些。在物件導向程式設計中,類的設計還是挺需要技巧,挺需要一定設計經驗的。你要去思考如何封裝合適的資料和方法到一個類裡,如何設計類之間的關係,如何設計類之間的互動等等諸多設計問題。

介面和抽象類的區別以及各自的應用場景

抽象類

  1. 抽象類不允許被例項化,只能被繼承。
  2. 抽象類可以包含屬性和方法。
  3. 子類繼承抽象類,必須實現抽象類中的所有抽象方法。

介面

  1. 介面不能包含屬性(也就是成員變數)。
  2. 介面只能宣告方法,方法不能包含程式碼實現。
  3. 類實現介面的時候,必須實現介面中宣告的所有方法。

抽象類實際上就是類,只不過是一種特殊的類,這種類不能被例項化為物件,只能被子類繼承。我們知道,繼承關係是一種 is-a 的關係,那抽象類既然屬於類,也表示一種 is-a 的關係。相對於抽象類的 is-a 關係來說,介面表示一種 has-a 關係,表示具有某些功能。對於介面,有一個更加形象的叫法,那就是協議(contract)。

什麼時候該用抽象類?什麼時候該用介面?實際上,判斷的標準很簡單。如果要表示一種 is-a 的關係,並且是為了解決程式碼複用問題,我們就用抽象類;如果要表示一種 has-a 關係,並且是為了解決抽象而非程式碼複用問題,那我們就用介面。

基於介面而非實現程式設計的設計思想

1.“基於介面而非實現程式設計”,這條原則的另一個表述方式,是“基於抽象而非實現程式設計”。後者的表述方式其實更能體現這條原則的設計初衷。我們在做軟體開發的時候,一定要有抽象意識、封裝意識、介面意識。越抽象、越頂層、越脫離具體某一實現的設計,越能提高程式碼的靈活性、擴充套件性、可維護性。

2. 我們在定義介面的時候,一方面,命名要足夠通用,不能包含跟具體實現相關的字眼;另一方面,與特定實現有關的方法不要定義在介面中。

3.“基於介面而非實現程式設計”這條原則,不僅僅可以指導非常細節的程式設計開發,還能指導更加上層的架構設計、系統設計等。比如,服務端與客戶端之間的“介面”設計、類庫的“介面”設計。

多用組合少用繼承的設計思想

繼承是物件導向的四大特性之一,用來表示類之間的 is-a 關係,可以解決程式碼複用的問題。雖然繼承有諸多作用,但繼承層次過深、過複雜,也會影響到程式碼的可維護性。在這種情況下,我們應該儘量少用,甚至不用繼承。

繼承主要有三個作用:表示 is-a 關係,支援多型特性,程式碼複用。而這三個作用都可以通過組合、介面、委託三個技術手段來達成。除此之外,利用組合還能解決層次過深、過複雜的繼承關係影響程式碼可維護性的問題。

儘管我們鼓勵多用組合少用繼承,但組合也並不是完美的,繼承也並非一無是處。在實際的專案開發中,我們還是要根據具體的情況,來選擇該用繼承還是組合。如果類之間的繼承結構穩定,層次比較淺,關係不復雜,我們就可以大膽地使用繼承。反之,我們就儘量使用組合來替代繼承。除此之外,還有一些設計模式、特殊的應用場景,會固定使用繼承或者組合。

物件導向分析(OOA)、物件導向設計(OOD)、物件導向程式設計(OOP)

物件導向分析(OOA)、物件導向設計(OOD)、物件導向程式設計(OOP),是物件導向開發的三個主要環節。

物件導向分析的產出是詳細的需求描述。物件導向設計的產出是類。在物件導向設計這一環節中,我們將需求描述轉化為具體的類的設計。這個環節的工作可以拆分為下面四個部分。

1. 劃分職責進而識別出有哪些類

根據需求描述,我們把其中涉及的功能點,一個一個羅列出來,然後再去看哪些功能點職責相近,操作同樣的屬性,可否歸為同一個類。

2. 定義類及其屬性和方法

我們識別出需求描述中的動詞,作為候選的方法,再進一步過濾篩選出真正的方法,把功能點中涉及的名詞,作為候選屬性,然後同樣再進行過濾篩選。

3. 定義類與類之間的互動關係

UML 統一建模語言中定義了六種類之間的關係。它們分別是:泛化、實現、關聯、聚合、組合、依賴。我們從更加貼近程式設計的角度,對類與類之間的關係做了調整,保留四個關係:泛化、實現、組合、依賴。

4. 將類組裝起來並提供執行入口

我們要將所有的類組裝在一起,提供一個執行入口。這個入口可能是一個 main() 函式,也可能是一組給外部用的 API 介面。通過這個入口,我們能觸發整個程式碼跑起來。

設計原則

SOLID 原則 -SRP 單一職責原則

單一職責原則的英文是 Single Responsibility Principle,縮寫為 SRP。這個原則的英文描述是這樣的:A class or module should have a single responsibility。翻譯成中文:一個類或者模組只負責完成一個職責(或者功能)。

一個類只負責完成一個職責或者功能。不要設計大而全的類,要設計粒度小、功能單一的類。單一職責原則是為了實現程式碼高內聚、低耦合,提高程式碼的複用性、可讀性、可維護性。

下面這幾條判斷原則,比起很主觀地去思考類是否職責單一,要更有指導意義、更具有可執行性:

  • 類中的程式碼行數、函式或屬性過多,會影響程式碼的可讀性和可維護性,我們就需要考慮對類進行拆分;

  • 類依賴的其他類過多,或者依賴類的其他類過多,不符合高內聚、低耦合的設計思想,我們就需要考慮對類進行拆分;

  • 私有方法過多,我們就要考慮能否將私有方法獨立到新的類中,設定為 public 方法,供更多的類使用,從而提高程式碼的複用性;

  • 比較難給類起一個合適名字,很難用一個業務名詞概括,或者只能用一些籠統的 Manager、Context 之類的詞語來命名,這就說明類的職責定義得可能不夠清晰;

  • 類中大量的方法都是集中操作類中的某幾個屬性;

SOLID 原則 -OCP 開閉原則

開閉原則的英文全稱是 Open Closed Principle,簡寫為 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。我們把它翻譯成中文就是:軟體實體(模組、類、方法等)應該“對擴充套件開放、對修改關閉”。

新增一個新的功能,應該是通過在已有程式碼基礎上擴充套件程式碼(新增模組、類、方法、屬性等),而非修改已有程式碼(修改模組、類、方法、屬性等)的方式來完成。關於定義,我們有兩點要注意。第一點是,開閉原則並不是說完全杜絕修改,而是以最小的修改程式碼的代價來完成新功能的開發。第二點是,同樣的程式碼改動,在粗程式碼粒度下,可能被認定為“修改”;在細程式碼粒度下,可能又被認定為“擴充套件”。

我們要時刻具備擴充套件意識、抽象意識、封裝意識。在寫程式碼的時候,我們要多花點時間思考一下,這段程式碼未來可能有哪些需求變更,如何設計程式碼結構,事先留好擴充套件點,以便在未來需求變更的時候,在不改動程式碼整體結構、做到最小程式碼改動的情況下,將新的程式碼靈活地插入到擴充套件點上。

SOLID 原則 -LSP 裡式替換原則

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

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

理解這個原則,我們還要弄明白裡式替換原則跟多型的區別。雖然從定義描述和程式碼實現上來看,多型和裡式替換有點類似,但它們關注的角度是不一樣的。多型是物件導向程式設計的一大特性,也是物件導向程式語言的一種語法。它是一種程式碼實現的思路。而裡式替換是一種設計原則,用來指導繼承關係中子類該如何設計,子類的設計要保證在替換父類的時候,不改變原有程式的邏輯及不破壞原有程式的正確性。

SOLID 原則 -ISP 介面隔離原則

介面隔離原則的英文翻譯是“ Interface Segregation Principle”,縮寫為 ISP。Robert Martin 在 SOLID 原則中是這樣定義它的:“Clients should not be forced to depend upon interfaces that they do not use。”直譯成中文的話就是:客戶端不應該被強迫依賴它不需要的介面。其中的“客戶端”,可以理解為介面的呼叫者或者使用者。

理解“介面隔離原則”的重點是理解其中的“介面”二字。這裡有三種不同的理解。

如果把“介面”理解為一組介面集合,可以是某個微服務的介面,也可以是某個類庫的介面等。如果部分介面只被部分呼叫者使用,我們就需要將這部分介面隔離出來,單獨給這部分呼叫者使用,而不強迫其他呼叫者也依賴這部分不會被用到的介面。

如果把“介面”理解為單個 API 介面或函式,部分呼叫者只需要函式中的部分功能,那我們就需要把函式拆分成粒度更細的多個函式,讓呼叫者只依賴它需要的那個細粒度函式。

如果把“介面”理解為 OOP 中的介面,也可以理解為物件導向程式語言中的介面語法。那介面的設計要儘量單一,不要讓介面的實現類和呼叫者,依賴不需要的介面函式。

單一職責原則針對的是模組、類、介面的設計。介面隔離原則相對於單一職責原則,一方面更側重於介面的設計,另一方面它的思考角度也是不同的。介面隔離原則提供了一種判斷介面的職責是否單一的標準:通過呼叫者如何使用介面來間接地判定。如果呼叫者只使用部分介面或介面的部分功能,那介面的設計就不夠職責單一。

SOLID 原則 -DIP 依賴倒置原則

依賴反轉原則。依賴反轉原則的英文翻譯是 Dependency Inversion Principle,縮寫為 DIP。中文翻譯有時候也叫依賴倒置原則。

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

所謂高層模組和低層模組的劃分,簡單來說就是,在呼叫鏈上,呼叫者屬於高層,被呼叫者屬於低層。在平時的業務程式碼開發中,高層模組依賴底層模組是沒有任何問題的。實際上,這條原則主要還是用來指導框架層面的設計

1. 控制反轉

實際上,控制反轉是一個比較籠統的設計思想,並不是一種具體的實現方法,一般用來指導框架層面的設計。這裡所說的“控制”指的是對程式執行流程的控制,而“反轉”指的是在沒有使用框架之前,程式設計師自己控制整個程式的執行。在使用框架之後,整個程式的執行流程通過框架來控制。流程的控制權從程式設計師“反轉”給了框架。

2. 依賴注入

依賴注入和控制反轉恰恰相反,它是一種具體的編碼技巧。我們不通過 new 的方式在類內部建立依賴類的物件,而是將依賴的類物件在外部建立好之後,通過建構函式、函式引數等方式傳遞(或注入)給類來使用。

3. 依賴注入框架

我們通過依賴注入框架提供的擴充套件點,簡單配置一下所有需要的類及其類與類之間依賴關係,就可以實現由框架來自動建立物件、管理物件的生命週期、依賴注入等原本需要程式設計師來做的事情。

4. 依賴反轉原則

依賴反轉原則也叫作依賴倒置原則。這條原則跟控制反轉有點類似,主要用來指導框架層面的設計。高層模組不依賴低層模組,它們共同依賴同一個抽象。抽象不要依賴具體實現細節,具體實現細節依賴抽象。

KISS原則

Keep It Simple and Stupid.

KISS 原則算是一個萬金油型別的設計原則,可以應用在很多場景中。它不僅經常用來指導軟體開發,還經常用來指導更加廣泛的系統設計、產品設計等。

對於如何寫出滿足 KISS 原則的程式碼,總結了下面幾條指導原則:

  1. 不要使用同事可能不懂的技術來實現程式碼;

  2. 不要重複造輪子,要善於使用已經有的工具類庫;

  3. 不要過度優化。

YAGNI 原則

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

DRY 原則

Don’t Repeat Yourself

實現邏輯重複、功能語義重複、程式碼執行重複。實現邏輯重複,但功能語義不重複的程式碼,並不違反 DRY 原則。實現邏輯不重複,但功能語義重複的程式碼,也算是違反 DRY 原則。除此之外,程式碼執行重複也算是違反 DRY 原則。

提高程式碼可複用性的一些方法

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

  2. 滿足單一職責原則
    如果職責不夠單一,模組、類設計得大而全,那依賴它的程式碼或者它依賴的程式碼就會比較多,進而增加了程式碼的耦合。根據上一點,也就會影響到程式碼的複用性。相反,越細粒度的程式碼,程式碼的通用性會越好,越容易被複用。

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

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

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

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

  7. 應用模板等設計模式
    一些設計模式,也能提高程式碼的複用性。比如,模板模式利用了多型來實現,可以靈活地替換其中的部分程式碼,整個流程模板程式碼可複用。

我們在第一次寫程式碼的時候,如果當下沒有複用的需求,而未來的複用需求也不是特別明確,並且開發可複用程式碼的成本比較高,那我們就不需要考慮程式碼的複用性。在之後開發新的功能的時候,發現可以複用之前寫的這段程式碼,那我們就重構這段程式碼,讓其變得更加可複用。

LOD 法則

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

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

“高內聚、鬆耦合”是一個非常重要的設計思想,能夠有效提高程式碼的可讀性和可維護性,縮小功能改動導致的程式碼改動範圍。“高內聚”用來指導類本身的設計,“鬆耦合”用來指導類與類之間依賴關係的設計。

所謂高內聚,就是指相近的功能應該放到同一個類中,不相近的功能不要放到同一類中。相近的功能往往會被同時修改,放到同一個類中,修改會比較集中。所謂鬆耦合指的是,在程式碼中,類與類之間的依賴關係簡單清晰。即使兩個類有依賴關係,一個類的程式碼改動也不會或者很少導致依賴類的程式碼改動。

不該有直接依賴關係的類之間,不要有依賴;有依賴關係的類之間,儘量只依賴必要的介面。迪米特法則是希望減少類之間的耦合,讓類越獨立越好。每個類都應該少了解系統的其他部分。一旦發生變化,需要了解這一變化的類就會比較少。

總結:物件導向設計的本質就是把合適的程式碼放到合適的類中。合理地劃分程式碼可以實現程式碼的高內聚、低耦合,類與類之間的互動簡單清晰,程式碼整體結構一目瞭然。

重構程式碼

1. 重構的目的:為什麼重構(why)?

對於專案來言,重構可以保持程式碼質量持續處於一個可控狀態,不至於腐化到無可救藥的地步。對於個人而言,重構非常鍛鍊一個人的程式碼能力,並且是一件非常有成就感的事情。它是我們學習的經典設計思想、原則、模式、程式設計規範等理論知識的練兵場。

2. 重構的物件:重構什麼(what)?

按照重構的規模,我們可以將重構大致分為大規模高層次的重構和小規模低層次的重構。大規模高層次重構包括對程式碼分層、模組化、解耦、梳理類之間的互動關係、抽象複用元件等等。這部分工作利用的更多的是比較抽象、比較頂層的設計思想、原則、模式。小規模低層次的重構包括規範命名、註釋、修正函式引數過多、消除超大類、提取重複程式碼等等程式設計細節問題,主要是針對類、函式級別的重構。小規模低層次的重構更多的是利用編碼規範這一理論知識。

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

我們一定要建立持續重構意識,把重構作為開發必不可少的部分,融入到日常開發中,而不是等到程式碼出現很大問題的時候,再大刀闊斧地重構。

4. 重構的方法:如何重構(how)?

大規模高層次的重構難度比較大,需要組織、有計劃地進行,分階段地小步快跑,時刻讓程式碼處於一個可執行的狀態。而小規模低層次的重構,因為影響範圍小,改動耗時短,所以,只要你願意並且有時間,隨時隨地都可以去做。

改善程式碼質量的程式設計規範

1.關於命名

  • 命名的關鍵是能準確達意。對於不同作用域的命名,我們可以適當地選擇不同的長度。
  • 我們可以藉助類的資訊來簡化屬性、函式的命名,利用函式的資訊來簡化函式引數的命名。
  • 命名要可讀、可搜尋。不要使用生僻的、不好讀的英文單詞來命名。命名要符合專案的統一規範,也不要用些反直覺的命名。
  • 介面有兩種命名方式:一種是在介面中帶字首“I”;另一種是在介面的實現類中帶字尾“Impl”。對於抽象類的命名,也有兩種方式,一種是帶上字首“Abstract”,一種是不帶字首。這兩種命名方式都可以,關鍵是要在專案中統一。

2. 關於註釋

  • 註釋的內容主要包含這樣三個方面:做什麼、為什麼、怎麼做。對於一些複雜的類和介面,我們可能還需要寫明“如何用”。
  • 類和函式一定要寫註釋,而且要寫得儘可能全面詳細。函式內部的註釋要相對少一些,一般都是靠好的命名、提煉函式、解釋性變數、總結性註釋來提高程式碼可讀性。

3. 關於程式碼風格

  • 函式、類多大才合適?函式的程式碼行數不要超過一螢幕的大小,比如 50 行。類的大小限制比較難確定。
  • 一行程式碼多長最合適?最好不要超過 IDE 的顯示寬度。當然,也不能太小,否則會導致很多稍微長點的語句被折成兩行,也會影響到程式碼的整潔,不利於閱讀。
  • 善用空行分割單元塊。對於比較長的函式,為了讓邏輯更加清晰,可以使用空行來分割各個程式碼塊。
  • 四格縮排還是兩格縮排?我個人比較推薦使用兩格縮排,這樣可以節省空間,尤其是在程式碼巢狀層次比較深的情況下。不管是用兩格縮排還是四格縮排,一定不要用 tab 鍵縮排。
  • 大括號是否要另起一行?將大括號放到跟上一條語句同一行,可以節省程式碼行數。但是將大括號另起新的一行的方式,左右括號可以垂直對齊,哪些程式碼屬於哪一個程式碼塊,更加一目瞭然。
  • 類中成員怎麼排列?在 Google Java 程式設計規範中,依賴類按照字母序從小到大排列。類中先寫成員變數後寫函式。成員變數之間或函式之間,先寫靜態成員變數或函式,後寫普通變數或函式,並且按照作用域大小依次排列。

4. 關於編碼技巧

  • 將複雜的邏輯提煉拆分成函式和類。
  • 通過拆分成多個函式或將引數封裝為物件的方式,來處理引數過多的情況。
  • 函式中不要使用引數來做程式碼執行邏輯的控制。
  • 函式設計要職責單一。
  • 移除過深的巢狀層次,方法包括:去掉多餘的 if 或 else 語句,使用 continue、break、return 關鍵字提前退出巢狀,調整執行順序來減少巢狀,將部分巢狀邏輯抽象成函式。
  • 用字面常量取代魔法數。
  • 用解釋性變數來解釋複雜表示式,以此提高程式碼可讀性。

5. 統一編碼規範

  • 最後,還有一條非常重要的,那就是,專案、團隊,甚至公司,一定要制定統一的編碼規範,並且通過 Code Review 督促執行,這對提高程式碼質量有立竿見影的效果。

程式設計規範

程式設計規範主要解決的是程式碼的可讀性問題。編碼規範相對於設計原則、設計模式,更加具體、更加偏重程式碼細節。即便你可能對設計原則不熟悉、對設計模式不瞭解,但你最起碼要掌握基本的編碼規範,比如,如何給變數、類、函式命名,如何寫程式碼註釋,函式不宜過長、引數不能過多等等。

程式碼重構

在軟體開發中,只要軟體在不停地迭代,就沒有一勞永逸的設計。隨著需求的變化,程式碼的不停堆砌,原有的設計必定會存在這樣那樣的問題。針對這些問題,我們就需要進行程式碼重構。重構是軟體開發中非常重要的一個環節。持續重構是保持程式碼質量不下降的有效手段,能有效避免程式碼腐化到無可救藥的地步。

  • 物件導向程式設計因為其具有豐富的特性(封裝、抽象、繼承、多型),可以實現很多複雜的設計思路,是很多設計原則、設計模式等編碼實現的基礎。

  • 設計原則是指導我們程式碼設計的一些經驗總結,對於某些場景下,是否應該應用某種設計模式,具有指導意義。比如,“開閉原則”是很多設計模式(策略、模板等)的指導原則。

  • 設計模式是針對軟體開發中經常遇到的一些設計問題,總結出來的一套解決方案或者設計思路。應用設計模式的主要目的是提高程式碼的可擴充套件性。從抽象程度上來講,設計原則比設計模式更抽象。設計模式更加具體、更加可執行。

  • 程式設計規範主要解決的是程式碼的可讀性問題。編碼規範相對於設計原則、設計模式,更加具體、更加偏重程式碼細節、更加能落地。持續的小重構依賴的理論基礎主要就是程式設計規範。

  • 重構作為保持程式碼質量不下降的有效手段,利用的就是物件導向、設計原則、設計模式、編碼規範這些理論。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章