天天看點

軟體設計的六大原則剖析

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

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

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

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

一、裡氏替換原則

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

//父類
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
    }
}
      

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

其實,剛開始學習裡氏替換原則,我也産生了這樣的疑惑。後來查了很多資料,才明白,子類不應該去重寫父類已經實作的方法(非抽象方法),而是去實作父類的抽象方法。也就是說,盡量要基于抽象類和接口的繼承,而不是基于可執行個體化的父類繼承。關于這一點的解釋, ​

二、單一職責原則

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

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

軟體設計的六大原則剖析

另外,單一職責原則也适用于方法,一個方法隻做一件事。

三、依賴倒置原則

依賴倒置原則的定義為:高層子產品不應該依賴低層子產品,二者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。其實,就是在說要面向抽象,面向接口程式設計。

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

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接口就可以了。 這就是依賴倒置原則的面向接口程式設計。

它們之間的類圖關系如下

軟體設計的六大原則剖析

四、接口隔離原則

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

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

例如,我定義一個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();
    }
}
//老闆釋出任務
//部門上司釋出任務
//員工執行任務
      

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

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

六、開閉原則

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

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

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