設計模式原則(上)

參考鏈接:https://www.imooc.com/read/53/article/1078

1、基本概念

本節開始介紹設計模式的七大原則的基本概念,其中包括開閉原則、單一職責原則、里氏替換原則、依賴倒置原則,剩下的三大原則(接口隔離原則、迪米特法則、合成複用原則)會在下一節繼續講解。

本節以介紹基本概念爲主,其中會加入部分演示代碼、uml 類圖講解,能理解基本概念即可。後續章節設計模式的講解會詳細介紹這些原則的應用。

本節主要內容有:

  • 什麼是單一職責原則、里氏替換原則及依賴倒置原則

  • 爲何要遵循這些原則

2.開閉原則:

開閉原則(Open Closed Principle,OCP)由勃蘭特・梅耶(Bertrand Meyer)提出,他在 1988 年的著作《面向對象軟件構造》(Object Oriented Software Construction)中提出:軟件實體應當對擴展開放,對修改關閉(Software entities should be open for extension,but closed for modification),這就是開閉原則的經典定義。

開閉原則是設計模式中的總原則,開閉原則就是說:對拓展開放、對修改關閉。 模塊應該在儘量不修改代碼的前提下進行拓展,這就需要使用接口和抽象類來實現預期效果。

我們舉例說明什麼是開閉原則,以 4s 店銷售汽車爲例,其類圖如圖所示:

圖片描述
ICar 接口定義了汽車的兩個屬性:名稱和價格。BenzCar 是一個奔馳車的實現類,代表所有奔馳車的總稱。Shop4S 代表售賣的 4s 店,ICar 接口的代碼清單如下:

public interface ICar {
    // 車名
    public String getName();
    // 車價格
    public int getPrice();
}

一般情況下 4s 店只售出一種品牌車,這裏以梅賽德斯奔馳爲例,奔馳車類如代碼清單所示:

public class BenzCar implements ICar{
    // 車名
    private String name;
    // 車價格
    private int price;

    // 通過構造方法實例化
    public BenzCar(String _name, int _price) {
        this.name = _name;
        this.price = _price;
    }

    // 獲取車名
    @Override
    public String getName() {
        return this.name;
    }
    // 獲取車價格
    @Override
    public int getPrice() {
        return this.price;
    }
}

然後我們模擬下 4s 店售車記錄,Shop4S 類代碼清單如下所示:

import java.util.ArrayList;

public class Shop4S {

    private final static ArrayList<ICar> carList = new ArrayList<ICar>();
    // 使用static代碼塊模擬數據初始化操作
    static {
        carList.add(new BenzCar("梅賽德斯-邁巴赫S級轎車",138));
        carList.add(new BenzCar("梅賽德斯-AMG S 63 L 4MATIC+", 230));
        carList.add(new BenzCar("梅賽德斯-奔馳V級", 50));
    }

    public static void main(String[] args) {
        System.out.println("4s店售車記錄:");
        for (ICar car: carList){
            System.out.println("車名:" + car.getName() + "\t價格:" + car.getPrice() + "萬元");
        }
    }

}

在 Shop4S 類中,使用 static 代碼塊來模擬數據初始化過程,使用私有變量集合 carList 來記錄所有售出車輛信息,一般項目中這部分都由持久化曾來完成,運行效果如下:

4s店售車記錄:
車名:梅賽德斯-邁巴赫S級轎車 價格:138萬元
車名:梅賽德斯-AMG S 63 L 4MATIC+  價格:230萬元
車名:梅賽德斯-奔馳V級  價格:50萬元

暫時來看,以上設計是沒有啥問題的。但是,某一天,4s 店老闆說奔馳轎車統一要收取一筆金融服務費,收取規則是價格在 100 萬元以上的收取 5%,50~100 萬元的收取 2%,其餘不收取。爲了應對這種需求變化,之前的設計又該如何呢?

目前,解決方案大致有如下三種:

  • 修改 ICar 接口:在 ICar 接口上加一個 getPriceWithFinance 接口,專門獲取加上金融服務費後的價格信息。這樣的後果是,實現類 BenzCar 也要修改,業務類 Shop4S 也要做相應調整。ICar 接口一般應該是足夠穩定的,不應頻繁修改,否則就失去了接口鍥約性了。
  • 修改 BenzCar 實現類:直接修改 BenzCar 類的 getPrice 方法,添加金融服務費的處理。這樣的一個直接後果就是,之前依賴 getPrice 的業務模塊的業務邏輯就發生了改變了,price 也不是之前的 price 了。
  • 使用子類拓展來實現:增加子類 FinanceBenzCar,覆寫父類 BenzCar 的 getPrice 方法,實現金融服務費相關邏輯處理。這樣的好處是:只需要調整 Shop4S 中的靜態模塊區中的代碼,main 中的邏輯是不用做任何修改的。

修改後的 FinanceBenzCar 類代碼清單如下:

public class FinanceBenzCar extends BenzCar{
    public FinanceBenzCar(String _name, int _price) {
        super(_name, _price);
    }

    // 覆寫價格信息
    @Override
    public int getPrice() {
        // 獲取原價
        int selfPrice = super.getPrice();
        int financePrice = 0;
        if (selfPrice >= 100) {
            financePrice = selfPrice + selfPrice * 5 / 100; // 收取5%的金融服務費
        } else if (selfPrice >= 50) {
            financePrice = selfPrice + selfPrice * 2 / 100; // 收取2%的金融服務費
        } else {
            financePrice = selfPrice; // 其餘不收取服務費
        }
        return financePrice;
    }
 }

再來看看修改後的 Shop4S 類代碼清單如下:

import java.util.ArrayList;

public class Shop4S {

    private final static ArrayList<ICar> carList = new ArrayList<ICar>();
    private final static ArrayList<ICar> financeCarList = new ArrayList<ICar>();
    // 使用static代碼塊模擬數據初始化操作
    static {
        carList.add(new BenzCar("梅賽德斯-邁巴赫S級轎車",138));
        carList.add(new BenzCar("梅賽德斯-AMG S 63 L 4MATIC+", 230));
        carList.add(new BenzCar("梅賽德斯-奔馳V級", 50));
        financeCarList.add(new FinanceBenzCar("梅賽德斯-邁巴赫S級轎車",138));
        financeCarList.add(new FinanceBenzCar("梅賽德斯-AMG S 63 L 4MATIC+", 230));
        financeCarList.add(new FinanceBenzCar("梅賽德斯-奔馳V級", 50));
    }

    public static void main(String[] args) {
        System.out.println("4s店售車記錄(不含金融服務費):");
        for (ICar car: carList){
            System.out.println("車名:" + car.getName() + "\t價格:" + car.getPrice() + "萬元");
        }
        System.out.println("\n4s店售車記錄(包含金融服務費):");
        for (ICar car: financeCarList) {
            System.out.println("車名:" + car.getName() + "\t價格:" + car.getPrice() + "萬元");
        }
    }

}

運行效果如下:

4s店售車記錄(不含金融服務費):
車名:梅賽德斯-邁巴赫S級轎車 價格:138萬元
車名:梅賽德斯-AMG S 63 L 4MATIC+  價格:230萬元
車名:梅賽德斯-奔馳V級  價格:50萬元

4s店售車記錄(包含金融服務費):
車名:梅賽德斯-邁巴赫S級轎車 價格:144萬元
車名:梅賽德斯-AMG S 63 L 4MATIC+  價格:241萬元
車名:梅賽德斯-奔馳V級  價格:51萬元

這樣,在業務規則發生改變的情況下,我們通過拓展子類及修改持久層(高層次模塊)便足以應對多變的需求。開閉原則要求我們儘可能通過拓展來實現變化,儘可能少地改變已有模塊,特別是底層模塊。

開閉原則總結:

  • 提高代碼複用性
  • 提高代碼的可維護性   

3.單一職責原則

單一職責原則,簡單得來說就是保證設計類、接口、方法時做到功能單一,權責明確。

怎麼理解呢?比如應用開發時經常會有修改用戶信息的接口,如下:圖片描述這裏我們定義 “更新用戶” 的接口,倘若有一天新來的前端要求加一個修改用戶密碼的接口,後端直接說:“你去調 updateUser ” 接口吧,傳入密碼信息就行。這種後端往往不是太懶就是新手,updateUser 接口的粒度太粗,接口職責不夠單一,所以應該將接口拆分爲各個細分接口,比如修改如下:

圖片描述

這裏很明顯,我們看到分拆後的接口職責更加單一,權責更加清楚,日後維護開發也更加便捷。

單一職責原則,指的是一個類或者模塊有且只有一個改變的原因。 如果模塊或類承擔的職責過多,就等於這些職責耦合在一起,這樣一個模塊的變快可能會削弱或抑制其它模塊的能力,這樣的耦合是十分脆弱地。所以應該儘量保持單一職責原則,此原則的核心就是解耦和增強內聚性。

在現在流行的微服務架構體系中,最頭疼的就是服務拆分,拆分的粒度也很有講究,標準的應該是遵從單一原則,避免服務拆分時發生各種撕逼行爲:” 本應該在 A 服務中的被安排在了 B 服務中 “,所以服務的職責劃分尤爲重要。

再有就是,做 service 層開發時,早期的開發人員會將數據庫操作放在 service 中,比如 getConnection,然後執行 prepareStatement,再就是 service 邏輯處理等等。可是後來發現數據庫要由原來的 mysql 變更爲 oracle,service 層代碼豈不是需要重寫一遍,天了嚕… 直接崩潰跑路。

” 我單純,所以我快樂 “用來形容單一職責原則再恰當不過了。

單一職責原則總結:

  • 單一職責可以降低類的複雜性,提高代碼可讀性、可維護性

  • 但是用 “職責” 或 “變化原因” 來衡量接口或類設計得是否優良,但是 “職責” 和 “變化原因” 都是不可度量的,因項目、環境而異;指責劃分稍微不當,很容易造成資源浪費,代碼量增多,好比微服務時服務邊界拆分不清

4.里氏替換原則

里氏替換原則的解釋是,所有引用基類的地方必須能透明地使用其子類的對象。 通俗來講的話,就是說,只要父類能出現的地方子類就可以出現,並且使用子類替換掉父類的話,不會產生任何異常或錯誤,使用者可能根本就不需要知道是父類還是子類。反過來就不行了,有子類的地方不一定能使用父類替換。

比如某個方法接受一個 Map 型參數,那麼它一定可以接受 HashMap、LinkedHashMap 等參數,但是反過來的話,一個接受 HashMap 的方法不一定能接受所有 Map 類型參數。

里氏替換原則是開閉原則的實現基礎,它告訴我們設計程序的時候儘可能使用基類進行對象的定義及引用,具體運行時再決定基類對應的具體子類型。

接下來舉個栗子,我們定義一個抽象類 AbstractAnimal 對象,該對象聲明內部方法 “跳舞”,其中,Rabbit、Dog、Lion 分別繼承該對象,另外聲明一個 Person 類,該類負責餵養各種動物,Client 類負責邏輯調用,類圖如下:
圖片描述
其中,Person 類代碼如下:

public class Person {
    private AbstractAnimal animal;

    public void feedAnimal(AbstractAnimal _animal) {
        this.animal = _animal;
    }

    public void walkAnimal(){
        System.out.println("人開始溜動物...");
        animal.dance();
    }
}

main 函數調用的時候如下:

public class Main {

    public static void main(String[] args) {
        Person person = new Person();
        person.feedAnimal(new Rabbit());
        person.walkAnimal();
    }
}

打印輸出:

人開始溜動物…
小白兔跳舞…

這裏,Person 類中本該出現的父類 AbstractAnimal 我們運行時使用具體子類代替,只要是父類能出現的地方子類就能出現,這就要求我們模塊設計時儘量以基類進行對象的定義及應用。

里氏替換原則總結:

  • 里氏替換可以提高代碼複用性,子類繼承父類時自然繼承到了父類的屬性和方法

  • 提高代碼可拓展性,子類通過實現父類方法進行功能拓展,個性化定製

  • 里氏替換中的繼承有侵入性。繼承,就必然擁有父類的屬性和方法

  • 增加了代碼的耦合性。父類方法或屬性的變更,需要考慮子類所引發的變更

5.依賴倒置原則

依賴倒置原則的定義:程序要依賴於抽象接口,不要依賴於具體實現。簡單的說就是要求對抽象進行編程,不要對實現進行編程,這樣就降低了客戶與實現模塊間的耦合。

依賴倒置原則要求我們在程序代碼中傳遞參數時或在關聯關係中,儘量引用層次高的抽象層類,即使用接口和抽象類進行變量類型聲明、參數類型聲明、方法返回類型聲明,以及數據類型的轉換等,而不要用具體類來做這些事情。

依賴倒置原則,高層模塊不應該依賴低層模塊,都應該依賴抽象。抽象不應該依賴細節,細節應該依賴抽象。其核心思想是:要面向接口編程,不要面向實現編程。

舉個栗子,拿顧客商店購物來說,定義顧客類如下,包含一個 shopping 方法:

public class Customer {
    public void shopping (YanTaShop shop) {
        System.out.println(shop.sell());
    }
}

以上表示顧客在 "雁塔店" 進行購物,假如再加入一個新的店鋪 "高新店",表示修改如下:

public class Customer {
    public void shopping (GaoXinShop shop) {
        System.out.println(shop.sell());
    }
}

這顯然是設計不合理的,違背了開閉原則。同時,顧客類的設計和店鋪類綁定了,違背了依賴倒置原則。解決辦法很簡單,將 Shop 抽象爲具體接口,shopping 入參使用接口形式,顧客類面向接口編程,如下:

public class Customer {
    public void shopping (Shop shop) {
        System.out.println(shop.sell());
    }
}

interface Shop{
    String sell();
}

類圖關係如下:
圖片描述
依賴倒置原則總結:

  • 高層模塊不應該依賴低層模塊,都應該依賴抽象(接口或抽象類)

  • 接口或抽象類不應該依賴於實現類

  • 實現類應該依賴於接口或抽象類

 

6、總結

本節介紹了設計模式的幾個原則,分別是開閉原則、單一職責原則、里氏替換原則、依賴倒置原則,重在理解即可,下節我們還會再介紹剩餘幾個原則。
圖片描述

 

點擊下載本文源碼

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