溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

面向?qū)ο笤O(shè)計(jì)的六大原則是什么

發(fā)布時(shí)間:2021-10-23 17:24:39 來(lái)源:億速云 閱讀:98 作者:iii 欄目:編程語(yǔ)言

這篇文章主要講解了“面向?qū)ο笤O(shè)計(jì)的六大原則是什么”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“面向?qū)ο笤O(shè)計(jì)的六大原則是什么”吧!

六大原則

言歸正傳,這是我學(xué)習(xí)設(shè)計(jì)模式系列的第一篇文章,本文主要講的是面向?qū)ο笤O(shè)計(jì)應(yīng)該遵循的六大原則,掌握這些原則能幫助我們更好的理解面向?qū)ο蟮母拍?,也能更好的理解設(shè)計(jì)模式。這六大原則分別是:

  • 單一職責(zé)原則——SRP

  • 開閉原則——OCP

  • 里式替換原則——LSP

  • 依賴倒置原則——DIP

  • 接口隔離原則——ISP

  • 迪米特原則——LOD

單一職責(zé)原則

單一職責(zé)原則,Single Responsibility Principle,簡(jiǎn)稱SRP。其定義是應(yīng)該有且僅有一個(gè)類引起類的變更,這話的意思就是一個(gè)類只擔(dān)負(fù)一個(gè)職責(zé)。

舉個(gè)例子,在創(chuàng)業(yè)公司里,由于人力成本控制和流程不夠規(guī)范的原因,往往一個(gè)人需要擔(dān)任N個(gè)職責(zé),一個(gè)工程師可能不僅要出需求,還要寫代碼,甚至要面談客戶,光背的鍋就好幾種,簡(jiǎn)單用代碼表達(dá)大概如此:

public class Engineer {public void makeDemand(){}public void writeCode(){}public void meetClient(){}
}

代碼看上去好像沒什么問題,因?yàn)槲覀兤綍r(shí)就是這么寫的啊,但是細(xì)讀一下就能發(fā)現(xiàn),這種寫法很明顯不符合單一職責(zé)的原則,因?yàn)橐痤惖淖兓恢挥幸粋€(gè),至少有三個(gè)方法都可以引起類的變化,比如有天因?yàn)闃I(yè)務(wù)需要,出需求的方法需要加個(gè)功能 (比如需求的成本分析),或者是見客戶也需要個(gè)參數(shù)之類的,那樣一來(lái)類的變化就會(huì)有多種可能性了,其他引用該類的類也需要相應(yīng)的變化,如果引用類的數(shù)目很多的話,代碼維護(hù)的成本可想而知會(huì)有多高。所以我們需要把這些方法拆分成獨(dú)立的職責(zé),可以讓一個(gè)類只負(fù)責(zé)一個(gè)方法,每個(gè)類只專心處理自己的方法即可。

單一職責(zé)原則的優(yōu)點(diǎn):

  • 類的復(fù)雜性降低,實(shí)現(xiàn)什么職責(zé)都有明確的定義;

  • 邏輯變得簡(jiǎn)單,類的可讀性提高了,而且,因?yàn)檫壿嫼?jiǎn)單,代碼的可維護(hù)性也提高了;

  • 變更的風(fēng)險(xiǎn)降低,因?yàn)橹粫?huì)在單一的類中的修改。

開閉原則

開閉原則,Open Closed Principle,是Java世界里最基礎(chǔ)的設(shè)計(jì)原則,其定義是:

一個(gè)軟件實(shí)體如類、模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉

也就是說(shuō),一個(gè)軟件實(shí)體應(yīng)該通過(guò)擴(kuò)展來(lái)實(shí)現(xiàn)變化,而不是通過(guò)修改已有的代碼實(shí)現(xiàn)變化。這是為軟件實(shí)體的未來(lái)事件而制定的對(duì)現(xiàn)行開發(fā)設(shè)計(jì)進(jìn)行約束的一個(gè)原則。

在我們編碼的過(guò)程中,需求變化是不斷的發(fā)生的,當(dāng)我們需要對(duì)代碼進(jìn)行修改時(shí),我們應(yīng)該盡量做到能不動(dòng)原來(lái)的代碼就不動(dòng),通過(guò)擴(kuò)展的方式來(lái)滿足需求。

遵循開閉原則的最好手段就是抽象,例如前面單一職責(zé)原則舉的工程師類,我們說(shuō)的是把方法抽離成單獨(dú)的類,每個(gè)類負(fù)責(zé)單一的職責(zé),但其實(shí)從開閉原則的角度說(shuō),更好的方式是把職責(zé)設(shè)計(jì)成接口,例如把寫代碼的職責(zé)方法抽離成接口的形式,同時(shí),我們?cè)谠O(shè)計(jì)之初需要考慮到未來(lái)所有可能發(fā)生變化的因素,比如未來(lái)有可能因?yàn)闃I(yè)務(wù)需要分成后臺(tái)和前端的功能,這時(shí)設(shè)計(jì)之初就可以設(shè)計(jì)成兩個(gè)接口,

public interface BackCode{	void writeCode();
}
public interface FrontCode{	void writeCode();
}

如果將來(lái)前端代碼的業(yè)務(wù)發(fā)生變化,我們只需擴(kuò)展前端接口的功能,或者修改前端接口的實(shí)現(xiàn)類即可,后臺(tái)接口以及實(shí)現(xiàn)類就不會(huì)受到影響,這就是抽象的好處。

里氏替換原則

里氏替換原則,英文名Liskov Substitution Principle,它的定義是

如果對(duì)每一個(gè)類型為T1的對(duì)象o1,都有類型為T2的對(duì)象o2,使得以T1定義的所有程序P在所有對(duì)象o1都替換成o2的時(shí)候,程序P的行為都沒有發(fā)生變化,那么類型T2是類型T1的子類型。

看起來(lái)有點(diǎn)繞口,它還有一個(gè)簡(jiǎn)單的定義:

所有引用基類的地方必須能夠透明地使用其子類的對(duì)象。

通俗點(diǎn)說(shuō),只要父類能出現(xiàn)的地方子類就可以出現(xiàn),而且替換為子類也不會(huì)產(chǎn)生任何異常。 但是反過(guò)來(lái)就不行了,因?yàn)樽宇惪梢詳U(kuò)展父類沒有的功能,同時(shí)子類還不能改變父類原有的功能。

我們都知道,面向?qū)ο蟮娜筇卣魇欠庋b、繼承和多態(tài),這三者缺一不可,但三者之間卻并不 “和諧“。因?yàn)槔^承有很多缺點(diǎn),當(dāng)子類繼承父類時(shí),雖然可以復(fù)用父類的代碼,但是父類的屬性和方法對(duì)子類都是透明的,子類可以隨意修改父類的成員。如果需求變更,子類對(duì)父類的方法進(jìn)行了一些復(fù)寫的時(shí)候,其他的子類可能就需要隨之改變,這在一定程度上就違反了封裝的原則,解決的方案就是引入里氏替換原則。

里氏替換原則為良好的繼承定義了一個(gè)規(guī)范,它包含了4層含義:

1、子類可以實(shí)現(xiàn)父類的抽象方法,但是不能覆蓋父類的非抽象方法。

2、子類可以有自己的個(gè)性,可以有自己的屬性和方法。

3、子類覆蓋或重載父類的方法時(shí)輸入?yún)?shù)可以被放大。

比如父類有一個(gè)方法,參數(shù)是HashMap

public class Father {public void test(HashMap map){
        System.out.println("父類被執(zhí)行。。。。。");
    }
}

那么子類的同名方法輸入?yún)?shù)的類型可以擴(kuò)大,例如我們輸入?yún)?shù)為Map,

public class Son extends Father{
    public void test(Map map){System.out.println("子類被執(zhí)行。。。。");
    }
}

我們寫一個(gè)場(chǎng)景類測(cè)試一下父類的方法執(zhí)行效果,

public class Client {public static void main(String[] args) {
        Father father = new Father();
        HashMap map = new HashMap();
        father.test(map);
    }
}

結(jié)果輸出:父類被執(zhí)行。。。。。

因?yàn)槔锸咸鎿Q原則,只要父類能出現(xiàn)的地方子類就可以出現(xiàn),而且替換為子類也不會(huì)產(chǎn)生任何異常。我們改下代碼,調(diào)用子類的方法,

public class Client {public static void main(String[] args) {
        Son son = new Son();
        HashMap map = new HashMap();
        father.test(map);
    }
}

運(yùn)行結(jié)果是一樣的,因?yàn)樽宇惙椒ǖ妮斎雲(yún)?shù)類型范圍擴(kuò)大了,子類代替父類傳遞到調(diào)用者中,子類的方法永遠(yuǎn)不會(huì)被執(zhí)行,這樣的結(jié)果其實(shí)是正確的,如果想讓子類方法執(zhí)行,可以重寫方法體。

反之,如果子類的輸入?yún)?shù)類型范圍比父類還小,比如父類中的參數(shù)是Map,而子類是HashMap,那么執(zhí)行上述代碼的結(jié)果就會(huì)是子類的方法體,有人說(shuō),這難道不對(duì)嗎?子類顯示自己的內(nèi)容啊。其實(shí)這是不對(duì)的,因?yàn)樽宇悰]有復(fù)寫父類的同名方法,方法就被執(zhí)行了,這會(huì)引起邏輯的混亂,如果父類是抽象類,子類是實(shí)現(xiàn)類,你傳遞一個(gè)這樣的實(shí)現(xiàn)類就違背了父類的意圖了,容易引起邏輯混亂,所以子類覆蓋或重載父類的方法時(shí)輸入?yún)?shù)必定是相同或者放大的。

4、子類覆蓋或重載父類的方法時(shí)輸出結(jié)果可以被縮小,也就是說(shuō)返回值要小于或等于父類的方法返回值。

確保程序遵循里氏替換原則可以要求我們的程序建立抽象,通過(guò)抽象去建立規(guī)范,然后用實(shí)現(xiàn)去擴(kuò)展細(xì)節(jié),所以,它跟開閉原則往往是相互依存的。

依賴倒置原則

依賴倒置原則,Dependence Inversion Principle,簡(jiǎn)稱DIP,它的定義是:

高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴其抽象;

抽象不應(yīng)該依賴細(xì)節(jié);

細(xì)節(jié)應(yīng)該依賴抽象;

什么是高層模塊和底層模塊呢?不可分割的原子邏輯就是底層模塊,原子邏輯的再組裝就是高層模塊。

在Java語(yǔ)言中,抽象就是指接口或抽象類,兩者都不能被實(shí)例化;而細(xì)節(jié)就是實(shí)現(xiàn)接口或繼承抽象類產(chǎn)生的類,也就是可以被實(shí)例化的實(shí)現(xiàn)類。依賴倒置原則是指模塊間的依賴是通過(guò)抽象來(lái)發(fā)生的,實(shí)現(xiàn)類之間不發(fā)生直接的依賴關(guān)系,其依賴關(guān)系是通過(guò)接口是來(lái)實(shí)現(xiàn)的,這就是俗稱的面向接口編程。

我們用歌手唱歌來(lái)舉例,比如一個(gè)歌手唱國(guó)語(yǔ)歌,用代碼表示就是:

public class ChineseSong {public String language() {return "國(guó)語(yǔ)歌";
    }
}public class Singer {//唱歌的方法public void sing(ChineseSong song) {
        System.out.println("歌手" + song.language());
    }
}public class Client {public static void main(String[] args) {
        Singer singer = new Singer();
        ChineseSong song = new ChineseSong();
        singer.sing(song);
    }
}

運(yùn)行main方法,結(jié)果就會(huì)輸出:歌手唱國(guó)語(yǔ)歌

現(xiàn)在,我們需要給歌手加一點(diǎn)難度,比如說(shuō)唱英文歌,在這個(gè)類中,我們發(fā)現(xiàn)是很難做的。因?yàn)槲覀僑inger類依賴于一個(gè)具體的實(shí)現(xiàn)類ChineseSong,也許有人會(huì)說(shuō)可以在加一個(gè)方法啊,但這樣一來(lái)我們就修改了Singer類了,如果以后需要增加更多的歌種,那歌手類不是一直要被修改?也就是說(shuō),依賴類已經(jīng)不穩(wěn)定了,這顯然不是我們想看到的。

所以我們需要用面向接口編程的思想來(lái)優(yōu)化我們的方案,改成如下的代碼:

public interface Song {public String language();
}public class ChineseSong implements Song{public String language() {return "唱國(guó)語(yǔ)歌";
    }
}public class EnglishSong implements Song {public String language() {return "唱英語(yǔ)歌";
    }
}public class Singer {//唱歌的方法public void sing(Song song) {
        System.out.println("歌手" + song.language());
    }
}public class Client {public static void main(String[] args) {
        Singer singer = new Singer();
        EnglishSong englishSong = new EnglishSong();// 唱英文歌singer.sing(englishSong);
    }
}

我們把歌單獨(dú)抽成一個(gè)接口Song,每個(gè)歌種都實(shí)現(xiàn)該接口并重寫方法,這樣一來(lái),歌手的代碼不必改動(dòng),如果需要添加歌的種類,只需寫多一個(gè)實(shí)現(xiàn)類繼承Song即可。

通過(guò)這樣的面向接口編程,我們的代碼就有了更好的擴(kuò)展性,同時(shí)也降低了耦合,提高了系統(tǒng)的穩(wěn)定性。

接口隔離原則

接口隔離原則,Interface Segregation Principle,簡(jiǎn)稱ISP,其定義是:

客戶端不應(yīng)該依賴它不需要的接口

意思就是客戶端需要什么接口就提供什么接口,把不需要的接口剔除掉,這就需要對(duì)接口進(jìn)行細(xì)化,保證接口的純潔性。換成另一種說(shuō)法就是,類間的依賴關(guān)系應(yīng)該建立在最小的接口上,也就是建立單一的接口。

你可能會(huì)疑惑,建立單一接口,這不是單一職責(zé)原則嗎?其實(shí)不是,單一職責(zé)原則要求的是類和接口職責(zé)單一,注重的是職責(zé),一個(gè)職責(zé)的接口是可以有多個(gè)方法的,而接口隔離原則要求的是接口的方法盡量少,模塊盡量單一,如果需要提供給客戶端很多的模塊,那么就要相應(yīng)的定義多個(gè)接口,不要把所有的模塊功能都定義在一個(gè)接口中,那樣會(huì)顯得很臃腫。

舉個(gè)例子,現(xiàn)在的智能手機(jī)非常的發(fā)達(dá),幾乎是人手一部的社會(huì)狀態(tài),在我們年輕人的觀念里,好的智能手機(jī)應(yīng)該是價(jià)格便宜,外觀好看,功能豐富的,由此我們可以定義一個(gè)智能手機(jī)的抽象接口 ISmartPhone,代碼如下所示:

public interface ISmartPhone {public void cheapPrice();public void goodLooking();public void richFunction();
}

接著,我們定義一個(gè)手機(jī)接口的實(shí)現(xiàn)類,實(shí)現(xiàn)這三個(gè)抽象方法,

public class SmartPhone implements ISmartPhone{public void cheapPrice() {
        System.out.println("這手機(jī)便宜~~~~~");
    }public void goodLooking() {
        System.out.println("這手機(jī)外觀好看~~~~~");
    }public void richFunction() {
        System.out.println("這手機(jī)功能真多~~~~~");
    }
}

然后,定義一個(gè)用戶的實(shí)體類 User,并定義一個(gè)構(gòu)造方法,以ISmartPhone 作為參數(shù)傳入,同時(shí),我們也定義一個(gè)使用的方法usePhone 來(lái)調(diào)用接口的方法,

public class User {private ISmartPhone phone;public User(ISmartPhone phone){this.phone = phone;
    }public void usePhone(){
        phone.cheapPrice();
        phone.goodLooking();
        phone.richFunction();
    }
}

可以看出,當(dāng)我們實(shí)例化User類并調(diào)用其方法usePhone后,控制臺(tái)上就會(huì)顯示手機(jī)接口三個(gè)方法的方法體信息,這種設(shè)計(jì)看上去沒什么大毛病,但是我們可以仔細(xì)想下,ISmartPhone這個(gè)接口的設(shè)計(jì)是否已經(jīng)達(dá)到最優(yōu)了呢?很遺憾,答案是沒有,接口其實(shí)還可以再優(yōu)化。

因?yàn)槌四贻p人之外,中年商務(wù)人士也在用智能手機(jī),在他們的觀念里,智能手機(jī)并不需要豐富的功能,甚至不用考慮是否便宜 (有錢就是任性~~~~),因?yàn)槌晒θ耸慷急容^忙,對(duì)智能手機(jī)的要求大多是外觀大氣,功能簡(jiǎn)單即可,這才是他們心中好的智能手機(jī)的特征,這樣一來(lái),我們定義的 ISmartPhone 接口就無(wú)法適用了,因?yàn)槲覀兊慕涌诙x了智能手機(jī)必須滿足三個(gè)特性,如果實(shí)現(xiàn)該接口就必須三個(gè)方法都實(shí)現(xiàn),而對(duì)商務(wù)人員的標(biāo)準(zhǔn)來(lái)說(shuō),我們定義的方法只有外觀符合且可以重用而已。你可能會(huì)說(shuō),我可以重寫一個(gè)實(shí)現(xiàn)類啊,只實(shí)現(xiàn)外觀的方法,另外兩個(gè)方法置空,什么都不寫,這不就行了嗎?但是這也不行,因?yàn)?nbsp;User 引用的是ISmartPhone 接口,它調(diào)用三個(gè)方法,你只實(shí)現(xiàn)了兩個(gè),那么打印信息就少了兩條了,只靠外觀的特性,使用者怎么知道智能手機(jī)是否符合自己的預(yù)期?

分析到這里,我們大概就明白了,其實(shí)ISmartPhone的設(shè)計(jì)是有缺陷的,過(guò)于臃腫了,按照接口隔離原則,我們可以根據(jù)不同的特性把智能手機(jī)的接口進(jìn)行拆分,這樣一來(lái),每個(gè)接口的功能就會(huì)變得單一,保證了接口的純潔性,也進(jìn)一步提高了代碼的靈活性和穩(wěn)定性。

迪米特原則

迪米特原則,Law of Demeter,簡(jiǎn)稱LoD,也被稱為最少知識(shí)原則,它描述的規(guī)則是:

一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象有最少的了解

也就是說(shuō),一個(gè)類應(yīng)該對(duì)自己需要耦合或調(diào)用的類知道的最少,類與類之間的關(guān)系越密切,耦合度越大,那么類的變化對(duì)其耦合的類的影響也會(huì)越大,這也是我們面向設(shè)計(jì)的核心原則:低耦合,高內(nèi)聚。

迪米特法則還有一個(gè)解釋:只與直接的朋友通信。

什么是直接的朋友呢?每個(gè)對(duì)象都必然與其他對(duì)象有耦合關(guān)系,兩個(gè)對(duì)象的耦合就成為朋友關(guān)系,這種關(guān)系的類型很多,例如組合、聚合、依賴等。其中,我們稱出現(xiàn)成員變量、方法參數(shù)、方法返回值中的類為直接的朋友,而出現(xiàn)在局部變量中的類則不是直接的朋友。也就是說(shuō),陌生的類最好不要作為局部變量的形式出現(xiàn)在類的內(nèi)部。

舉個(gè)例子,上體育課之前,老師讓班長(zhǎng)先去體務(wù)室拿20個(gè)籃球,等下上課的時(shí)候要用。根據(jù)這一場(chǎng)景,我們可以設(shè)計(jì)出三個(gè)類 Teacher(老師),Monitor (班長(zhǎng)) 和 BasketBall (籃球),以及發(fā)布命令的方法command 和 拿籃球的方法takeBall,

public class Teacher {// 命令班長(zhǎng)去拿球public void command(Monitor monitor) {
        List<BasketBall> ballList = new ArrayList<BasketBall>();// 初始化籃球數(shù)目for (int i = 0;i<20;i++){
            ballList.add(new BasketBall());
        }// 通知班長(zhǎng)開始去拿球monitor.takeBall(ballList);
    }
}public class BasketBall {
}public class Monitor {// 拿球public void takeBall(List<BasketBall> balls) {
        System.out.println("籃球數(shù)目:" + balls.size());
    }
}

然后,我們寫一個(gè)情景類進(jìn)行測(cè)試:

public class Client {public static void main(String[] args) {
        Teacher teacher = new Teacher();
        teacher.command(new Monitor());
    }
}

結(jié)果顯示如下:

籃球數(shù)目:20

雖然結(jié)果是正確的,但我們的程序其實(shí)還是存在問題,因?yàn)閺膱?chǎng)景來(lái)說(shuō),老師只需命令班長(zhǎng)拿籃球即可,Teacher只需要一個(gè)朋友----Monitor,但在程序里,Teacher的方法體中卻依賴了BasketBall類,也就是說(shuō),Teacher類與一個(gè)陌生的類有了交流,這樣Teacher的健壯性就被破壞了,因?yàn)橐坏〣asketBall類做了修改,那么Teacher也需要做修改,這很明顯違背了迪米特法則。

因此,我們需要對(duì)程序做些修改,在Teacher的方法中去掉對(duì)BasketBall類的依賴,只讓Teacher類與朋友類Monitor產(chǎn)生依賴,修改后的代碼如下:

public class Teacher {// 命令班長(zhǎng)去拿球public void command(Monitor monitor) {// 通知班長(zhǎng)開始去拿球monitor.takeBall();
    }
}public class Monitor {// 拿球public void takeBall() {
        List<BasketBall> ballList = new ArrayList<BasketBall>();// 初始化籃球數(shù)目for (int i = 0;i<20;i++){
            ballList.add(new BasketBall());
        }
        System.out.println("籃球數(shù)目:" + ballList.size());
    }
}

這樣一來(lái),Teacher類就不會(huì)與BasketBall類產(chǎn)生依賴了,即時(shí)日后因?yàn)闃I(yè)務(wù)需要修改BasketBall也不會(huì)影響Teacher類。

感謝各位的閱讀,以上就是“面向?qū)ο笤O(shè)計(jì)的六大原則是什么”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)面向?qū)ο笤O(shè)計(jì)的六大原則是什么這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI