設計模式之概述篇

2022-06-30 18:03:39

1、設計模式的本質

物件導向設計原則的實際運用,是對類的封裝性、繼承性和多型性以及類的關聯關係和組合關係的充分理解。

2、設計模式的目的

提高程式碼可讀性、重用性、可靠性、可延伸性,實現「高內聚,低耦合」。

名詞解釋

  1. 可讀性:按照規範程式設計,便於其他程式設計師閱讀和理解
  2. 重用性:相同功能的程式碼,可以重複使用,無需多次編寫
  3. 可靠性:增加功能時,對原有功能沒有影響
  4. 可延伸性:增加功能時方便,可維護性強

3、設計模式的依據

常見的設計模式有23種,但是發展到今天還有很多叫不上名字來的設計模式,無一例外都遵循著「軟體設計七大原則」。

3.1 單一職責原則(Single Responsibility Principle, SRP)

3.1.1 解釋

​ 單一職責就是一個類或者一個方法只負責一項職責

3.1.2 舉例

​ 假設有一個IT部門,一個開發,一個測試,一個運維。我們把三個人的工作抽象為一個類

3.1.2.1 Demo1
public static void main(String[] args) {
    Employee.work("Developer");
    Employee.work("Tester");
    Employee.work("Operator");
}
// 員工類
static class Employee {
    public static void work(String name) {
        System.out.println(name + "正在寫程式碼...");
    }
}

執行結果

Developer正在寫程式碼...
Tester正在寫程式碼...
Operator正在寫程式碼...

​ 很明顯,開發、測試、運維都在寫程式碼,顯然不合理;正常來說,開發寫程式碼、測試寫用例、運維寫指令碼,而Demo1中的work實現了三種不同職責,違背了單一職責原則

3.1.2.2 Demo2
public static void main(String[] args) {
    Developer.work("Developer");
    Tester.work("Tester");
    Operator.work("Operator");
}
// 員工類:開發
static class Developer {
    public static void work(String name) {
        System.out.println(name + "正在寫程式碼...");
    }
}
// 員工類:測試
static class Tester {
    public static void work(String name) {
        System.out.println(name + "正在寫用例...");
    }
}
// 員工類:運維
static class Operator {
    public static void work(String name) {
        System.out.println(name + "正在寫指令碼...");
    }
}

執行結果

Developer正在寫程式碼...
Tester正在寫用例...
Operator正在寫指令碼...

​ 看執行結果,已經符合了單一職責原則,但是看Demo2程式碼會發現,三種職責我們建立了三個類,把Employee分解為DeveloperTesterOperator,並且呼叫方main也做了修改,這樣改動太大

3.1.2.3 Demo3
public static void main(String[] args) {
    Employee.workCode("Developer");
    Employee.workUseCase("Tester");
    Employee.workScript("Operator");
}
// 員工類
static class Employee {
    public static void workCode(String name) {
        System.out.println(name + "正在寫程式碼...");
    }
    public static void workUseCase(String name) {
        System.out.println(name + "正在寫用例...");
    }
    public static void workScript(String name) {
        System.out.println(name + "正在寫指令碼...");
    }
}

執行結果

Developer正在寫程式碼...
Tester正在寫用例...
Operator正在寫指令碼...

​ 在Demo3中把work一分為三,沒有分解類,而是在方法級別上進行了拆分,也達到了預期的效果,並且呼叫者main中改動量也很小

3.1.3 總結

​ 1. 單一職責可以細化為類級別方法級別,最低限度是在方法級別保證單一職責原則;但是如果一個類中有幾十個方法,那麼就需要衡量下是否需要進行類分解

​ 2. 提高程式碼可讀性,可維護性,可延伸性

​ 3. 降低類的複雜度

3.2 介面隔離原則(Interface Segregation Principle,ISP)

3.2.1 解釋

一個類對另一個類的依賴應該建立在最小的介面上,即一個類不應該依賴它不需要的介面

3.2.2 舉例

​ 就拿涼拌黃瓜和炒黃瓜的步驟來舉例

  1. 涼拌黃瓜:洗菜 -> 切菜 -> 涼拌

  2. 炒 黃 瓜:洗菜 -> 切菜 -> 炒菜

3.2.2.1 Demo1
private static final String name = "黃瓜";
public static void main(String[] args) {
    CookingCold cookingCold = new CookingCold();
    ColdMixCucumber coldMixCucumber = new ColdMixCucumber();
    coldMixCucumber.wash(cookingCold);
    coldMixCucumber.cut(cookingCold);
    coldMixCucumber.coldMix(cookingCold);
    System.out.println();
    CookingHot cookingHot = new CookingHot();
    FryCucumber fryCucumber = new FryCucumber();
    fryCucumber.wash(cookingHot);
    fryCucumber.cut(cookingHot);
    fryCucumber.fry(cookingHot);;
}
// 做菜介面
interface Cooking {
    // 洗菜
    void wash(String name);
    // 切菜
    void cut(String name);
    // 涼拌
    void coldMix(String name);
    // 炒菜
    void fry(String name);
}
// 做冷盤
static class CookingCold implements Cooking {
    @Override
    public void wash(String name) {
        System.out.println("洗" + name);
    }
    @Override
    public void cut(String name) {
        System.out.println("切" + name);
    }
    @Override
    public void coldMix(String name) {
        System.out.println("涼拌" + name);
    }
    @Override
    public void fry(String name) {}
}
// 做熱菜
static class CookingHot implements Cooking {
    @Override
    public void wash(String name) {
        System.out.println("洗" + name);
    }
    @Override
    public void cut(String name) {
        System.out.println("切" + name);
    }
    @Override
    public void coldMix(String name) {}
    @Override
    public void fry(String name) {
        System.out.println("炒" + name);
    }
}
// 涼拌黃瓜
static class ColdMixCucumber {
    // 洗黃瓜
    public void wash(Cooking cooking) {
        cooking.wash(name);
    }
    // 切黃瓜
    public void cut(Cooking cooking) {
        cooking.cut(name);
    }
    // 涼拌黃瓜
    public void coldMix(Cooking cooking) {
        cooking.coldMix(name);
    }
}
// 炒黃瓜
static class FryCucumber {
    // 洗黃瓜
    public void wash(Cooking cooking) {
        cooking.wash(name);
    }
    // 切黃瓜
    public void cut(Cooking cooking) {
        cooking.cut(name);
    }
    // 炒黃瓜
    public void fry(Cooking cooking) {
        cooking.fry(name);
    }
}

​ 下圖為Demo1的UML類圖,據此分析CookingColdCookingHot均實現了Cooking介面並實現其所有方法,但在ColdMixCucumberFryCucumber中僅使用了其中的三個方法;雖然執行結果沒有問題,但是明顯Cooking介面的設計不合理,不符合介面隔離原則

3.2.2.2 Demo2

​ 我們注意到Cooking介面中,washcut是都會用到的,而coldMixfry並不會全部用到;根據介面隔離原則,我們把Cooking介面分解為三個介面,UML類圖如下所示:

private static final String name = "黃瓜";
public static void main(String[] args) {
    CookingCold cookingCold = new CookingCold();
    ColdMixCucumber coldMixCucumber = new ColdMixCucumber();
    coldMixCucumber.wash(cookingCold);
    coldMixCucumber.cut(cookingCold);
    coldMixCucumber.coldMix(cookingCold);
    System.out.println();
    CookingHot cookingHot = new CookingHot();
    FryCucumber fryCucumber = new FryCucumber();
    fryCucumber.wash(cookingHot);
    fryCucumber.cut(cookingHot);
    fryCucumber.fry(cookingHot);;
}
// 做菜介面_01
interface Cooking_01 {
    // 洗菜
    void wash(String name);
    // 切菜
    void cut(String name);
}
// 做菜介面_02
interface Cooking_02 {
    // 涼拌
    void coldMix(String name);
}
// 做菜介面_03
interface Cooking_03 {
    // 炒菜
    void fry(String name);
}
// 做冷盤
static class CookingCold implements Cooking_01, Cooking_02 {
    @Override
    public void wash(String name) {
        System.out.println("洗" + name);
    }
    @Override
    public void cut(String name) {
        System.out.println("切" + name);
    }
    @Override
    public void coldMix(String name) {
        System.out.println("涼拌" + name);
    }
}
// 做熱菜
static class CookingHot implements Cooking_01, Cooking_03 {
    @Override
    public void wash(String name) {
        System.out.println("洗" + name);
    }
    @Override
    public void cut(String name) {
        System.out.println("切" + name);
    }
    @Override
    public void fry(String name) {
        System.out.println("炒" + name);
    }
}
// 涼拌黃瓜
static class ColdMixCucumber {
    // 洗黃瓜
    public void wash(Cooking_01 cooking) {
        cooking.wash(name);
    }
    // 切黃瓜
    public void cut(Cooking_01 cooking) {
        cooking.cut(name);
    }
    // 涼拌黃瓜
    public void coldMix(Cooking_02 cooking) {
        cooking.coldMix(name);
    }
}
// 炒黃瓜
static class FryCucumber {
    // 洗黃瓜
    public void wash(Cooking_01 cooking) {
        cooking.wash(name);
    }
    // 切黃瓜
    public void cut(Cooking_01 cooking) {
        cooking.cut(name);
    }
    // 炒黃瓜
    public void fry(Cooking_03 cooking) {
        cooking.fry(name);
    }
}

3.2.3 總結

​ 1.提高程式碼可讀性,可重用性,可維護性

​ 3.降低類的複雜度,降低耦合性

3.3 依賴倒置 / 倒轉原則(Dependency Inversion Principle,DIP)

3.3.1 解釋

​ 1. 依賴倒置/倒轉的核心是面向介面程式設計

​ 2. 相對於細節的多變性,抽象的東西相對穩定得多;以抽象為基礎的架構比以細節為基礎的架構穩定得多;而在Java中抽象是指抽象類和介面細節是指抽象類和介面的具體實現

​ 3. 抽象類和介面可以理解為制定規範,但不涉及任何具體操作,把展現細節的工作交給他們具體的實現類完成,以此來提高系統的可靠性和可維護性

3.3.2 舉例

​ 以付款場景為例

3.3.2.1 Demo1

4.1.2 抽象類

// 接收方
public abstract class Receiver {
    // 訊息
    private String message;
    // 獲取接收型別
    protected abstract String getType();
    public String getMessage() {
        return message;
    }
}

​ 抽象類與具體類基本相同,只是矩形第一層的抽象類名是斜體的,成員方法中的抽象方法也是斜體的

4.1.3 介面

// 形狀
public interface Shape {
    // 獲取尺寸
    int getSize();
}

​ 介面在矩形的第一層中第一行為 <<Interface>>*做介面標識第二行為介面名

4.2 UML類圖表示關係

4.2.1 依賴關係(Dependency)

​ 簡單來說,只要類中用到了另一個類,他們之間就存在了依賴關係(UML類圖中依賴關係用帶虛線的箭頭表示)

public class UserService {
    // 成員變數
    private UserDao userDao;
    // 方法引數
    public void insert(User user) {}
    // 方法返回值
    public Role getRole(Long id) {
        return null;
    }
    // 方法區域性變數
    public void update() {
        Dept dept = new Dept();
    }
}

類中用到了另一個類包括以上幾種情況:

  1. 類的成員變數
  2. 類中方法的引數
  3. 類中方法的返回值
  4. 類的方法中使用到(區域性變數)

4.2.2 泛化關係(Generalization)

​ 泛化關係實際上就是繼承關係,是依賴關係的特例(在UML類圖中用帶空心三角的實線表示)

public class User extends Person {}

4.2.3 實現關係(Realization / Implementation)

​ 實現關係是指類A實現了介面B,是依賴關係的特例(在UML類圖中用帶空心三角的虛線表示)

public class UserServiceImpl implements IUserService{}

4.2.4 關聯關係(Association)

​ 關聯關係是類與類之間存在的聯絡,是依賴關係的特例(在UML類圖中用帶雙箭頭的實線或者不帶箭頭的雙實線表示雙向關聯,用帶單箭頭的實線表示單向關聯

​ 關聯具有導航性單向關聯雙向關聯

​ 關聯具有多重性一對一多對一多對多

  1. 數位:精確的數量
  2. *或者0..*:表示0到多個
  3. 0..1:表示0或者1個
  4. 1..*:表示1到多個

// 單向一對一關聯
public class Association_01 {
    // 人
    static class Person {
        private IDCard idCard;
    }
    // 身份證
    static class IDCard {
    }
}

// 雙向一對一關聯
public class Association_02 {
    // 人
    static class Person {
        private IDCard idCard;
    }
    // 身份證
    static class IDCard {
        private Person person;
    }
}

// 單向多對一關聯
public class Association_03 {
    // 人
    static class Person {
        private List<BankCard> bankCardList;
    }
    // 銀行卡
    static class BankCard {
    }
}

// 雙向多對一關聯
public class Association_04 {
    // 人
    static class Person {
        private List<BankCard> bankCardList;
    }
    // 銀行卡
    static class BankCard {
        private Person person;
    }
}

// 多對多關聯
public class Association_05 {
    // 使用者
    static class User {
        private List<Role> roleList;
    }
    // 角色
    static class Role {
        private List<User> userList;
    }
}

4.2.5 聚合關係(Aggregation)

​ 聚合關係是指整體與部分的關係,整體和部分可以分開(比如電腦和滑鼠,滑鼠是電腦的一部分,可以分開),是關聯關係的特例,所以同樣具有導航性多重性(在UML類圖中用空心菱形加實線箭頭表示空心菱形在整體一方箭頭指向部分一方,表示把部分聚合到整體中來)

// 聚合關係
public class Aggregation_01 {
    // 人
    static class Person {
        private IDCard idCard;
    }
    // 身份證
    static class IDCard {
    }
}

4.2.6 組合關係(Composition)

​ 組合關係是指整體與部分的關係,整體和部分不可以分開(比如人的身體和頭,頭是身體的一部分,不可以分開),是關聯關係的特例,所以同樣具有導航性多重性(在UML類圖中用實心菱形加實線箭頭表示,實心菱形在整體一方,箭頭指向部分一方,表示把部分組合到整體中來)

// 組合關係
public class Composition_01 {
    // 人
    static class Person {
        private IDCard idCard = new IDCard();
    }
    // 身份證
    static class IDCard {
    }
}

4.3 UML類圖畫圖軟體

​ 我使用的是draw.io(線上版本、PC版都有),支援多種語言

​ 下載連結:https://github.com/jgraph/drawio-desktop/releases

​ 主介面如下:

5、設計模式的型別

​ 以下羅列了常見的23中設計模式:

建立型模式:

  1. 單例模式
  2. 工廠模式
  3. 抽象工廠模式
  4. 原型模式
  5. 建造者模式

結構型模式:

  1. 介面卡模式
  2. 橋接模式
  3. 裝飾模式
  4. 組合模式
  5. 外觀模式
  6. 享元模式
  7. 代理模式

行為型模式:

  1. 模板方法模式
  2. 命令模式
  3. 存取者模式
  4. 迭代器模式
  5. 觀察者模式
  6. 中介者模式
  7. 備忘錄模式
  8. 直譯器模式(Interpreter模式)
  9. 狀態模式
  10. 策略模式
  11. 職責鏈模式(責任鏈模式)

6、相關原始碼

​ 本篇章完整程式碼:https://github.com/yushixin-1024/DesignPattern

PS:程式碼不涉及具體業務邏輯,僅僅是為了舉例,方便理解。