[01][01][01] 七大軟件設計原則

1. 七大軟件設計原則

  • 單一職責原則
  • 里氏替換原則
  • 依賴倒置原則
  • 接口隔離原則
  • 合成複用原則
  • 開閉原則
  • 迪米特法則

快速記憶口訣:單一的里氏依賴接口合成開閉的迪米特法則前端

2. 開閉原則

開閉原則(Open-ClosedPrinciple,OCP)是指一個軟件實體如類,模塊和函數應該對擴展開放,對修改關閉.所謂的開閉,也正是對擴展和修改兩個行爲的一個原則.強調的是用抽象構建框架,用實現擴展細節.能夠提升軟件系統的可複用性及可維護性.開閉原則,是面向對象設計中最基礎的設計原則.它指導咱們如何創建穩定靈活的系統,例如:咱們版本更新,我儘量不修改源代碼,可是能夠增長新功能.java

在現實生活中對於開閉原則也有體現.好比,不少互聯網公司都實行彈性製做息時間,規定天天工做8小時.意思就是說,對於天天工做8小時這個規定是關閉的,可是你何時來,何時走是開放的.早來早走,晚來晚走.編程

實現開閉原則的核心思想就是面向抽象編程,接下來咱們來看一段代碼:以咕泡學院的課程體系爲例,首先建立一個課程接口 ICourse:設計模式

public interface ICourse {
    Integer getId();
    String getName();
    Double getPrice();
}

整個課程生態有 Java 架構,大數據,人工智能,前端,軟件測試等,咱們來建立一個 Java 架構課程的類 JavaCourse:架構

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 Integer getId() {
        return this.Id;
    }

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

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

如今咱們要給 Java 架構課程作活動,價格優惠.若是修改 JavaCourse 中的 getPrice()方法,則會存在必定的風險,可能影響其餘地方的調用結果.咱們如何在不修改原有代碼前提早下,實現價格優惠這個功能呢?如今,咱們再寫一個處理優惠邏輯的類,JavaDiscountCourse 類(思考一下爲何要叫 JavaDiscountCourse,而不叫 DiscountCourse):框架

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

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

    public Double getPrice(){
        return super.getPrice() * 0.61;
    }
}

回顧一下,簡單一下類結構圖:
ide

3. 依賴倒置原則

依賴倒置原則(DependenceInversionPrinciple,DIP)是指設計代碼結構時,高層模塊不該該依賴底層模塊,兩者都應該依賴其抽象.抽象不該該依賴細節;細節應該依賴抽象.經過依賴倒置,能夠減小類與類之間的耦合性,提升系統的穩定性,提升代碼的可讀性和可維護性,並可以下降修改程序所形成的風險.接下來看一個案例,仍是以課程爲例,先來建立一個類 Tom:函數

public class Tom {
    public void studyJavaCourse() {
        System.out.println("Tom 在學習 Java 的課程");
    }

    public void studyPythonCourse() {
        System.out.println("Tom 在學習 Python 的課程");
    }
}

來調用一下:學習

public static void main(String[] args) {
    Tom tom = new Tom();
    tom.studyJavaCourse();
    tom.studyPythonCourse();
}

Tom 熱愛學習,目前正在學習 Java 課程和 Python 課程.你們都知道,學習也是會上癮的.隨着學習興趣的暴漲,如今 Tom 還想學習 AI 人工智能的課程.這個時候,業務擴展,咱們的代碼要從底層到高層(調用層)一次修改代碼.在 Tom 類中增長 studyAICourse()的方法,在高層也要追加調用.如此一來,系統發佈之後,其實是很是不穩定的,在修改代碼的同時也會帶來意想不到的風險.接下來咱們優化代碼,建立一個課程的抽象 ICourse 接口:測試

public interface ICourse {
    void study();
}

而後寫 JavaCourse 類:

public class JavaCourse implements ICourse {
    @Override
    public void study() {
        System.out.println("Tom 在學習 Java 課程");
    }
}

再實現 PythonCourse 類:

public class PythonCourse implements ICourse {
    @Override
    public void study() {
        System.out.println("Tom 在學習 Python 課程");
    }
}

修改 Tom 類:

public class Tom {
    public void study(ICourse course){
        course.study();
    }
}

來看調用:

public static void main(String[] args) {
    Tom tom = new Tom();
    tom.study(new JavaCourse());
    tom.study(new PythonCourse());
}

咱們這時候再看來代碼,Tom 的興趣不管怎麼暴漲,對於新的課程,我只須要新建一個類,經過傳參的方式告訴 Tom,而不須要修改底層代碼.實際上這是一種你們很是熟悉的方式,叫依賴注入.注入的方式還有構造器方式和 setter 方式.咱們來看構造器注入方式:

public class Tom {
    private ICourse course;

    public Tom(ICourse course){
        this.course = course;
    }

    public void study(){
        course.study();
    }
}

看調用代碼:

public static void main(String[] args) {
    Tom tom = new Tom(new JavaCourse());
    tom.study();
}

根據構造器方式注入,在調用時,每次都要建立實例.那麼,若是 Tom 是全局單例,則咱們就只能選擇用 Setter 方式來注入,繼續修改 Tom 類的代碼:

public class Tom {
    private ICourse course;
    public void setCourse(ICourse course) {
        this.course = course;
    }

    public void study(){
        course.study();
    }
}

看調用代碼:

public static void main(String[] args) {
    Tom tom = new Tom();
    tom.setCourse(new JavaCourse());
    tom.study();
    tom.setCourse(new PythonCourse());
    tom.study();
}

如今咱們再來看最終的類圖:

你們要切記:以抽象爲基準比以細節爲基準搭建起來的架構要穩定得多,所以你們在拿到需求以後,要面向接口編程,先頂層再細節來設計代碼結構

4. 單一職責原則

單一職責(SimpleResponsibilityPinciple,SRP)是指不要存在多於一個致使類變動的緣由.假設咱們有一個 Class 負責兩個職責,一旦發生需求變動,修改其中一個職責的邏輯代碼,有可能會致使另外一個職責的功能發生故障.這樣一來,這個 Class 存在兩個致使類變動的緣由.如何解決這個問題呢?咱們就要給兩個職責分別用兩個 Class 來實現,進行解耦.後期需求變動維護互不影響.這樣的設計,能夠下降類的複雜度,提升類的可讀性,提升系統的可維護性,下降變動引發的風險.整體來講就是一個 Class/Interface/Method 只負責一項職責

接下來,咱們來看代碼實例,仍是用課程舉例,咱們的課程有直播課和錄播課.直播課不能快進和快退,錄播能夠能夠任意的反覆觀看,功能職責不同.仍是先建立一個 Course 類:

public class Course {
    public void study(String courseName) {
        if ("直播課".equals(courseName)) {
            System.out.println("不能快進");
        } else {
            System.out.println("能夠任意的來回播放");
        }
    }
}

看代碼調用:

public static void main(String[] args) {
    Course course = new Course();
    course.study("直播課");
    course.study("錄播課");
}

從上面代碼來看,Course 類承擔了兩種處理邏輯.假如,如今要對課程進行加密,那麼直播課和錄播課的加密邏輯都不同,必需要修改代碼.而修改代碼邏輯勢必會相互影響容易形成不可控的風險.咱們對職責進行分離解耦,來看代碼,分別建立兩個類 ReplayCourse 和 LiveCourse:
LiveCourse 類:

public class LiveCourse {
    public void study(String courseName) {
        System.out.println(courseName + "不能快進看");
    }
}

ReplayCourse 類:

public class ReplayCourse {
    public void study(String courseName) {
        System.out.println("能夠任意的來回播放");
    }
}

調用代碼:

public static void main(String[] args) {
    LiveCourse liveCourse = new LiveCourse();
    liveCourse.study("直播課");
    ReplayCourse replayCourse = new ReplayCourse();
    replayCourse.study("錄播課");
}

業務繼續發展,課程要作權限.沒有付費的學員能夠獲取課程基本信息,已經付費的學員能夠得到視頻流,即學習權限.那麼對於控制課程層面上至少有兩個職責.咱們能夠把展現職責和管理職責分離開來,都實現同一個抽象依賴.設計一個頂層接口,建立 ICourse 接口:

public interface ICourse {
    //得到基本信息
    String getCourseName();
    //得到視頻流
    byte[] getCourseVideo();
    //學習課程
    void studyCourse();
    //退款
    void refundCourse();
}

咱們能夠把這個接口拆成兩個接口,建立一個接口 ICourseInfo 和 ICourseManager:
ICourseInfo 接口:

public interface ICourseInfo {
    String getCourseName();
    byte[] getCourseVideo();
}

ICourseManager 接口:

public interface ICourseManager {
    void studyCourse(); void refundCourse();
}

來看一下類圖:

下面咱們來看一下方法層面的單一職責設計.有時候,咱們爲了偷懶,一般會把一個方法寫成下面這樣:

private void modifyUserInfo(String userName,String address) {
    userName = "Tom";
    address = "Changsha";
}

還可能寫成這樣:

private void modifyUserInfo(String userName,String... fileds) {
    userName = "Tom";
    // address = "Changsha";
}

private void modifyUserInfo(String userName,String address,boolean bool) {
    if (bool) {

    } else{

    }
    userName = "Tom";
    address = "Changsha";
}

顯然,上面的 modifyUserInfo()方法中都承擔了多個職責,既能夠修改 userName,也能夠修改 address,甚至更多,明顯不符合單一職責.那麼咱們作以下修改,把這個方法拆成兩個:

private void modifyUserName(String userName){
    userName = "Tom";
}

private void modifyAddress(String address){
    address = "Changsha";
}

這修改以後,開發起來簡單,維護起來也容易.可是,咱們在實際開發中會項目依賴,組合,聚合這些關係,還有還有項目的規模,週期,技術人員的水平,對進度的把控,不少類都不符合單一職責.可是,咱們在編寫代碼的過程,儘量地讓接口和方法保持單一職責,對咱們項目後期的維護是有很大幫助的

5. 接口隔離原則

接口隔離原則(InterfaceSegregationPrinciple,ISP)是指用多個專門的接口,而不使用單一的總接口,客戶端不該該依賴它不須要的接口.這個原則指導咱們在設計接口時應當注意一下幾點:

  • 一個類對一類的依賴應該創建在最小的接口之上
  • 創建單一接口,不要創建龐大臃腫的接口
  • 儘可能細化接口,接口中的方法儘可能少(不是越少越好,必定要適度)

接口隔離原則符合咱們常說的高內聚低耦合的設計思想,從而使得類具備很好的可讀性,可擴展性和可維護性.咱們在設計接口的時候,要多花時間去思考,要考慮業務模型,包括之後有可能發生變動的地方還要作一些預判.因此,對於抽象,對業務模型的理解是很是重要的.下面咱們來看一段代碼,寫一個動物行爲的抽象:

IAnimal 接口:

public interface IAnimal {
    void eat();
    void fly();
    void swim();
}

Bird 類實現:

public class Bird implements IAnimal {
    @Override
    public void eat() {}
    @Override
    public void fly() {}
    @Override
    public void swim() {}
}

Dog 類實現:

public class Dog implements IAnimal {
    @Override
    public void eat() {}
    @Override
    public void fly() {}
    @Override
    public void swim() {}
}

能夠看出,Bird 的 swim()方法可能只能空着,Dog 的 fly()方法顯然不可能的.這時候,咱們針對不一樣動物行爲來設計不一樣的接口,分別設計 IEatAnimal, IFlyAnimal 和 ISwimAnimal 接口,來看代碼:
IEatAnimal 接口:

public interface IEatAnimal {
    void eat();
}

IFlyAnimal 接口:

public interface IFlyAnimal {
    void fly();
}

ISwimAnimal 接口:

public interface ISwimAnimal {
    void swim();
}

Dog 只實現 IEatAnimal 和 ISwimAnimal 接口:

public class Dog implements ISwimAnimal,IEatAnimal {
    @Override
    public void eat() {}

    @Override
    public void swim() {}
}

來看下兩種類圖的對比,仍是很是清晰明瞭的:

6. 迪米特法則

迪米特原則(LawofDemeterLoD)是指一個對象應該對其餘對象保持最少的瞭解,又叫最少知道原則(LeastKnowledgePrinciple,LKP),儘可能下降類與類之間的耦合.迪米特原則主要強調只和朋友交流,不和陌生人說話.出如今成員變量,方法的輸入,輸出參數中的類均可以稱之爲成員朋友類,而出如今方法體內部的類不屬於朋友類.

如今來設計一個權限系統,TeamLeader 須要查看目前發佈到線上的課程數量.這時候,TeamLeader 要找到員工 Employee 去進行統計,Employee 再把統計結果告訴 TeamLeader.接下來咱們仍是來看代碼:

Course 類:

public class Course {
}

Employee 類:

public class Employee{
    public void checkNumberOfCourses(List<Course> courseList){
        System.out.println("目前已發佈的課程數量是:" + courseList.size());
    }
}

TeamLeader 類:

public class TeamLeader{
    public void commandCheckNumber(Employee employee){
        List<Course> courseList = new ArrayList<Course>();
        for (int i= 0; i < 20 ;i ++){
            courseList.add(new Course());
        }
        employee.checkNumberOfCourses(courseList);
    }
}

測試代碼:

public static void main(String[] args) {
    TeamLeader teamLeader = new TeamLeader();
    Employee employee = new Employee();
    teamLeader.commandCheckNumber(employee);
}

寫到這裏,其實功能已經都已經實現,代碼看上去也沒什麼問題.根據迪米特原則,TeamLeader 只想要結果,不須要跟 Course 產生直接的交流.而 Employee 統計須要引用 Course 對象.TeamLeader 和 Course 並非朋友,從下面的類圖就能夠看出來:

下面來對代碼進行改造:
Employee 類:

public class Employee {
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());
    }
}

TeamLeader 類:

public class TeamLeader {
    public void commandCheckNumber(Employee employee){
        employee.checkNumberOfCourses();
    }
}

再來看下面的類圖,Course 和 TeamLeader 已經沒有關聯了.

學習軟件設計原則,千萬不能造成強迫症.碰到業務複雜的場景,咱們須要隨機應變

7. 里氏替換原則

里氏替換原則(LiskovSubstitutionPrinciple,LSP)是指若是對每個類型爲 T1 的對象 o1,都有類型爲 T2 的對象 o2,使得以 T1 定義的全部程序 P 在全部的對象 o1 都替換成 o2 時,程序 P 的行爲沒有發生變化,那麼類型 T2 是類型 T1 的子類型.

定義看上去仍是比較抽象,咱們從新理解一下,能夠理解爲一個軟件實體若是適用一個父類的話,那必定是適用於其子類,全部引用父類的地方必須能透明地使用其子類的對象,子類對象可以替換父類對象,而程序邏輯不變.根據這個理解,咱們總結一下:

引伸含義:子類能夠擴展父類的功能,但不能改變父類原有的功能.

  • 子類能夠實現父類的抽象方法,但不能覆蓋父類的非抽象方法.
  • 子類中能夠增長本身特有的方法.
  • 當子類的方法重載父類的方法時,方法的前置條件(即方法的輸入/入參)要比父類方法的輸入參數更寬鬆.
  • 當子類的方法實現父類的方法時(重寫/重載或實現抽象方法),方法的後置條件(即方法的輸出/返回值)要比父類更嚴格或相等.

在前面講開閉原則的時候埋下了一個伏筆,咱們記得在獲取折後時重寫覆蓋了父類的 getPrice()方法,增長了一個獲取源碼的方法 getOriginPrice(),顯然就違背了里氏替換原則.咱們修改一下代碼,不該該覆蓋 getPrice()方法,增長 getDiscountPrice()方法:

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

    public Double getDiscountPrice(){
        return super.getPrice() * 0.61;
    }
}

使用里氏替換原則有如下優勢:

  • 約束繼承氾濫,開閉原則的一種體現
  • 增強程序的健壯性,同時變動時也能夠作到很是好的兼容性,提升程序的維護性,擴展性.下降需求變動時引入的風險.如今來描述一個經典的業務場景,用正方形,矩形和四邊形的關係說明裏氏替換原則,咱們都知道正方形是一個特殊的長方形,那麼就能夠建立一個長方形父類 Rectangle 類:

    public class Rectangle {
    private long height;
    private long width;
    public long getHeight() {
    return height;
    }
    public void setHeight(long height) {
    this.height = height;
    }
    public long getWidth() {
    return width;
    }
    public void setWidth(long width) {
    this.width = width;
    }
    }

建立正方形 Square 類繼承長方形:

public class Square extends Rectangle {
    private long length;

    public long getLength() {
        return length;
    }

    public void setLength(long length) {
        this.length = length;
    }

    @Override
    public long getHeight() {
        return getLength();
    }

    @Override
    public void setHeight(long height) {
        setLength(height);
    }

    @Override
    public long getWidth() {
        return getLength();
    }

    @Override
    public void setWidth(long width) {
        setLength(width);
    }
}

在測試類中建立 resize()方法,根據邏輯長方形的寬應該大於等於高,咱們讓高一直自增,知道高等於寬變成正方形:

public static void resize(Rectangle rectangle){

    while (rectangle.getWidth() >= rectangle.getHeight()){
        rectangle.setHeight(rectangle.getHeight() + 1);
        System.out.println("Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight());
    }

    System.out.println("Resize End,Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight());
}

測試代碼:

public static void main(String[] args) {
    Rectangle rectangle = new Rectangle();
    rectangle.setWidth(20);
    rectangle.setHeight(10);
    resize(rectangle);
}

運行結果:

發現高比寬還大了,在長方形中是一種很是正常的狀況.如今咱們再來看下面的代碼,把長方形 Rectangle 替換成它的子類正方形 Square,修改測試代碼:

public static void main(String[] args) {
    Square square = new Square();
    square.setLength(10);
    resize(square);
}

這時候咱們運行的時候就出現了死循環,違背了里氏替換原則,將父類替換爲子類後,程序運行結果沒有達到預期.所以,咱們的代碼設計是存在必定風險的.里氏替換原則只存在父類與子類之間,約束繼承氾濫.咱們再來建立一個基於長方形與正方形共同的抽象四邊形 Quadrangle 接口:

public interface QuadRangle {
    long getWidth();
    long getHeight();
}

修改長方形 Rectangle 類:

public class Rectangle implements QuadRangle {
    private long height;
    private long width;

    public long getHeight() {
        return height;
    }

    public void setHeight(long height) {
        this.height = height;
    }

    public long getWidth() {
        return width;
    }

    public void setWidth(long width) {
        this.width = width;
    }
}

修改正方形類 Square 類:

public class Square implements QuadRangle {
    private long length;

    public long getLength() {
        return length;
    }

    public void setLength(long length) {
        this.length = length;
    }

    public long getWidth() {
        return length;
    }

    public long getHeight() {
        return length;
    }
}

此時,若是咱們把 resize()方法的參數換成四邊形 Quadrangle 類,方法內部就會報錯.由於正方形 Square 已經沒有了 setWidth()和 setHeight()方法了.所以,爲了約束繼承濫,resize()的方法參數只能用 Rectangle 長方形.固然,咱們在後面的設計模式課程中還會繼續深刻講解

8. 合成複用原則

public interface IAnimal {
    void eat();
    void fly();
    void swim();
}

Bird 類實現:

public class Bird implements IAnimal {
    @Override
    public void eat() {}

    @Override
    public void fly() {}

    @Override
    public void swim() {}
}

Dog 類實現:

public class Dog implements IAnimal {
    @Override
    public void eat() {}

    @Override
    public void fly() {}

    @Override
    public void swim() {}
}

能夠看出,Bird 的 swim()方法可能只能空着,Dog 的 fly()方法顯然不可能的.這時候,咱們針對不一樣動物行爲來設計不一樣的接口,分別設計 IEatAnimal,IFlyAnimal 和 ISwimAnimal 接口,來看代碼:

IEatAnimal 接口:

public interface IEatAnimal {
    void eat();
}

IFlyAnimal 接口:

public interface IFlyAnimal {
    void fly();
}

ISwimAnimal 接口:

public interface ISwimAnimal {
    void swim();
}

Dog 只實現 IEatAnimal 和 ISwimAnimal 接口:

public class Dog implements ISwimAnimal,IEatAnimal {
    @Override
    public void eat() {}

    @Override
    public void swim() {}
}

來看下兩種類圖的對比,仍是很是清晰明瞭的:

相關文章
相關標籤/搜索