第一章 設計原則

1.開閉原則

一個軟體實體,像類,模組,函式應該對擴充套件開放,對修改關閉

 在設計的時候,要時刻考慮,讓這個類儘量的好,寫好了就不要去修改。如果有新的需求來,在增加一個類就完事了,原來的程式碼能不動就不動。這個原則的特性一個是對擴充套件開發,一個是對修改關閉。即面對需求,我們要做的是通過增加程式碼來完成,而不是更改現有的程式碼。這也是設計原則的基礎,精神所在

程式碼解釋:

建立一個ICourse介面,用javaCourse類去實現它。

/**
 * @program: designModel
 * @description: 課程介面
 * @author: YuKai Fan
 * @create: 2018-11-13 10:36
 **/
public interface ICourse {

    Integer getId();
    String getName();
    Double getPrice();
}
/**
 * @program: designModel
 * @description:
 * @author: YuKai Fan
 * @create: 2018-11-13 10:37
 **/
public class JavaCourse implements ICourse {
    private Integer Id;
    private String name;
    private Double price;

    public JavaCourse(Integer id, String name, Double price) {
        this.Id = id;
        this.name = name;
        this.price = price;
    }

    public JavaCourse() {
    }

    public Integer getId() {
        return this.Id;
    }

    public String getName() {
        return this.name;
    }

    public Double getPrice() {
        return this.price;
    }
}

建立一個測試類

/**
 * @program: designModel
 * @description:
 * @author: YuKai Fan
 * @create: 2018-11-13 10:38
 **/
public class Test {
    public static void main(String[] args) {
        
        ICourse javaCourse  = new JavaCourse(96, "Java設計原則", 998d);
        System.out.println("課程Id:" + javaCourse.getId() + "課程名稱:" + javaCourse.getName() + "課程價格:" + javaCourse.getPrice());
    }
}

現在因為有活動,需要對java有關課程進行8折活動,需要修改程式碼,但是對於其他型別的課程不一定打8折,而且如果直接在介面中改動,則太麻煩,其實現類也需要改動,如果有多個類實現介面,則大大降低了開發效率,甚至會使專案崩潰,所以要遵循開閉原則。

不能改動原有的程式碼。新增一個新的類,來繼承javaCourse,重寫price方法,即可完成需求

/**
 * @program: designModel
 * @description:
 * @author: YuKai Fan
 * @create: 2018-11-13 10:45
 **/
public class JavaDiscountCourse extends JavaCourse {

    public JavaDiscountCourse(Integer id, String name, Double price) {
        super(id, name, price);
    }

    public Double getOriginPrice() {
        return super.getPrice();
    }

    @Override
    public Double getPrice() {
        return super.getPrice() * 0.8;
    }
}
/**
 * @program: designModel
 * @description:
 * @author: YuKai Fan
 * @create: 2018-11-13 10:38
 **/
public class Test {
    public static void main(String[] args) {
        ICourse iCourse = new JavaDiscountCourse(96, "Java設計原則", 998d);
        JavaDiscountCourse javaCourse = (JavaDiscountCourse) iCourse;
        System.out.println("課程Id:" + javaCourse.getId() + "課程名稱:" + javaCourse.getName() + "課程價格:" + javaCourse.getPrice() + "課程原價:" + javaCourse.getOriginPrice());
    }
}

根據面向物件的墮胎性質,JavaDiscountCourse,JavaCourse,ICourse都是同一型別,可以直接強轉。

UML類圖:

 

2.里氏替換原則

如果一個型別為T1的物件o1,都有型別為T2的物件o2,使得在程式P中將所有的o1替換成o2時,程式的行為P沒有發生變化,那麼T2就是T1的子型別。

子型別必須能夠替換調它們的父型別。如果一個軟體實體中使用的是父類,那麼一定適用於子類,而且程式察覺不出它們之間的區別。比如,我們說企鵝是一種鳥,但是在程式設計中,不能用企鵝來繼承鳥。因為在面向物件設計中,子類應該擁有父類所有非private的屬性和行為,因為鳥會飛,而企鵝不會飛,所以企鵝不能繼承鳥。

當子類可以替換掉父類,軟體單位的功能不受影響時,父類才能真正被複用,而子類也能夠在父類的基礎上增加新的行為,正是有里氏代換原則,使得繼承複用成為了可能。正是由於子型別的可替換性才使得使用父類型別的模組在無需修改的情況下就可以擴充套件,不然還談什麼擴充套件開放,修改關閉呢。

比較通俗的說,里氏替換原則就是子類可以擴充套件父類的功能,但是不能改變父類原有的功能:

1.子類可以實現父類的抽象方法,但是不能覆蓋父類的抽象方法

2.子類可以增加自己特有的方法

3.當子類的方法過載父類的方法時,方法的前置條件(方法的形參)要比父類的方法更加寬鬆

4.當子類實現父類的抽象方法時,方法的後置條件(方法的返回值)要比父類更嚴格

 

2.依賴倒置原則

高層模組不應該依賴低層模組,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象,即針對介面程式設計,而不是針對實現程式設計。

依賴倒置可以說是面向物件的設計的標誌原則,無論用哪種語言編寫程式,如果編寫的都是如何針對抽象而不是針對細節,即程式中的依賴關係都終止於介面或者抽象類,那就是面向介面設計,返回就是面向過程了。這樣的好處就是實現了低耦合,高擴充套件,易維護的特性。通過程式碼來看問題會更加明顯。現在有一個學生,他想學習java和前端,可以直接在這個類中實現這兩個方法:

/**
 * @program: designModel
 * @description: 學生類
 * @author: YuKai Fan
 * @create: 2018-11-13 11:16
 **/
public class Student {
    public void studyJava() {
        System.out.println("學習java");
    }

    public void studyFE() {
        System.out.println("學習前端");
    }

}
/**
 * @program: designModel
 * @description: test屬於應用層,不應該依賴student低層次模組 ,學生想學習什麼都需要在Student中新增方法,然後在Test使用,擴充套件性比較差。
 * @author: YuKai Fan
 * @create: 2018-11-13 11:17
 **/
public class Test {
    public static void main(String[] args) {
        Student student = new Student();
        student.studyFE();
        student.studyJava();
    }
}

但是就如同程式碼上的註釋一樣,如果現在學生又想學習linux課程,那就必須要在Student中新增方法,在Test應用層中實現。這樣高層依賴低層,耦合度很高,擴充套件性差。所以現在進行改動,編寫一個介面,讓對應的課程去實現介面完成學習任務

/**
 * @program: designModel
 * @description:
 * @author: YuKai Fan
 * @create: 2018-11-13 11:20
 **/
public interface ICourse {
    void studyCourse();
}
/**
 * @program: designModel
 * @description:
 * @author: YuKai Fan
 * @create: 2018-11-13 11:20
 **/
public class JavaCourse implements ICourse {
    public void studyCourse() {
        System.out.println("學生學習java課程");
    }
}
/**
 * @program: designModel
 * @description:
 * @author: YuKai Fan
 * @create: 2018-11-13 11:21
 **/
public class FECourse implements ICourse {
    public void studyCourse() {
        System.out.println("學生學習前端課程");
    }
}

而在Student中,就不需要這兩個方法,寫一個公共的方法,使用介面作為入參。Test直接呼叫。這種注入的方法,稱為介面注入

/**
 * @program: designModel
 * @description: test屬於應用層,不應該依賴student低層次模組 ,學生想學習什麼都需要在Student中新增方法,然後在Test使用,擴充套件性比較差。
 * @author: YuKai Fan
 * @create: 2018-11-13 11:17
 **/
public class Test {
//    public static void main(String[] args) {
//        Student student = new Student();
//        student.studyFE();
//        student.studyJava();
//    }
  //介面注入
    public static void main(String[] args) {
        Student student = new Student();
        student.studyStudentCourse(new JavaCourse());
        student.studyStudentCourse(new FECourse());
    }
}

還有另一種方式,在Student中定義一個有參構造器,將介面傳入

/**
 * @program: designModel
 * @description: 學生類
 * @author: YuKai Fan
 * @create: 2018-11-13 11:16
 **/
public class Student {
    /*public void studyJava() {
        System.out.println("學習java");
    }

    public void studyFE() {
        System.out.println("學習前端");
    }*/
    private ICourse iCourse;

    public Student(ICourse iCourse) {
        this.iCourse = iCourse;
    }

    public void studyStudentCourse() {
        iCourse.studyCourse();
    }

}

Test類中,在new student時直接可以傳入介面實現類物件,這種稱為構造器注入

/**
 * @program: designModel
 * @description: test屬於應用層,不應該依賴student低層次模組 ,學生想學習什麼都需要在Student中新增方法,然後在Test使用,擴充套件性比較差。
 * @author: YuKai Fan
 * @create: 2018-11-13 11:17
 **/
public class Test {
//    public static void main(String[] args) {
//        Student student = new Student();
//        student.studyFE();
//        student.studyJava();
//    }

      //介面注入
//    public static void main(String[] args) {
//        Student student = new Student();
//        student.studyStudentCourse(new JavaCourse());
//        student.studyStudentCourse(new FECourse());
//    }

    //構造器注入
    public static void main(String[] args) {
        Student student = new Student(new JavaCourse());
        student.studyStudentCourse();

        Student student1 = new Student(new FECourse());
        student1.studyStudentCourse();
    }
}

第三種方法,也是在實際應用中使用最多的方法settr方法注入

/**
 * @program: designModel
 * @description: 學生類
 * @author: YuKai Fan
 * @create: 2018-11-13 11:16
 **/
public class Student {
    /*public void studyJava() {
        System.out.println("學習java");
    }

    public void studyFE() {
        System.out.println("學習前端");
    }*/

    /*public Student(ICourse iCourse) {
        this.iCourse = iCourse;
    }*/
    
    private ICourse iCourse;

    public void studyStudentCourse() {
        iCourse.studyCourse();
    }

    public void setiCourse(ICourse iCourse) {
        this.iCourse = iCourse;
    }
}

Test應用類,通過介面的set方法,將物件注入

/**
 * @program: designModel
 * @description: test屬於應用層,不應該依賴student低層次模組 ,學生想學習什麼都需要在Student中新增方法,然後在Test使用,擴充套件性比較差。
 * @author: YuKai Fan
 * @create: 2018-11-13 11:17
 **/
public class Test {
//    public static void main(String[] args) {
//        Student student = new Student();
//        student.studyFE();
//        student.studyJava();
//    }

      //介面注入
//    public static void main(String[] args) {
//        Student student = new Student();
//        student.studyStudentCourse(new JavaCourse());
//        student.studyStudentCourse(new FECourse());
//    }

    //構造器注入
//    public static void main(String[] args) {
//        Student student = new Student(new JavaCourse());
//        student.studyStudentCourse();
//
//        Student student1 = new Student(new FECourse());
//        student1.studyStudentCourse();
//    }

    public static void main(String[] args) {
        Student student = new Student();
        student.setiCourse(new JavaCourse());
        student.studyStudentCourse();

        student.setiCourse(new FECourse());
        student.studyStudentCourse();
    }
}

UML類圖:

依賴倒置原則的中心思想就是面向介面程式設計,依賴傳遞方式有三種:介面注入,構造器注入,setter方法注入。這三種方法,在學習Spring的時候,都會用到依賴傳遞。

相對於細節的東西,抽象的東西要穩定的多。以抽象基礎搭起來的架構要比細節搭出來的架構要穩定的多。在java中,抽象指的是抽象類和介面,細節就是具體的實現類,使用介面和抽象類的目的就是為了制定規範和約束,而不涉及任何的就操作。

在實際程式設計中,一般要做到:低層模組都要有抽象類或者介面,變數的宣告型別儘量是抽象類或者介面,使用繼承是遵循里氏替換原則

 

3.單一職責原則

 不要存在多於一個導致類變更的原因。直白的說就是一個類只負責一項職責,應該只有一個引起他變化的原因

說到單一職責原則,很多人都會不屑一顧。因為它太簡單了。稍有經驗的程式設計師即使從來沒有讀過設計模式、從來沒有聽說過單一職責原則,在設計軟體時也會自覺的遵守這一重要原則,因為這是常識。在軟體程式設計中,誰也不希望因為修改了一個功能導致其他的功能發生故障。而避免出現這一問題的方法便是遵循單一職責原則。雖然單一職責原則如此簡單,並且被認為是常識,但是即便是經驗豐富的程式設計師寫出的程式,也會有違背這一原則的程式碼存在。為什麼會出現這種現象呢?因為有職責擴散。所謂職責擴散,就是因為某種原因,職責P被分化為粒度更細的職責P1和P2。

遵循單一職責原則的優點:

1.可以降低類的複雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單得多

2.提高類的可讀性,提高系統的可維護性

3.變更引起的風險降低,變更是必然的,但是如果遵循單一職責原則,當修改一個功能時,可以顯著降低對其他功能的影響。

需要說明的一點是單一職責原則不只是面向物件程式設計思想所特有的,只要是模組化的程式設計,都需要遵循這一重要原則

 

4.介面隔離原則

用多個專門的介面,而不是用單一的總介面,客戶端不應該依賴它不需要的介面。我們要為各個類建立專用的介面,而不要試圖去建立一個很龐大的介面供所有依賴它的類去呼叫。在程式設計中,依賴幾個專用的介面要比依賴一個綜合的介面更靈活。介面是設計時對外部設定的“契約”,通過分散定義多個介面,可以預防外來變更的擴散,提高系統的靈活性和可維護性。

我最初感覺單一職責原則與介面隔離原則很相似,但是後來發現兩者不一樣。單一職責原則注重的職責,主要約束的是類,其次才是介面和方法,針對的是程式中的實現和細節。而介面隔離原則,注重介面依賴的隔離,主要約束的是介面,針對抽象,程式整體架構的構建。但是使用介面隔離原則需要注意幾點:

1.介面儘量小,但是要有限度;雖然對介面進行細化會提高程式設計的靈活性,但是如果過小,會造成介面數量過多,使設計複雜化。

2.為依賴介面的類定製服務,只暴露給呼叫的類它需要的方法,它不需要的方法則隱藏起來。只有專注地為一個模組提供定製服務,才能建立最小的依賴關係。

3.提高內聚,減少對外互動。使介面用最少的方法去完成最多的事情。

比如下面的UML類圖

狗可以吃,也可以游泳,但是不會飛。鳥可以吃,可以飛,但是不會游泳。這樣就會造成多餘的空方法。

改造後UML類圖:

 

5.迪米特原則(最少知道原則)

一個物件應該對其他物件保持最少的瞭解,又叫最少知道原則,儘量降低類與類之間的耦合。強調只和朋友交流,不和陌生人說話。

朋友:出現在成員變數,方法的輸入輸出引數中的類稱為成員朋友類,而出現在方法體內部的類不屬於朋友類。

迪米特法則的的根本思想是,強調類之間的鬆耦合,類之間耦合越弱,越有利於複用。一個處於弱耦合的類被修改時,不會對其有關係的類造成影響,換句話說,也就是資訊的隱藏促進了軟體的複用。

我們都知道軟體程式設計的總原則是,高內聚,低耦合,無論是面向物件程式設計還是面向過程,只有各個模組的耦合度儘量低,才能提高程式碼的複用率。

最少知道原則:顧名思義,一個類對自己依賴的類知道的越少越好,即對於被依賴的類來說,無論邏輯多麼複雜,都儘量的將邏輯封裝在類的內部,對外除了提供public的方法,不對外洩露任何資訊。

只與直接的朋友通訊。首先來解釋一下什麼是直接的朋友:每個物件都會與其他物件有耦合關係,只要兩個物件之間有耦合關係,我們就說這兩個物件之間是朋友關係。耦合的方式很多,依賴、關聯、組合、聚合等。其中,我們稱出現成員變數、方法引數、方法返回值中的類為直接的朋友,而出現在區域性變數中的類則不是直接的朋友。也就是說,陌生的類最好不要作為區域性變數的形式出現在類的內部。

現在有一個老闆Boss,他想指令一個團隊領導Teamleader統計課程Course的數量,並打印出來。可以如下設計

/**
 * @program: designModel
 * @description:
 * @author: YuKai Fan
 * @create: 2018-11-13 15:29
 **/
public class Boss {
    public void commandCheckNumber(TeamLeader teamLeader) {
        List<Course> courseList = new ArrayList<Course>();
        for (int i = 0; i < 20; i++) {
            courseList.add(new Course());
        }
        teamLeader.checkNumberOfCourses(courseList);
    }
}
/**
 * @program: designModel
 * @description:
 * @author: YuKai Fan
 * @create: 2018-11-13 15:30
 **/
public class TeamLeader {
    public void checkNumberOfCourses(List<Course> courseList) {
        System.out.println("線上課程的數量是:" + courseList.size());
    }
}
/**
 * @program: designModel
 * @description:
 * @author: YuKai Fan
 * @create: 2018-11-13 15:33
 **/
public class Test {
    public static void main(String[] args) {
        Boss boss = new Boss();
        TeamLeader teamLeader = new TeamLeader();
        boss.commandCheckNumber(teamLeader);
    }
}

根據UML類圖可知:

 

其中Boss與課程Course是沒有關係的,所以Boss不應該知道有關Course的內容,而是Teamleader去了解Course,所以作如下改動

/**
 * @program: designModel
 * @description:
 * @author: YuKai Fan
 * @create: 2018-11-13 15:29
 **/
public class Boss {
    public void commandCheckNumber(TeamLeader teamLeader) {
        /*List<Course> courseList = new ArrayList<Course>();
        for (int i = 0; i < 20; i++) {
            courseList.add(new Course());
        }*/
        teamLeader.checkNumberOfCourses();
    }
}
/**
 * @program: designModel
 * @description:
 * @author: YuKai Fan
 * @create: 2018-11-13 15:30
 **/
public class TeamLeader {
    public void checkNumberOfCourses() {
        List<Course> courseList = new ArrayList<Course>();
        for (int i = 0; i < 20; i++) {
            courseList.add(new Course());
        }
        System.out.println("線上課程的數量是:" + courseList.size());
    }
}

UML類圖為: