面向對象的七種原則:單一職責原則,開放關閉原則

我們的知識星球馬上就要開始更新設計模式了,在更新設計模式之前,我們是不是需要做一些準備呢?否則設計模式中一些遵循的原則大家會一頭霧水,所以我今天來給大家說一些面向對象的七種原則,有人說是6種有人說是7種,我個人認爲是7種,我就按照7種來說,今天我就介紹2種,下一篇文章將會繼續介紹剩下的五種原則,這些原則也會在設計模式中出現,各位技術人,歡迎大家的踊躍參加呦。

前言

在面向對象的軟件設計中,只有儘量降低各個模塊之間的耦合度,才能提高代碼的複用率,系統的可維護性、可擴展性才能提高。面向對象的軟件設計中,有23種經典的設計模式,是一套前人代碼設計經驗的總結,如果把設計模式比作武功招式,那麼設計原則就好比是內功心法。常用的設計原則有七個,下文將具體介紹。

設計原則簡介

  • 單一職責原則:專注降低類的複雜度,實現類要職責單一;

  • 開放關閉原則:所有面向對象原則的核心,設計要對擴展開發,對修改關閉;

  • 裏式替換原則:實現開放關閉原則的重要方式之一,設計不要破壞繼承關係;

  • 依賴倒置原則:系統抽象化的具體實現,要求面向接口編程,是面向對象設計的主要實現機制之一;

  • 接口隔離原則:要求接口的方法儘量少,接口儘量細化;

  • 迪米特法則:降低系統的耦合度,使一個模塊的修改儘量少的影響其他模塊,擴展會相對容易;

  • 組合複用原則:在軟件設計中,儘量使用組合/聚合而不是繼承達到代碼複用的目的。

這些設計原則並不說我們一定要遵循他們來進行設計,而是根據我們的實際情況去怎麼去選擇使用他們,來讓我們的程序做的更加的完善。

單一職責原則

解釋

就一個類而言,應該僅有一個引起它變化的原因,通俗的說,就是一個類只負責一項職責。

此原則的核心就是解耦和增強內聚性

那麼爲什麼要使用單一職責原則:

如果一個類承擔的職責過多,就等於把這些職責耦合在一起,一個職責的變化可能會削弱或者抑制這個類完成其他職責的能力。這種耦合會導致脆弱的設計。

這也是他的優點,我們總結一下

優點

(1)降低類的複雜度;

(2)提高類的可讀性,提高系統的可維護性;

(3)降低變更引起的風險(降低對其他功能的影響)。

我們來舉一些簡單的例子來說明一下這個單一職責原則

實例

    //我們用動物生活來做測試
    class Animal{
        public void breathe(String animal){
            System.out.println(animal+"生活在陸地上");
        }
    }
    public class Client{
        public static void main(String[] args){
            Animal animal = new Animal();
            animal.breathe("羊");
            animal.breathe("牛");
            animal.breathe("豬");
        }
    }
   
 運行結果
 羊生活在陸地上
 牛生活在陸地上
 豬生活在陸地上

但是問題來了,動物並不是都生活在陸地上的,魚就是生活在水中的,修改時如果遵循單一職責原則,需要將Animal類細分爲陸生動物類Terrestrial,水生動物Aquatic,代碼如下:

class Terrestrial{
    public void breathe(String animal){
        System.out.println(animal+"生活在陸地上");
    }
}
class Aquatic{
    public void breathe(String animal){
        System.out.println(animal+"生活在水裏");
    }
}

    public class Client{
        public static void main(String[] args){
            Terrestrial terrestrial = new Terrestrial();
            terrestrial.breathe("羊");
            terrestrial.breathe("牛");
            terrestrial.breathe("豬");
    
            Aquatic aquatic = new Aquatic();
            aquatic.breathe("魚");
        }

運行結果:

羊生活在陸地上
牛生活在陸地上
豬生活在陸地上
魚生活在水裏

但是問題來了如果這樣修改花銷是很大的,除了將原來的類分解之外,還需要修改客戶端。而直接修改類Animal來達成目的雖然違背了單一職責原則,但花銷卻小的多,代碼如下:


class Animal{
    public void breathe(String animal){
        if("魚".equals(animal)){
            System.out.println(animal+"生活在水中");
        }else{
            System.out.println(animal+"生活在陸地上");
        }
    }
}

public class Client{
    public static void main(String[] args){
        Animal animal = new Animal();
        animal.breathe("羊");
        animal.breathe("牛");
        animal.breathe("豬");
        animal.breathe("魚");
    }

可以看到,這種修改方式要簡單的多。但是卻存在着隱患:有一天需要將魚分爲生活在淡水中的魚和生活在海水的魚,則又需要修改Animal類的breathe方法,而對原有代碼的修改會對調用“豬”“牛”“羊”等相關功能帶來風險,也許某一天你會發現程序運行的結果變爲“牛生活在水中”了。

這種修改方式直接在代碼級別上違背了單一職責原則,雖然修改起來最簡單,但隱患卻是最大的。還有一種修改方式:


class Animal{
    public void breathe(String animal){
        System.out.println(animal+"生活在陸地上");
    }

    public void breathe2(String animal){
        System.out.println(animal+"生活在水中");
    }
}

public class Client{
    public static void main(String[] args){
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("豬");
        animal.breathe2("魚");
    }
}

可以看到,這種修改方式沒有改動原來的方法,而是在類中新加了一個方法,這樣雖然也違背了單一職責原則,但在方法級別上卻是符合單一職責原則的,因爲它並沒有動原來方法的代碼。

這三種方式各有優缺點,那麼在實際編程中,採用哪一中呢?其實這真的比較難說,需要根據實際情況來確定。我的原則是:只有邏輯足夠簡單,纔可以在代碼級別上違反單一職責原則;只有類中方法數量足夠少,纔可以在方法級別上違反單一職責原則;

例如本文所舉的這個例子,它太簡單了,它只有一個方法,所以,無論是在代碼級別上違反單一職責原則,還是在方法級別上違反,都不會造成太大的影響。
實際應用中的類都要複雜的多,一旦發生職責擴散而需要修改類時,除非這個類本身非常簡單,否則還是遵循單一職責原則的好。

以上就是我所說的單一職責原則了,很多書中介紹的說它並不屬於面向對象設計原則中的一種,但是我認爲它是,所以我就把他解釋出來了。

開放關閉原則

開放關閉原則又稱爲開放封閉原則。

定義

一個軟件實體應該對擴展開放,對修改關閉,這個原則也是說,在設計一個模塊的時候,應當使這個模塊可以在不被修改的前提下被擴展,換句話說,應當可以在不必修改源碼的情況下改變這個模塊的行爲。

這句話其實剛開始看上去是有些矛盾的,接下來在我後邊文章解釋裏面,我會把他解釋清楚一點。

我們先用個比較好玩的例子來說一下。

西遊記大家都看過,玉帝招安孫悟空的時候的橋段,大家還有沒有印象?

當年大腦天宮的時候美猴王對玉皇大帝做了個挑戰,美猴王說:皇帝輪流做,明年到我家,只叫他搬出去,將天宮讓給我,對於這個挑戰,太白金星給玉皇大帝提了個意見,我們把它招上來,給他個小官做,他不就不鬧事了?

換一句話說,不用興師動衆的,不破壞天庭的規矩這就是閉,但是收他爲官,便是開,招安的方法便是符合開閉原則的,給他個‘弼馬溫’的官,便可以讓這個系統正常不受威脅,是吧,何樂而不爲?
我給大家畫個圖。

在這裏插入圖片描述

招安的方法的關鍵就是不允許更改現有的天庭秩序,但是允許將妖猴納入現有的秩序中,從而擴展了這個秩序,用面向對象的語言來說:不允許更改的是系統的抽象層,而允許擴展的是系統的實現層。

我們寫一些簡單的代碼來進行一個完整的展示來進行一下理解。

實例

    //書店賣書
    interface Books{
        //書籍名稱
        public Sting getName();
        //書籍價格
        public int getPrice();
        //書籍作者
        public String getAuthor();
    }
    
public class NovelBook implements Books {
    private String name;

    private int price;

    private String author;

    public NovelBook(String name, int price, String author) {
        this.name = name;
        this.price = price;
        this.author = author;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public int getPrice() {
        return price;
    }

    @Override
    public String getAuthor() {
        return author;
    }
}


以上的代碼是數據的實現類和書籍的類別;

下面我們將要開始對書籍進行一個售賣活動:

public class BookStore {
    private final static ArrayList<Books> sBookList = new ArrayList<Books>();

    static {
        sBookList.add(new NovelBook("天龍八部", 4400, "金庸"));
        sBookList.add(new NovelBook("射鵰英雄傳", 7600, "金庸"));
        sBookList.add(new NovelBook("鋼鐵是怎麼煉成的", 7500, "保爾·柯查金"));
        sBookList.add(new NovelBook("紅樓夢", 3300, "曹雪芹"));
    }

    public static void main(String[] args) throws IOException {
        NumberFormat format = NumberFormat.getCurrencyInstance();
        format.setMaximumFractionDigits(2);
       System.out.println("----書店賣出去的書籍記錄如下---");
        for (Books book : sBookList) {
            System.out.println("書籍名稱:" + book.getName()
                    + "\t書籍作者:" + book.getAuthor()
                    + "\t書籍價格:" + format.format(book.getPrice() / 100.00) + "元");
        }
    }
}

運行結果如下:

D:\develop\JDK8\jdk1.8.0_181\bin\java.exe "-javaagent:D:\develop\IDEA\IntelliJ IDEA 2018.2.4\lib\idea_rt.jar=62787:D:\develop\IDEA\IntelliJ IDEA 2018.2.4\bin" -Dfile.encoding=UTF-8 -classpath D:\develop\JDK8\jdk1.8.0_181\jre\lib\charsets.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\deploy.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\dnsns.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\jaccess.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\localedata.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\nashorn.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\sunec.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\ext\zipfs.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\javaws.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\jce.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\jfr.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\jfxswt.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\jsse.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\management-agent.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\plugin.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\resources.jar;D:\develop\JDK8\jdk1.8.0_181\jre\lib\rt.jar;D:\develop\IDEA_Workspace\CloudCode\out\production\PattemMoudle com.yldyyn.test.BookStore
----書店賣出去的書籍記錄如下---
書籍名稱:天龍八部	書籍作者:金庸	書籍價格:¥44.00元
書籍名稱:射鵰英雄傳	書籍作者:金庸	書籍價格:¥76.00元
書籍名稱:鋼鐵是怎麼煉成的	書籍作者:保爾·柯查金	書籍價格:¥75.00元
書籍名稱:紅樓夢	書籍作者:曹雪芹	書籍價格:¥33.00元

Process finished with exit code 0

但是如果說現在書店賣書的時候要求打折出售,40以上的我們要7折售賣,40一下的我們打8折。

方法有三種,第一個辦法:修改接口。在Books上新增加一個方法getOnSalePrice(),專門進行打折,所有實現類實現這個方法。 但是這樣修改的後果就是實現類NovelBook要修改,BookStore中的main方法也修改,同時Books作爲接口應該是穩定且可靠的,不應該經常發生變化,否則接口做爲契約的作用就失去了效能,其他不想打折的書籍也會因爲實現了書籍的接口必須打折,因此該方案被否定。

第二個辦法:修改實現類。修改NovelBook 類中的方法,直接在getPrice()中實現打折處理,這個應該是大家在項目中經常使用的就是這樣辦法,通過class文件替換的方式可以完成部分業務(或是缺陷修復)變化,但是該方法還是有缺陷的,例如採購書籍人員也是要看價格的,由於該方法已經實現了打折處理價格,因此採購人員看到的也是打折後的價格,這就產生了信息的矇蔽效果,導致信息不對稱而出現決策失誤的情況。該方案也不是一個最優的方案。

第三個辦法,通過擴展實現變化增加一個子類 OffNovelBook,覆寫getPrice方法,高層次的模塊(也就是static靜態模塊區)通過OffNovelBook類產生新的對象,完成對業務變化開發任務。好辦法,風險也小。

public class OnSaleBook extends NovelBook {

    public OnSaleBook(String name, int price, String author) {
        super(name, price, author);
    }

    @Override
    public String getName() {
        return super.getName();
    }

    @Override
    public int getPrice() {
        int OnsalePrice = super.getPrice();
        int salePrce = 0;
        if (OnsalePrice >4000){
            salePrce = OnsalePrice * 70/100;
        }else{
            salePrce = OnsalePrice * 80/100;
        }
        return  salePrce;
    }

    @Override
    public String getAuthor() {
        return super.getAuthor();
    }
}

上面的代碼是擴展出來的一個類,而不是在原來的類中進行的修改。

public class BookStore {
    private final static ArrayList<Books> sBookList = new ArrayList<Books>();

    static {
        sBookList.add(new OnSaleBook("天龍八部", 4400, "金庸"));
        sBookList.add(new OnSaleBook("射鵰英雄傳", 7600, "金庸"));
        sBookList.add(new OnSaleBook("鋼鐵是怎麼煉成的", 7500, "保爾·柯查金"));
        sBookList.add(new OnSaleBook("紅樓夢", 3300, "曹雪芹"));
    }

    public static void main(String[] args) throws IOException {
        NumberFormat format = NumberFormat.getCurrencyInstance();
        format.setMaximumFractionDigits(2);
       System.out.println("----書店賣出去的書籍記錄如下---");
        for (Books book : sBookList) {
            System.out.println("書籍名稱:" + book.getName()
                    + "\t書籍作者:" + book.getAuthor()
                    + "\t書籍價格:" + format.format(book.getPrice() / 100.00) + "元");
        }
    }
}

結果展示:

----書店賣出去的書籍記錄如下---
書籍名稱:天龍八部	書籍作者:金庸	書籍價格:¥30.80元
書籍名稱:射鵰英雄傳	書籍作者:金庸	書籍價格:¥53.20元
書籍名稱:鋼鐵是怎麼煉成的	書籍作者:保爾·柯查金	書籍價格:¥52.50元
書籍名稱:紅樓夢	書籍作者:曹雪芹	書籍價格:¥26.40元

Process finished with exit code 0

在開閉原則中,抽象化是一個關鍵,解決問題的關鍵在於抽象化,在JAVA語言這種面向對象的語言中,可以給系統定義出一個一勞永逸的,不再更改的抽象化的設計,此設計允許擁有無窮無盡的實現層被實現。

在JAVA語言中,可以給出一個或者多個抽象的JAVA類或者是JAVA接口,規定所有的具體類必須提供方法特徵作爲系統設計的抽象層,這個抽象層會遇見所有的可能出現的擴展,因此,在任何擴展情況下都不回去改變,這就讓系統的抽象層不需要修改,從而滿足開閉原則的第二條,對修改進行閉合。

同時,從抽象層裏面導出一個或者多個新的具體類可以改變系統的行爲,這樣就滿足了開閉原則的第一條。

儘管很多時候我們無法百分百的做到開閉原則,但是如果向着這個方向去努力,就能夠有部分的成功,這也是可以改善系統的結構的。

我是懿,一個正在被打擊還在努力前進的碼農。歡迎大家關注我們的公衆號,加入我們的知識星球,我們在知識星球中等着你的加入。

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