軟件設計的六大原則剖析

我們平時編寫代碼時,很少有人爲了刻意迎合軟件設計原則而編寫。其實,有時候是你用到了其中的某個或多個設計原則,而不自知而已。也有可能是有的人壓根就不知道設計原則是什麼。

不過,沒關係,爲了搞明白既抽象又玄幻的六大準則,我總結了一句話來概括每一種設計原則所體現的主要思想。

里氏替換原則是指繼承時不要破壞父類原有的功能;依賴倒置原則是指要面向接口編程;開閉原則是指對擴展是開放的,對修改是關閉的;職責單一原則是指實現類的職責要單一;接口隔離原則是指設計的接口要儘量簡單,專一;迪米特法則是指要降低類之間的耦合度。

下面一一介紹六大類設計原則,看完之後,你會對上邊的總結有更深的理解。

一、里氏替換原則

里氏替換原則,乍一看名字,讓人摸不着頭腦。其實,這是一位姓裏的女士提出來的,因此用她的姓氏命名。里氏替換原則,通俗來講,就是指子類繼承父類時,可以擴展父類的功能,但是不要修改父類原有的功能。什麼意思呢,舉個例子。

//父類
public class Calculate {
    public int cal(int a,int b){
        return a   b;
    }
}
//子類
public class Calculate2 extends Calculate {
    public int cal(int a,int b){
        return a - b;
    }
}
//測試
public class TestCal {
    public static void main(String[] args) {
        Calculate2 cal2 = new Calculate2();
        int res = cal2.cal(1, 1);
        System.out.println("1 1=" res); // 1 1 = 0
    }
}

子類繼承了父類之後,想實現新功能,卻沒有擴展新方法,而是重寫了父類的cal方法,因此導致結果 1 1=0. 這就違反了里氏替換原則。

應該把子類Calculate2修改爲,添加一個新方法cal2來實現相減功能

public class Calculate2 extends Calculate {
    public int cal2(int a, int b){
        return a - b;
    }
}

public class TestCal {
    public static void main(String[] args) {
        Calculate2 cal2 = new Calculate2();
        int res = cal2.cal2(1, 1);
        System.out.println("1-1=" res); // 1-1=0
    }
}

有心的人可能會發現,里氏替換原則規定子類不能重寫父類的方法。這不是和麪向對象中的三大特徵之一“多態”衝突嗎,多態實現的一個重要前提就是子類繼承父類並重寫父類的方法啊。

其實,剛開始學習里氏替換原則,我也產生了這樣的疑惑。後來查了很多資料,才明白,子類不應該去重寫父類已經實現的方法(非抽象方法),而是去實現父類的抽象方法。也就是說,儘量要基於抽象類和接口的繼承,而不是基於可實例化的父類繼承。關於這一點的解釋,可以看這篇文章,我感覺總結的挺不錯的:https://www.jianshu.com/p/e6a7bbde8844?utm_campaign

二、單一職責原則

簡單來說,就是要控制類的粒度大小,降低類的複雜度,一個類只負責一項職責。

例如,在研發一個產品新功能時。需要項目經理接需求,評估工作量,然後分發任務給程序員。程序員,根據需求編寫代碼,然後自測。各司其職,才能保證項目穩定向前推進。其類圖如下file

另外,單一職責原則也適用於方法,一個方法只做一件事。

三、依賴倒置原則

依賴倒置原則的定義爲:高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。其實,就是在說要面向抽象,面向接口編程。

舉個栗子,如果一個學生去學習歷史知識,只需要把歷史書給他就可以了

public class History {
    public String getKnowledge(){
        return "歷史知識";
    }
}

public class Student {
    public void study(History history){
        System.out.println("學習"   history.getKnowledge());
    }
}

public class Test {
    public static void main(String[] args) {
        Student stu = new Student();
        stu.study(new History()); //學習歷史知識
    }
}

但是,如果他需要學習地理知識呢,我們需要把History改爲Geography,然後修改study方法的參數類型爲Geography

public class Geography {
    public String getKnowledge(){
        return "地理知識";
    }
}

public class Student {
    public void study(Geography geography){
        System.out.println("學習"   geography.getKnowledge()); 
    }
}
//學習地理知識

雖然,這樣實現也是可以的,但是通用性太差,類之間的耦合度太高了。設想,如果該學生又要學習數學知識呢,語文呢,英語呢,是不是每次都要修改study方法。這樣的設計不符合依賴倒置原則,應該把各個學科知識抽象出來,定義一個接口IKnowledge,然後每個學科去實現這個接口,而study方法的參數傳一個固定類型IKnowledge就可以了。

public interface IKnowledge {
    String getKnowledge();
}

public class History implements IKnowledge{
    public String getKnowledge(){
        return "歷史知識";
    }
}

public class Geography implements IKnowledge{
    public String getKnowledge(){
        return "地理知識";
    }
}

public class Student {
    public void study(IKnowledge iKnowledge){
        System.out.println("學習"   iKnowledge.getKnowledge());
    }
}

public class Test {
    public static void main(String[] args) {
        Student stu = new Student();
        stu.study(new History());  //學習歷史知識
        stu.study(new Geography());  //學習地理知識
    }
}

這樣的話,如果需要再學習英語知識,只需要定義一個English類,去實現IKnowledge接口就可以了。 這就是依賴倒置原則的面向接口編程。

它們之間的類圖關係如下file

四、接口隔離原則

接口隔離原則的定義:客戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。

什麼意思呢,就是說設計接口的時候,不要把一大堆需要實現的抽象方法都定義到同一個接口中,應該根據不同的功能,來拆分成不同的接口。我們知道,實現類去實現接口的時候,需要實現所有的抽象方法。如果接口中有某些不需要的方法,也需要實現,但是方法體卻是空的,這樣完全沒有意義。

例如,我定義一個Animal的接口,用獅子去實現接口

public interface Animal {
    void eat();
    void fly();
    void run();
}

public class Lion implements Animal {
    @Override
    public void eat() {
        System.out.println("獅子吃肉");
    }

    @Override
    public void fly() {

    }

    @Override
    public void run() {
        System.out.println("獅子奔跑");
    }
}

很明顯,獅子是不會飛的,fly方法的方法體是空的。這樣設計,不符合接口隔離原則。因此,我們把接口進行拆分,拆分爲Animal,IFly,IRun三個接口,讓獅子選擇性實現。

public interface Animal {
    void eat();
}

public interface IFly {
    void fly();
}

public interface IRun {
    void run();
}

public class Lion implements Animal,IRun {
    @Override
    public void eat() {
        System.out.println("獅子吃肉");
    }

    @Override
    public void run() {
        System.out.println("獅子奔跑");
    }
}
// 獅子只需要實現吃的方法和奔跑的方法就可以了,不需要實現IFly接口。

可以發現,接口隔離原則和職責單一原則非常之相似,但其實是不同的。職責單一原則主要是約束類,針對的是具體的實現,強調類職責的單一。而接口隔離原則主要是約束接口的,注重的是高層的抽象和對接口依賴的隔離。

另外,需要注意,接口設計的過細也不太好,會增大系統的複雜度。想象一下,你爲了實現某些功能,卻需要實現十幾個接口的場景是多崩潰吧。因此需要適度地進行接口拆分。

五、迪米特法則

迪米特法則定義:一個對象應該對其他對象保持最少的瞭解。什麼意思呢,就是說要儘量降低類之間的耦合度,提高類的獨立性,這樣當一個類修改的時候,對其他類的影響也會降到最低。

通俗點講,就是一個類對它依賴的類知道的越少越好。對於被依賴的類來說,不管內部實現多複雜,只需給其他類暴露一個可以調用的公共方法。

舉個簡單的例子。當公司老闆需要下發一個任務時,不會直接把每個員工都叫到一起,給每個人分配具體的任務。而是先召集各部門經理給他們發佈任務,然後部門經理再給下邊員工分派任務。老闆只需要監督部門經理即可,不需要關心部門經理給每個員工分配的任務具體是什麼。

用代碼可以這樣表示

public class Employee {
    public void doTask(){
        System.out.println("員工執行任務");
    }
}

public class DeptManager {
    public void task(){
        System.out.println("部門領導發佈任務");
        Employee employee = new Employee();
        employee.doTask();
    }
}

public class Boss {
    private DeptManager deptMgr;

    public void setDeptMgr(DeptManager mgr){
        this.deptMgr = mgr;
    }

    public void task(){
        System.out.println("老闆發佈任務");
        deptMgr.task();
    }
}

public class TestD {
    public static void main(String[] args) {
        Boss boss = new Boss();
        boss.setDeptMgr(new DeptManager());
        boss.task();
    }
}
//老闆發佈任務
//部門領導發佈任務
//員工執行任務

這樣,老闆跟具體的每個員工就沒有任何直接聯繫,降低了耦合度。

可以看到,其實部門經理在這其中充當了中介的作用,用於建立老闆和員工之間的聯繫。需要注意,要適度的使用中介,如果中介太多,就會導致系統複雜度太高,通訊的效率降低。就如同一個公司,部門越多,級別層級越多,越不容易管理,溝通成本增加,執行任務的效率下降。因此,需要合理設計中介類。

六、開閉原則

開閉原則定義:對擴展是開放的,對修改是關閉的。

其實,這句話就體現了封裝,繼承和多態的思想。一個實體類,如果已經實現了原有的功能,就不應該再對其進行修改,需要的話應該對其進行功能擴展。這句話聽起來是不是跟里氏替換原則特別像。其實,開閉原則更像是對其他幾個原則的總結,最終要達到的目的就是用抽象構建高層模塊,用實現擴展具體的細節。

里氏替換原則和依賴倒置原則告訴你應該對類和方法進行抽象。單一職責和接口隔離告訴你應該怎樣做抽象才合理,迪米特法則告訴你具體實現怎樣做才能做到高內聚,低耦合。

其實,六大設計原則就規定了一些規則,它告訴你按照這樣做更好,但是如果你非要不遵守規則,也不是不行,代碼照樣可以跑,只不過是增加了代碼出問題的概率,健壯性也不好,可維護性不高。這就像我們生活中的很多規則,如過馬路,需要看紅綠燈。但是,你非要不看,硬闖紅燈,也沒人能把你怎樣,不過是增加了你被撞的概率而已。所以,遵守規則,能最大限度降低我們的損失。

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