優(yōu)雅代碼的秘密,都藏在這六個設(shè)計(jì)原則中
?前言
大家好,我是撿田螺的小男孩。
優(yōu)雅的代碼,猶如亭亭玉立的美女,讓人賞心悅目。而糟糕的代碼,卻猶如屎山,讓人避而遠(yuǎn)之。
如何寫出優(yōu)雅的代碼呢?那就要理解并熟悉應(yīng)用這6個設(shè)計(jì)原則啦:開閉原則、單一職責(zé)原則、接口隔離原則 、迪米特法則、里氏替換原則、依賴倒置原則。本文呢,將通過代碼demo,讓大家輕松理解這6個代碼設(shè)計(jì)原則,加油~
1. 開閉原則
開閉原則,即對擴(kuò)展開放,對修改關(guān)閉。
對于擴(kuò)展和修改,我們怎么去理解它呢?擴(kuò)展開放表示,未來業(yè)務(wù)需求是變化萬千,代碼應(yīng)該保持靈活的應(yīng)變能力。修改關(guān)閉表示不允許在原來類修改,保持穩(wěn)定性。
因?yàn)槿粘P枨笫遣粩嗟碌?,所以我們?jīng)常需要在原來的代碼中修改。如果代碼設(shè)計(jì)得不好,擴(kuò)展性不強(qiáng),每次需求迭代,都要在原來代碼中修改,很可能會引入bug。因此,我們的代碼應(yīng)該遵循開閉原則,也就是對擴(kuò)展開放,對修改關(guān)閉。
為了方便大家理解開閉原則,我們來看個例子:假設(shè)有這樣的業(yè)務(wù)場景,大數(shù)據(jù)系統(tǒng)把文件推送過來,根據(jù)不同類型采取不同的解析方式。多數(shù)的小伙伴就會寫出以下的代碼:
這段代碼有什么問題呢?
如果分支變多,這里的代碼就會變得臃腫,難以維護(hù),可讀性低。
如果你需要接入一種新的解析類型,那只能在原有代碼上修改。
顯然,增加、刪除某個邏輯,都需要修改到原來類的代碼,這就違反了開閉原則了。為了解決這個問題,我們可以使用策略模式去優(yōu)化它。
你可以先聲明一個文件解析的接口,如下:
然后實(shí)現(xiàn)不同策略的解析文件,如類型A解析:
如果未來需求變更的話,比如增加、刪除某個邏輯,不會再修改到原來的類啦,只需要修改對應(yīng)的文件解析類型的類即可。
對于如何使用設(shè)計(jì)模式,大家有興趣的話,可以看我以前的這篇文章哈:實(shí)戰(zhàn)!工作中常用到哪些設(shè)計(jì)模式
2. 單一職責(zé)原則
單一職責(zé)原則:一個類或者一個接口,最好只負(fù)責(zé)一項(xiàng)職責(zé)。比如一個類C?違反了單一原則,它負(fù)責(zé)兩個職責(zé)P1和P2?。當(dāng)職責(zé)P1?需要修改時(shí),就會改動到類C?,這就可能導(dǎo)致原本正常的P2也受影響。
如何更好理解呢?比如你實(shí)現(xiàn)一個圖書管理系統(tǒng),一個類既有圖書的增刪改查,又有讀者的增刪改查,你就可以認(rèn)為這個類違反了單一原則。因?yàn)檫@個類涉及了不同的功能職責(zé)點(diǎn),你可以把這個拆分。
以上圖書管理系統(tǒng)這個例子,違反單一原則,按業(yè)務(wù)拆分。這比較好理解,但是有時(shí)候,一個類并不是那么好區(qū)分。這時(shí)候大家可以看這個標(biāo)準(zhǔn),來判斷功能職責(zé)是否單一:
- 類中的私有方法過多
- 你很難給類起一個合適的名字
- 類中的代碼行數(shù)、函數(shù)或者屬性過多
- 類中大量的方法都是集中操作類中的某幾個屬性
- 類依賴的其他類過多,或者依賴類的其他類過多
比如,你寫了一個方法,這個方法包括了日期處理和借還書的業(yè)務(wù)操作,你就可以把日期處理抽到私有方法。再然后,如果你發(fā)現(xiàn),很多私有方法,都是類似的日期處理,你就可以把這個日期處理方法抽成一個工具類。
日常開發(fā)中,單一原則的思想都有體現(xiàn)的。比如微服務(wù)拆分。
3. 接口隔離原則
接口隔離原則:接口的調(diào)用者或者使用者,不應(yīng)該強(qiáng)迫依賴它不需要的接口。它要求建立單一的接口,不要建立龐大臃腫的接口,盡量細(xì)化接口,接口中的方法盡量少,讓接口中只包含客戶(調(diào)用者)感興趣的方法。即一個類對另一個類的依賴應(yīng)該建立在最小的接口上。
比如類A?通過接口I?依賴類B?,類C?通過接口I?依賴類D?,如果接口I?對于類A?和類B?來說,都不是最小接口,則類B?和類D必須去實(shí)現(xiàn)他們不需要的方法。如下圖:
這個圖表達(dá)的意思是:類A?依賴接口I?中的method1、method2?,類B是對類A依賴的實(shí)現(xiàn)。類C依賴接口I?中的method1、method3,類D是對類C依賴的實(shí)現(xiàn)。對于實(shí)現(xiàn)類B和D,它們都存在用不到的方法,但是因?yàn)閷?shí)現(xiàn)了接口I,所以必須要實(shí)現(xiàn)這些用不到的方法。
可以看下以下代碼:
大家可以發(fā)現(xiàn),如果接口過于臃腫,只要接口中出現(xiàn)的方法,不管對依賴于它的類有沒有用到,實(shí)現(xiàn)類都必須去實(shí)現(xiàn)這些方法。實(shí)現(xiàn)類B?沒用到method3?,它也要有個默認(rèn)實(shí)現(xiàn)。實(shí)現(xiàn)類D?沒用到method2,它也要有個默認(rèn)實(shí)現(xiàn)。
顯然,這不是一個好的設(shè)計(jì),違反了接口隔離原則。我們可以對接口I進(jìn)行拆分。拆分后的設(shè)計(jì)如圖2所示:
接口是不是分得越細(xì)越好呢?并不是。日常開發(fā)中,采用接口隔離原則對接口進(jìn)行約束時(shí),要注意以下幾點(diǎn):
接口盡量小,但是要有限度。對接口進(jìn)行細(xì)化可以提高程序設(shè)計(jì)靈活性是不掙的事實(shí),但是如果過小,則會造成接口數(shù)量過多,使設(shè)計(jì)復(fù)雜化。所以一定要適度。
為依賴接口的類定制服務(wù),只暴露給調(diào)用的類它需要的方法,它不需要的方法則隱藏起來。只有專注地為一個模塊提供定制服務(wù),才能建立最小的依賴關(guān)系。
提高內(nèi)聚,減少對外交互。使接口用最少的方法去完成最多的事情。運(yùn)用接口隔離原則,一定要適度,接口設(shè)計(jì)的過大或過小都不好。設(shè)計(jì)接口的時(shí)候,只有多花些時(shí)間去思考和籌劃,才能準(zhǔn)確地實(shí)踐這一原則。
4. 迪米特法則
定義:又叫最少知道原則。一個類對于其他類知道的越少越好,就是說一個對象應(yīng)當(dāng)對其他對象有盡可能少的了解,只和朋友談心,不和陌生人說話。它的核心思想就是,盡量降低類與類之間的耦合,盡最大能力減小代碼修改帶來的對原有的系統(tǒng)的影響。
比如一個生活例子:你對你的對象肯定了解的很多,但是如果你對別人的對象也了解很多,你的對象要是知道,那就要出大事了。
我們來看下一個違反迪米特法則的例子,業(yè)務(wù)場景是這樣的:一個學(xué)校,要求打印出所有師生的ID。
這塊代碼。問題出在類Principal?中,根據(jù)迪米特法則,只能與直接的朋友發(fā)生通信,而Student?類并不是Principal?類的直接朋友(以局部變量出現(xiàn)的耦合不屬于直接朋友),從邏輯上講校長Principal?只與管理者M(jìn)onitor?耦合就行了,可以讓Principal?繼承類Monitor?,重寫一個printMember的方法。優(yōu)化后的代碼如下:
5. 里氏替換原則
里氏替換原則:
如果對每一個類型為S?的對象o1?,都有類型為T?的對象o2?,使得以T?定義的所有程序P?在所有的對象o1?都代換成o2?時(shí),程序P?的行為沒有發(fā)生變化,那么類型S?是類型T的子類型。
一句話來描述就是:只要有父類出現(xiàn)的地方,都可以用子類來替代,而且不會出現(xiàn)任何錯誤和異常。 更通俗點(diǎn)講,就是子類可以擴(kuò)展父類的功能,但是不能改變父類原有的功能。
其實(shí),對里氏替換原則的定義可以總結(jié)如下:
- 子類可以實(shí)現(xiàn)父類的抽象方法,但不能覆蓋父類的非抽象方法
- 子類中可以增加自己特有的方法
- 當(dāng)子類的方法重載父類的方法時(shí),方法的前置條件(即方法的輸入?yún)?shù))要比父類的方法更寬松
- 當(dāng)子類的方法實(shí)現(xiàn)父類的方法時(shí)(重寫/重載或?qū)崿F(xiàn)抽象方法),方法的后置條件(即方法的的輸出/返回值)要比父類的方法更嚴(yán)格或相等
我們來看個例子:
這里例子是沒有違反里氏替換原則的,任何父類、父接口出現(xiàn)的地方子類都可以出現(xiàn)。如果給RedisCache加上參數(shù)校驗(yàn),如下:
這就違反了里氏替換原則了,因?yàn)樽宇惙椒ㄔ黾恿思恿藚?shù)校驗(yàn),拋出了異常,雖然子類仍然可以來替換父類。
6.依賴倒置原則
依賴倒置原則定義:
高層模塊不應(yīng)該依賴低層模塊,兩者都應(yīng)該依賴其抽象;抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象。它的核心思想是:要面向接口編程,而不要面向?qū)崿F(xiàn)編程。
依賴倒置原則可以降低類間的耦合性、提高系統(tǒng)的穩(wěn)定性、減少并行開發(fā)引起的風(fēng)險(xiǎn)、提高代碼的可讀性和可維護(hù)性。要滿足依賴倒置原則,我們需要在項(xiàng)目中滿足這個規(guī)則:
- 每個類盡量提供接口或抽象類,或者兩者都具備。
- 變量的聲明類型盡量是接口或者是抽象類。
- 任何類都不應(yīng)該從具體類派生。
- 使用繼承時(shí)盡量遵循里氏替換原則。
我們來看一段違反依賴倒置原則的代碼,業(yè)務(wù)需求是:顧客從淘寶購物。代碼如下:
以上代碼是存在問題的,如果未來產(chǎn)品變更需求,改為顧客從京東上購物,就需要把代碼修改為:
如果產(chǎn)品又變更為從天貓購物呢?那有得修改代碼了,顯然這違反了開閉原則?。顧客類設(shè)計(jì)時(shí),同具體的購物平臺類綁定了,這違背了依賴倒置原則??梢栽O(shè)計(jì)一個shop接口,不同購物平臺(如淘寶、京東)實(shí)現(xiàn)于這個接口,即修改顧客類面向該接口編程,就可以解決這個問題了。代碼如下: