設計模式(06)——設計原則(1)

iceWang丶發表於2020-12-01

設計原則

設計原則,是設計模式的內功心法,基本所有的設計模式都是基於設計原則進行的具體化,如果說設計模式是如何操作的話,那麼設計原則就是為何這麼做的基石,因此,只要我們能充分理解設計原則,那麼在此基礎上,對設計模式就能更好的理解,甚至能自己設計出一種設計模式來。

單一職責原則

定義

一個類或模組,只需要完成一個範圍的功能,而不要搞得大而全。

場景

例如我們設計一個社交網站,現在要儲存使用者資訊,類設計如下:

public class UserInfo {
    private String name;
    private String like;

    private String location;
}

現在,我們想想該類的設計是否符合單一職責原則?


答案是可能符合,也可能不符合。那麼判斷依據是什麼呢?


原因就是類或模組職責的判斷是根據業務來定的,並沒有一個普遍認同的規則。例如,如果該需要要的網站還提供賣東西的功能,那麼使用者的地址資訊,就是一個十分關鍵的資訊,且該塊功能就需要抽離出來作為一個單獨的模組,此時,地址資訊放在這裡就不合適了,違反了單一職責原則。


但如果地址資訊,只是一個值物件,也就是說其只是一個展示屬性,那麼放在這裡就是合適的。


綜上所述,可以看到單一職責原則並不是設計出來就一成不變的,其需要結合業務發展的具體情況來判斷。因此我們在設計之初,可以考慮一個大而全的類,但隨著業務的發展需要,需要持續不斷的進行優化(也就是持續重構的思想)。

用處

單一職責,因為類的設計比較小而精,因此可以極大提高程式碼的可維護性和可讀性。


此外因為每個類或模組只涉及自己的功能部分,因此,也做到了高內聚。

其他

但類的設計也不是越單一越好,因為如果拆分的過細的話,可能上層一個類需要修改,會導致下層所有依賴其的類都要修改,又影響了程式碼的可維護性,因此還是要根據業務需要來合理評估,重點是感覺要對。

開閉原則

定義

字面意思,一個類的設計,應該要對擴充開放,對修改關閉。因此這裡的重點就是以下定義該如何判斷:

  • 什麼樣的程式碼修改是擴充;
  • 什麼樣的程式碼修改是修改;
  • 修改程式碼就一定是違反了該原則嗎

場景

    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.consume(1);
    }

    // 根據傳遞過來的級別來進行不同的會員邏輯判斷
    public void consume(int type) {
        if (type == 1) {
            Console.log("您好,1級會員!");
        }
        if (type == 2) {
            Console.log("您好,2級會員!");
        }
    }

現在,又提出一個新的需求,還需要根據對應的會員等級進行對應的金額扣除,如果是上述的設計方式,那麼修改的方式則是下面這樣:

    public void consume(int type, int price) {
        if (type == 1) {
            Console.log("您好,1級會員,扣除金額{}", price);
        }
        if (type == 2) {
            Console.log("您好,2級會員,扣除金額{}", price);
        }
    }

很明顯,這樣的方式有問題,如果還要再傳遞一個欄位,例如優惠比例,那麼依照該方案,則還需要修改介面定義,這就意味著呼叫方都需要修改,測試用例也需要對應的修改。

那麼如果按照開閉原則的話,該如何設計呢?


首先我們對程式碼進行下重構

// 將所有相關屬性封裝起來
public class Vip {
    private int type;
    private int price;
    private int radio;
}

針對每種處理方式,根據他們的公有行為抽象出一個抽象層:

public interface VipHandler {
    void consume(Vip vip);
}


每種特殊處理方式實現對應的抽象:

public class FirstVipHandler implements VipHandler {
    @Override
    public void consume(Vip vip) {
        if (vip.getType() == 1) {
            Console.log("您好,1級會員,扣除金額{}", vip.getPrice() * vip.getRadio());
        }
    }
}

public class SecondVipHandler implements VipHandler {
    @Override
    public void consume(Vip vip) {
        if (vip.getType() == 2) {
            Console.log("您好,2級會員,扣除金額{}", vip.getPrice() * vip.getRadio());
        }
    }
}


通過這樣的處理方式,在每次接到新的任務後,就不需要重新修改原有的邏輯方法,可以直接進行擴充即可:

    // 根據傳遞過來的級別來進行不同的會員邏輯判斷
    public void consume(Vip vip, VipHandler vipHandler) {
        vipHandler.consume(vip);
    }

其他

可以看到即使是上述的方式來擴充程式碼,仍舊會修改原有程式碼,那麼這種方式是違反了開閉原則嗎?


在這裡,我們判斷其並符合了開閉原則,因為我們判斷是修改還是擴充,並不能只是簡單的根據看是否修改了原有程式碼,真正核心的關鍵問題應該是:

  • 改動沒有破壞原有程式碼的正常執行;
  • 改動沒有破壞原有單元測試

**
在上述的修改後,我們如果加一種特殊的情況,並沒有修改到原先的處理邏輯類,這也就意味著原先的程式碼不會引入一些可能的 bug,針對原始程式碼的測試用例也還是可以照常的進行編寫,而不用再根據新的改動而進行改動。

用途

開閉原則的關鍵點是程式碼的可擴充性,即如何快速的擁抱變化,當每次新的任務來後,不必修改原始程式碼,而直接在原有的基礎上進行擴充即可。


關閉修改是保持原有程式碼的穩定性

里氏替換原則

定義

子類物件可以代替父類物件出現的任何地方,並保證原來程式邏輯行為不被破壞。

因為要保證子類物件不能破壞原有程式邏輯行為,因此該方式跟多型的區別是:
如果子類進行了重寫,並在重寫的邏輯中加入了跟父類對應方法不同的邏輯,那麼該方式可以稱之為多型,但就不符合里氏替換原則了。

用途

該原則最重要的作用是指導子類的設計,保證在替換父類的時候,不改變原有程式的邏輯行為。


在這裡,重點是邏輯行為的不改變,這就意味著,我們可以對實現的細節進行修改,只要保證業務含義不變,那麼就是符合里氏替換原則的。


因此,針對這種情況,有一種用途是可以 改進 原有的實現,例如原先採用的排序演算法比較低效,那麼可以設計一個子類,然後重寫對應排序演算法,保證邏輯不發生變化。重點是,我們做的是改進,不管如何改,排序的業務含義是不變的。


實現方式,則是按照協議進行程式設計,關鍵是子類重寫過程中,要保證不破壞原有函式宣告的輸入、輸出、異常以及註釋中羅列的任何特殊情況宣告。


介面隔離原則

定義

首先,我們要對介面進行定義,明確其特殊含義,對於介面來說,我們將其分為兩種型別的表現形式。

  • 一種語法定義,其代表了一組方法的集合;
  • 向外暴露的單個API介面或函式實現


下面,我們分別對其進行介紹。

場景:一組API聚合

public interface UserInfoService {
    boolean login();
    void getUserInfoByPhone();
    
    void deleteUserByPhone();
}

看上述這個介面定義是否符合介面隔離原則???


其實這跟單一職責原則一樣,也是要看業務發展的。例如,如果該介面是提供給後臺管理系統來使用的,那麼沒有問題,作為一個後臺系統的 admin 許可權人員當然可以有很多操作的能力。


但如果該介面是給第三方使用者來使用的話,就不是很合適了。因為刪除操作是一個高許可權能力,作為使用者來說,一般是沒有許可權做的,那麼在設計時,對應實現類就不應該實現全部介面內定義的方法,這就是介面隔離原則中所說的,不強制依賴介面中的所有方法。
**
而是,根據具體的定義,將其進行拆分,對應許可權的實現類實現對應的許可權行為。

場景:單個API介面或函式實現

public class Statistics {
    private int max;
    private int min;
    private int sum;
    //......

    public Statistics count(List<Integer> data){
        Statistics statistics = new Statistics();
        // 計算 Statistics 中的每個值
        return statistics;
    }
}

首先,我們來看上述方法定義是否符合介面隔離原則???


在這裡,我們還是要結合具體的業務場景才能做出結論,如果使用該函式的呼叫者,在大部分場景下都需要用到其中的大部分欄位,那麼該設計就是可以的。


但是如果每次只用到其中的幾個,那麼該設計就不合理了,其會浪費大量的無效計算能力,影響效能。在該場景下,就需要進行拆分。

    public int max(List<Integer> data) {
        return data.stream().max(Statistics::compare).get();
    }

    public int min(List<Integer> data) {
        return data.stream().min(Statistics::compare).get();
    }

依賴翻轉原則

對於依賴翻轉原則來說,有很多看著很像的定義,我們分別對其進行介紹,看看其都是什麼含義,他們之間又有什麼關聯。

控制翻轉(IOC)

控制翻轉,針對是在原有的程式設計流程中,整個程式的執行流程是直接交由程式設計師來控制的,但是如果使用控制翻轉的思想,則是在一個架子中,已經定義好了執行的流程,而只是預先定義好了擴充點,後續程式設計師所能修改的只有擴充點,開發人員在擴充點裡新增相關業務邏輯即可。

public abstract class VipProcess {
    public abstract boolean isVip();

    public void consume() {
        if (isVip()) {
            Console.log("vip hello");
        } else {
            Console.log("get out");
        }
    }
}


public class Vip extends VipProcess {
    @Override
    public boolean isVip() {
        return true;
    }

    public static void main(String[] args) {
        VipProcess vip = new Vip();
        vip.consume();
    }
}


public class CommonPeople extends VipProcess {
    @Override
    public boolean isVip() {
        return false;
    }

    public static void main(String[] args) {
        VipProcess vip = new CommonPeople();
        vip.consume();
    }
}

上述方式,就是通過模板方法來實現的控制翻轉,提供一個擴充的 isVip() 邏輯來交給程式設計師來實現,而框架根據實際實現的方法返回來決定下面的程式流轉。

依賴注入(DI)

依賴注入更好理解,一句話概括:不用程式設計師顯式通過 new 來建立物件,而是通過建構函式,函式傳遞的方式來傳遞物件。


即A類如果需要依賴B類,不是通過在 A 中 new 一個 B 出來,而是在外面建立好 B 後,傳遞給 A。通過這樣的方式,可以在需求改變中,靈活的替換傳遞引數(B 實現 C 介面的話)。


而更進一步,現在一些框架都提供了 DI 的功能,只需要簡單的配置一下相關類物件,所需引數等,框架就會自動接管物件的建立流程已經生命週期等。(AutoWired)

依賴翻轉(DIP)

定義:高層模組不依賴於底層模組,而是通過一個抽象層來解耦。


該定義其實在我們的平常業務開發中,不怎麼會用到,因為我們平常就是高層依賴著底層,例如 Controller 依賴 Service,Service 依賴 Repository,該原則的重點還是指導框架層面的開發。


例如 Tomcat ,我們知道 Tomcat 的執行是我們將程式寫完後,打成 war 包扔到對應目錄就可以啟動了,而 Tomcat 和應用程式就是通過一個共同的抽象 **Servlet **來關聯的。


Tomcat 不直接依賴於底層實現:Web 程式,而是跟 Web 都依賴於 Servlet,而 Servlet 不依賴於具體的Tomcat 實現的 Web 的具體細節。


通過該實現方式,在編碼中,可以靈活的進行替換,比如我們還是一個 web 程式,但是執行的容器不使用 tomcat 了,也可以無縫的進行切換,只要保證要替換的容器,還是依賴於 Servlet 規範即可。

相關文章