阿里大佬告訴我,想學(xué)習(xí)設(shè)計(jì)模式,得先學(xué)好這些硬核技能
寫在前面
我們繼續(xù)學(xué)習(xí)架構(gòu)師技能,今天是本系列的第二篇,希望大家持續(xù)關(guān)注。
可能你不是科班出生,甚至大學(xué)都沒(méi)念,沒(méi)背景沒(méi)關(guān)系。我們只要每天進(jìn)步一點(diǎn)點(diǎn),一個(gè)月、兩個(gè)月、半年、一年......。
規(guī)劃性的學(xué)習(xí)一年半載后,你會(huì)覺(jué)得開始的你是多么的無(wú)知,如若不信,你可試試看!
只要你肯努力,遲早彎道超車!
坐穩(wěn)了,開始發(fā)車
網(wǎng)上都流行那么個(gè)段子:搞死一個(gè)程序員,不用動(dòng)刀或槍,只需要修改幾次需求。
開玩笑,確定嗎?
我也是程序員,也和相關(guān)人員互懟過(guò),不過(guò)都只是動(dòng)口不動(dòng)手。確實(shí),很多時(shí)候,相關(guān)人員提的需求確實(shí)有點(diǎn)過(guò)分,因?yàn)橐紤]到工期以及現(xiàn)有資源,有句話叫做“你給我做夠資源和時(shí)間,我可以給你造飛機(jī)火箭!”。
話又說(shuō)回來(lái),看這篇文章的相信都是程序員,咱們都是一路人。但,我想說(shuō)其實(shí)很多時(shí)候,我們也存在問(wèn)題。
比如:系統(tǒng)設(shè)計(jì)的可擴(kuò)展性、可維護(hù)性等,大家是否真的有認(rèn)真想過(guò),甚至部分人連軟件設(shè)計(jì)的七大原則都還搞不清楚。
不過(guò),很多人也是想好好設(shè)計(jì)的,但是無(wú)賴,給你工期不夠,否則,你需要加班,甚至加班估計(jì)夠嗆,從而也就導(dǎo)致系統(tǒng)逐漸變的非常難以維護(hù),臃腫,同樣的功能有n套代碼,到最后就是推翻了出個(gè)新版本(長(zhǎng)痛不如短痛)。
但,牛逼的人,只要工期不是很離譜,他們寫代碼永遠(yuǎn)是看起來(lái)非常舒服、還給你預(yù)留了很多擴(kuò)展口子、封裝了很多公用的工具類、抽象出了很多模型等等。
綜上,個(gè)人建議同行朋友,尤其是三年左右的,這時(shí)候,知識(shí)的廣度,深度都要有所涉及,同時(shí),系統(tǒng)設(shè)計(jì)或者某個(gè)模塊的設(shè)計(jì)也是體現(xiàn)你的能力的點(diǎn)(領(lǐng)導(dǎo)交給你的某個(gè)模塊,其實(shí)也可以理解為一個(gè)系統(tǒng),所以還是要認(rèn)真對(duì)待,大項(xiàng)目也是有多個(gè)模塊組成的)。希望大家一定多體會(huì)&&領(lǐng)會(huì)軟件設(shè)計(jì)的七大原則。
牛人們的總結(jié)
Robert C.Martin
一個(gè)可維護(hù)性(Maintainability)較低的軟件設(shè)計(jì),通常由于以下4個(gè)原因造成
1、過(guò)于僵化(Rigidity):設(shè)計(jì)難以修改
2、過(guò)于脆弱(Fragility):設(shè)計(jì)易遭到破壞(需要修改的時(shí)候,容易牽一發(fā)而動(dòng)全身,不該受到影響的代碼也被迫的破壞掉)
3、牢固性(Immobility):復(fù)用率低(當(dāng)想使用一些功能時(shí)會(huì)發(fā)現(xiàn)里面的某些代碼不是他想要的,想把這些代碼去掉時(shí),發(fā)現(xiàn)沒(méi)辦法去掉,原因是代碼耦合度太高了)
4、粘度過(guò)高(Viscosity):難以做正確事情(維護(hù)的過(guò)程中想進(jìn)行修改某些代碼,但是發(fā)現(xiàn)沒(méi)有辦法進(jìn)行修改,原因就是粘度太高)
PeterCoad
一個(gè)好的系統(tǒng)設(shè)計(jì)應(yīng)該具備如下三個(gè)特性:
- 可擴(kuò)展性(Extendibility)
- 靈活性(Flexibility)
- 可插入性(Pluggability)
面向?qū)ο笤O(shè)計(jì)原則和設(shè)計(jì)模式也是對(duì)系統(tǒng)進(jìn)行合理重構(gòu),重構(gòu)是在不改變軟件現(xiàn)有功能的基礎(chǔ)上,通過(guò)調(diào)整代碼改善軟件的質(zhì)量、性能,使其程序的設(shè)計(jì)模式和架構(gòu)更趨合理性,提高軟件的擴(kuò)展性和維護(hù)性。
軟件設(shè)計(jì)七大原則
- 開閉原則
- 依賴倒置原則
- 單一職責(zé)原則
- 接口隔離原則
- 迪米特法則
- 里氏替換原則
- 合成復(fù)用原則
關(guān)于這個(gè)七大設(shè)計(jì)原則,相信大部分人也聽說(shuō)過(guò),甚至很多朋友都學(xué)習(xí)過(guò),但是始終沒(méi)有掌握,希望通過(guò)本文的分享,不敢說(shuō)你一定能掌握,但是至少掌握部分。
本文主要內(nèi)容就是軟件設(shè)計(jì)的七大原則,重點(diǎn)在于用代碼來(lái)演示。
PS:七種原則并不是孤立存在的,他們相互依賴,相互補(bǔ)充。
開閉原則
開閉原則(Open Closed Principle,OCP)由勃蘭特·梅耶提出,他在 1988 年的著作《面向?qū)ο筌浖?gòu)造》中提出:
軟件實(shí)體應(yīng)當(dāng)對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉
這就是開閉原則的經(jīng)典定義。
在現(xiàn)實(shí)生活中,開閉原則也有體現(xiàn)。比如,很多互聯(lián)網(wǎng)公司都實(shí)行彈性制作息時(shí)間,規(guī)定每天工作8小時(shí)。意思就是,對(duì)于每天工作8小時(shí)這個(gè)規(guī)定是關(guān)閉的,但是什么時(shí)候來(lái)、什么時(shí)候走是開放的。早來(lái)早走,晚來(lái)晚走。
作用
開閉原則是面向?qū)ο蟪绦蛟O(shè)計(jì)的終極目標(biāo),它使軟件實(shí)體擁有一定的適應(yīng)性和靈活性的同時(shí)具備穩(wěn)定性和延續(xù)性。具體來(lái)說(shuō),其作用如下。
- 對(duì)軟件測(cè)試的影響:軟件遵守開閉原則的話,軟件測(cè)試時(shí)只需要對(duì)擴(kuò)展的代碼進(jìn)行測(cè)試就可以了,因?yàn)樵械臏y(cè)試代碼仍然能夠正常運(yùn)行。
- 可以提高代碼的可復(fù)用性:粒度越小,被復(fù)用的可能性就越大;在面向?qū)ο蟮某绦蛟O(shè)計(jì)中,根據(jù)原子和抽象編程可以提高代碼的可復(fù)用性。
- 可以提高軟件的可維護(hù)性:遵守開閉原則的軟件,其穩(wěn)定性高和延續(xù)性強(qiáng),從而易于擴(kuò)展和維護(hù)。
實(shí)際案例
報(bào)名一個(gè)網(wǎng)上課程,課程有價(jià)格、id、名稱。
//課程接口類 public interface ICourse { String getCourseName(); Integer getCourseId(); BigDecimal getCoursePrice(); } //整個(gè)課程生態(tài)有Java架構(gòu)、大數(shù)據(jù)、人工智能、前端、軟件測(cè)試等。 //我們創(chuàng)建一個(gè)Java架構(gòu)課程的類JavaCourse。 public class JavaCourse implements ICourse { @Override public String getCourseName() { return "JAVA課程"; } @Override public Integer getCourseId() { return 1; } @Override public BigDecimal getCoursePrice() { return new BigDecimal("599"); } } public class OpenCloseDemo { public static void main(String[] args) { ICourse course = new JavaCourse(); System.out.println("課程ID=" + course.getCourseId()); System.out.println("課程名稱=" + course.getCourseName()); System.out.println("課程價(jià)格=" + course.getCoursePrice()); } }
運(yùn)行OpenCloseDemo的main方法,結(jié)果:
課程ID=1 課程名稱=JAVA課程 課程價(jià)格=599
現(xiàn)在要給Java架構(gòu)課程做活動(dòng),價(jià)格優(yōu)惠,比如雙11、618等節(jié)日搞促銷活動(dòng)。如果修改JavaCourse中的getPrice()方法,則會(huì)存在一定風(fēng)險(xiǎn),可能影響其他地方的調(diào)用結(jié)果。
如何在不修改原有代碼的前提下,實(shí)現(xiàn)價(jià)格優(yōu)惠這個(gè)功能呢?我們?cè)賹懸粋€(gè)處理優(yōu)惠邏輯的類——JavaDiscountCourse類(可以思考一下為什么要叫JavaDiscountCourse,而不叫DiscountCourse)。
于是我們就這么干,增加一個(gè)java課程的打折類。
public class JavaDiscountCourse extends JavaCourse { public BigDecimal getDiscountCoursePrice(BigDecimal discount) { return super.getCoursePrice().multiply(discount); } } public class OpenCloseDemo { public static void main(String[] args) { JavaCourse course = new JavaDiscountCourse(); DiscountJavaCourse discountJavaCourse = (DiscountJavaCourse) course; System.out.println("課程ID=" + course.getCourseId()); System.out.println("課程名稱=" + course.getCourseName()); System.out.println("課程價(jià)格=" + course.getCoursePrice()); BigDecimal discount = new BigDecimal(0.5); System.out.println("課程折后價(jià)=" + discountJavaCourse.getDiscountCoursePrice()discount; } }
運(yùn)行結(jié)果:
課程ID=1 課程名稱=JAVA課程 課程價(jià)格=599 課程折后價(jià)=299.5
這樣的話,我們就沒(méi)必要?jiǎng)覬avaCourse這個(gè)類了,其他地方可能還在使用這個(gè)JavaCourse中的價(jià)格。
依賴倒置原則
定義
依賴倒置原則(Dependence Inversion Principle,DIP)指設(shè)計(jì)代碼結(jié)構(gòu)時(shí),高層模塊不應(yīng)該依賴底層模塊,二者都應(yīng)該依賴其抽象。抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象。通過(guò)依賴倒置,可以降低類與類之間的耦合性,提高系統(tǒng)的穩(wěn)定性,提高代碼的可讀性和可維護(hù)性,并降低修改程序帶來(lái)的風(fēng)險(xiǎn)。
案例
我們來(lái)看一個(gè)案例,還是以課程為例,首先創(chuàng)建一個(gè)類Tian。
public class Tian { public void studyJavaCourse(){ System.out.println("老田在學(xué)java課程"); } public void studyCCourse(){ System.out.println("老田在學(xué)C課程"); } }
然后編寫客戶端測(cè)試代碼并調(diào)用。
public static void main(String[] args) { Tian tian = new Tian(); tian.studyJavaCourse(); tian.studyCCourse(); }
隨著學(xué)習(xí)興趣的暴漲,老田還想學(xué)習(xí)AI課程。
這個(gè)時(shí)候,需要業(yè)務(wù)擴(kuò)展,代碼要從底層到高層(調(diào)用層)一次修改代碼。在Tian類中增加studyAICourse()的方法,在高層也要追加調(diào)用。如此一來(lái),在系統(tǒng)發(fā)布以后,實(shí)際上是非常不穩(wěn)定的,在修改代碼的同時(shí)會(huì)帶來(lái)意想不到的風(fēng)險(xiǎn)。因此我們優(yōu)化代碼,首先創(chuàng)建一個(gè)課程的抽象接口ICourse。
public interface ICourse { void study(); }
然后寫JavaCourse類。
public class JavaCourse implements ICourse { @Override public void study() { System.out.println("老田在學(xué)習(xí)java架構(gòu)師課程"); } }
再實(shí)現(xiàn)PythonCourse類。
public class PythonCourse implements ICourse { @Override public void study() { System.out.println("老田在學(xué)習(xí)Python課程"); } }
最后修改Tian類。
public class Tian { public void study(ICourse course) { course.study(); } }
來(lái)看客戶端測(cè)試代碼。
public static void main(String[] args) { Tian tian = new Tian(); ICourse course = new JavaCourse(); tian.study(course); }
這時(shí)候再看代碼,老田的興趣無(wú)論怎么暴漲,對(duì)于新的課程,只需要新建一個(gè)類,通過(guò)傳參的方式告訴Tian,而不需要修改底層代碼。實(shí)際上,這是一種大家非常熟悉的方式,叫作依賴注入。
- 構(gòu)造器注入方式
- Setter注入方式。
下面來(lái)看構(gòu)造器注入方式。
public class Tian { private ICourse course; /** 構(gòu)造函數(shù)方式注入course **/ public Tian(ICourse course) { this.course = course; } public void study() { course.study(); } }
來(lái)看客戶端代碼,將JavaCourse對(duì)象作為Tian對(duì)象的構(gòu)造參數(shù)注入。
public static void main(String[] args) { Tian tian = new Tian(new JavaCourse()); tian.study(); } }
根據(jù)構(gòu)造器注入方式,當(dāng)調(diào)用時(shí),每次都要?jiǎng)?chuàng)建實(shí)例。
但,如果Tian是全局單例,則只能選擇Setter注入方式,繼續(xù)修改Tian類的代碼。
public class Tian { private ICourse course; public void setCourse(ICourse course) { this.course = course; } public void study() { course.study(); } }
來(lái)看客戶端代碼,調(diào)用Tian對(duì)象的setCourse()方法,將JavaCourse對(duì)象作為參數(shù)。
public static void main(String[] args) { Tian tian = new Tian(); tian.setCourse(new JavaCourse()); tian.study(); tian.setCourse(new PythonCourse()); tian.study(); } }
注:
以抽象為基準(zhǔn)比以細(xì)節(jié)為基準(zhǔn)搭建起來(lái)的架構(gòu)要穩(wěn)定得多,因此大家在拿到需求后,要面向接口編程,按照先頂層再細(xì)節(jié)的順序設(shè)計(jì)代碼結(jié)構(gòu)。
單一職責(zé)原則
定義
單一職責(zé)原則的定義單一職責(zé)原則(Simple Responsibility Principle,SRP)指不要存在一個(gè)以上導(dǎo)致類變更的原因。
假設(shè)有一個(gè)Class負(fù)責(zé)兩個(gè)職責(zé),一旦發(fā)生需求變更,修改其中一個(gè)職責(zé)的邏輯代碼,有可能會(huì)導(dǎo)致另一個(gè)職責(zé)的功能發(fā)生故障。這樣一來(lái),這個(gè)Class就存在兩個(gè)導(dǎo)致類變更的原因。
如何解決這個(gè)問(wèn)題呢?我們就要分別用兩個(gè)Class來(lái)實(shí)現(xiàn)兩個(gè)職責(zé),進(jìn)行解耦。后期需求變更維護(hù)互不影響。這樣的設(shè)計(jì),可以降低類的復(fù)雜度,提高類的可讀性,提高系統(tǒng)的可維護(hù)性,降低變更引起的風(fēng)險(xiǎn)。
總體來(lái)說(shuō)就是一個(gè)Class、Interface、Method只負(fù)責(zé)一項(xiàng)職責(zé)。
案例
我們來(lái)看代碼實(shí)例,還是用課程舉例,我們的課程有直播課和錄播課。直播課不能快進(jìn)和快退,錄播課可以任意地反復(fù)觀看,功能職責(zé)不一樣。首先創(chuàng)建一個(gè)Course類。
public class ICourse { public void study(String courseName){ if("直播課".equals(courseName)){ System.out.println("不能快進(jìn)哦"); }else{ System.out.println("可以自定義播放速度,已經(jīng)來(lái)回播放"); } } }
然后看客戶端代碼,無(wú)論是直播課還是錄播課,都調(diào)用study()方法的邏輯。
public class Test1 { public static void main(String[] args) { Course course = new Course(); course.study("直播課"); course.study("看錄像"); } }
從上面代碼來(lái)看,Course類承擔(dān)了兩種處理邏輯。
假如,現(xiàn)在對(duì)課程進(jìn)行加密,那么直播課和錄播課的加密邏輯是不一樣的,必須修改代碼。
而修改代碼邏輯勢(shì)必會(huì)相互影響,容易帶來(lái)不可控的風(fēng)險(xiǎn)。
我們對(duì)職責(zé)進(jìn)行分離解耦,分別創(chuàng)建兩個(gè)類LiveCourse和ReplayCourse。
LiveCourse直播課程:
public class LiveCourse { public void study(String courseName){ System.out.println("現(xiàn)場(chǎng)直播,無(wú)法修改播放速度"); } }
ReplayCourse重播或錄像課程:
public class ReplayCourse { public void study(String courseName){ System.out.println("看錄像,可以隨便切換播放速度,以及來(lái)回播放"); } }
客戶端代碼如下,將直播課的處理邏輯調(diào)用LiveCourse類,錄播課的處理邏輯調(diào)用ReplayCourse類。
public class Test2 { public static void main(String[] args) { LiveCourse course = new LiveCourse(); course.study("直播課"); ReplayCourse replayCourse=new ReplayCourse(); replayCourse.study("錄播課"); } }
當(dāng)業(yè)務(wù)繼續(xù)發(fā)展時(shí),要對(duì)課程做權(quán)限。沒(méi)有付費(fèi)的學(xué)員可以獲得課程的基本信息,已經(jīng)付費(fèi)的學(xué)員可以獲得視頻流,即學(xué)習(xí)權(quán)限。
那么對(duì)于控制課程層面,至少有兩個(gè)職責(zé)。我們可以把展示職責(zé)和管理職責(zé)分離開,都實(shí)現(xiàn)同一個(gè)抽象依賴。
設(shè)計(jì)一個(gè)頂層接口,創(chuàng)建ICourse接口。
public interface ICourse { //獲得課程的基本信息 String getCourseName(); //獲取視頻流 byte[] getCourseVioeo(); //學(xué)習(xí)課程 void studyCourse(); //退款 void refundCourse(); }
可以把這個(gè)接口拆成兩個(gè)接口,創(chuàng)建一個(gè)接口ICourseInfo和ICourseManager。ICourseInfo接口的代碼如下。
ICourseInfo接口的代碼如下:
public interface ICourseInfo { //獲取課程名稱 String getCourseName(); //獲取課程視頻流 byte [] getCourseVideo(); }
ICourseManager接口的代碼如下:
public interface ICourseManager { //學(xué)習(xí)課程 void studyCourse(); //退款 void refundCourse(); }
下面來(lái)看方法層面的單一職責(zé)設(shè)計(jì)。有時(shí)候,為了偷懶,通常會(huì)把一個(gè)方法寫成下面這樣。
public void modifyUserInfo(String userName, String address){
userName = userName;
address = address;
}
還可能寫成這樣:
private void modifyUserInfo(String userName, String... fields){ userNmae = userName; } private void modifyUserInfo(String userName, String address,boolean flag){ if(flag){ //.... }else{ //.... } userName = userName; address = address; }
顯然,上面兩種寫法的modifyUserInfo()方法都承擔(dān)了多個(gè)職責(zé),既可以修改userName,也可以修改address,甚至更多,明顯不符合單一職責(zé)原則。那么我們做如下修改,把這個(gè)方法拆成兩個(gè)。
private void modifyUserName(String userName){ userName = userName; } private void modifyAddress(String address){ address = address; }
代碼在修改之后,開發(fā)起來(lái)簡(jiǎn)單,維護(hù)起來(lái)也容易。在實(shí)際項(xiàng)目中,代碼會(huì)存在依賴、組合、聚合關(guān)系,在項(xiàng)目開發(fā)過(guò)程中還受到項(xiàng)目的規(guī)模、周期、技術(shù)人員水平、對(duì)進(jìn)度把控的影響,導(dǎo)致很多類都不能滿足單一職責(zé)原則。但是,我們?cè)诰帉懘a的過(guò)程中,盡可能地讓接口和方法保持單一職責(zé),對(duì)項(xiàng)目后期的維護(hù)是有很大幫助的。
接口隔離原則
定義
接口隔離原則的定義接口隔離原則(Interface Segregation Principle,ISP)指用多個(gè)專門的接口,而不使用單一的總接口,客戶端不應(yīng)該依賴它不需要的接口。
這個(gè)原則指導(dǎo)我們?cè)谠O(shè)計(jì)接口時(shí),應(yīng)當(dāng)注意以下幾點(diǎn)。
(1)一個(gè)類對(duì)另一個(gè)類的依賴應(yīng)該建立在最小接口上。
(2)建立單一接口,不要建立龐大臃腫的接口。
(3)盡量細(xì)化接口,接口中的方法盡量少(不是越少越好,一定要適度)。
接口隔離原則符合“高聚合、低耦合”的設(shè)計(jì)思想,使得類具有很好的可讀性、可擴(kuò)展性和可維護(hù)性。在設(shè)計(jì)接口的時(shí)候,要多花時(shí)間思考,要考慮業(yè)務(wù)模型,包括還要對(duì)以后可能發(fā)生變更的地方做一些預(yù)判。所以,在實(shí)際開發(fā)中,我們對(duì)抽象、業(yè)務(wù)模型的理解是非常重要的。
案例
我們來(lái)寫一個(gè)動(dòng)物行為的抽象。
IAnimal接口的代碼如下:
public interface IAnimal{ //吃 void eat(); //飛 void fly(); //游泳 void swim(); }
Bird實(shí)現(xiàn)類代碼如如下:
public class Bird implements IAnimal{ //吃 @Override void eat(){ //小鳥吃東西 } //飛 @Override void fly(){ //小鳥在飛 } //游泳 void swim(){ //空著,因?yàn)樾▲B不游泳 } }
Dog類實(shí)現(xiàn)的代碼如下。
public class Dog implements IAnimal{ //吃 @Override void eat(){ //小狗吃東西 } //飛 @Override void fly(){ //空著,因?yàn)樾」凡粫?huì)飛 } //游泳 void swim(){ //小狗在游泳 } }
由上面代碼可以看出,Bird類的swim()方法可能只能空著,Dog類的fly()方法顯然是不可能的。這時(shí)候,我們針對(duì)不同動(dòng)物的行為來(lái)設(shè)計(jì)不同的接口,分別設(shè)計(jì)IEatAnimal、IFlyAnimal和ISwimAnimal接口。
IEatAnimal接口的代碼如下:
public interface IEatAnimal{ //吃 void eat(); }
IFlyAnimal接口的代碼如下:
public interface IFlyAnimal{ //飛 void fly(); }
ISwimAnimal接口的代碼如下:
public interface ISwimAnimal{ //游泳 void swim(); }
Dog只實(shí)現(xiàn)IEatAnimal和ISwimAnimal接口。
public class Dog implements IEatAnimal、ISwimAnimal{ //吃 @Override void eat(){ //小狗吃東西 } //游泳 void swim(){ //小狗在游泳 } }
迪米特法則
迪米特法則的定義
迪米特法則(Law of Demeter,LoD)又叫作最少知道原則(Least KnowledgePrinciple,LKP),指一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象保持最少的了解,盡量降低類與類之間的耦合。迪米特法則主要強(qiáng)調(diào)只和朋友交流,不和陌生人說(shuō)話。出現(xiàn)在成員變量、方法的輸入和輸出參數(shù)中的類都可以被稱為成員朋友類,而出現(xiàn)在方法體內(nèi)部的類不屬于朋友類。
案例
我們來(lái)設(shè)計(jì)一個(gè)權(quán)限系統(tǒng),TeamLeader需要查看目前發(fā)布到線上的課程數(shù)量。這時(shí)候,TeamLeader要讓Employee去進(jìn)行統(tǒng)計(jì),Employee再把統(tǒng)計(jì)結(jié)果告訴TeamLeader,來(lái)看代碼。
Course類的代碼如下:
public class Course{ }
Employee類的代碼如下:
public class Employee{ public void checkNumberOfCourse(ListcourseList){ System.out.println("目前已經(jīng)發(fā)布的課程數(shù)量是:" + courseList.size()); } }
TeamLeader類的代碼如下:
public class TeamLeader{ public void commandCheckNumber(Employee employee){ ListcourseList = new ArrayList(); for(int i=0;i<20;i++){ courseList.add(new Course()); } employee.checkNumberOfCourse(courseList); } }
客戶端測(cè)試代碼如下,將Employee對(duì)象作為參數(shù)傳送給TeamLeader對(duì)象
public static void main(String [] args){ TeamLeader teamLeader = new TeamLeader(); Employee employee = new Employee(); teamLeader.commandCheckNumber(employee); }
寫到這里,其實(shí)功能都已經(jīng)實(shí)現(xiàn),代碼看上去也沒(méi)什么問(wèn)題。根據(jù)迪米特法則,TeamLeader只想要結(jié)果,不需要跟Course產(chǎn)生直接交流。而Employee統(tǒng)計(jì)需要引用Course對(duì)象,TeamLeader和Course并不是朋友,從如下圖所示的類圖就可以看出來(lái)。
改造
Employee類的代碼如下。
public class Employee{ public void checkNumberOfCourse(ListcourseList){ ListcourseList = new ArrayList(); for(int i=0;i<20;i++){ courseList.add(new Course()); } System.out.println("目前已經(jīng)發(fā)布的課程數(shù)量是:" + courseList.size()); } }
TeamLeader類的代碼如下。
ublic class TeamLeader{ public void commandCheckNumber(Employee employee){ employee.checkNumberOfCourse(courseList); } }
學(xué)習(xí)軟件設(shè)計(jì)原則,千萬(wàn)不能形成強(qiáng)迫癥。當(dāng)碰到業(yè)務(wù)復(fù)雜的場(chǎng)景時(shí),需要隨機(jī)應(yīng)變。
里氏替換原則
定義
里氏替換原則(Liskov Substitution Principle,LSP)指如果對(duì)每一個(gè)類型為T1的對(duì)象O1,都有類型為T2的對(duì)象O2,使得以T1定義的所有程序P在所有對(duì)象O1都替換成O2時(shí),程序P的行為沒(méi)有發(fā)生變化,那么類型T2是類型T1的子類型。
定義看上去比較抽象,我們重新解釋一下,可以理解為一個(gè)軟件實(shí)體如果適用于一個(gè)父類,則一定適用于其子類,所有引用父類的地方必須能透明地使用其子類的對(duì)象,子類對(duì)象能夠替換父類對(duì)象,而程序邏輯不變。也可以理解為,子類可以擴(kuò)展父類的功能,但不能改變父類原有的功能。根據(jù)這個(gè)理解,我們對(duì)里氏替換原則的定義總結(jié)如下。
(1)子類可以實(shí)現(xiàn)父類的抽象方法,但不能覆蓋父類的非抽象方法。
(2)子類中可以增加自己特有的方法。
(3)當(dāng)子類的方法重載父類的方法時(shí),方法的前置條件(即方法的輸入?yún)?shù))要比父類的方法更寬松。
(4)當(dāng)子類的方法實(shí)現(xiàn)父類的方法時(shí)(重寫/重載或?qū)崿F(xiàn)抽象方法),方法的后置條件(即方法的輸出/返回值)要比父類的方法更嚴(yán)格或相等。
案例
在講開閉原則的時(shí)候,我們埋下了一個(gè)伏筆。我們?cè)讷@取折扣價(jià)格后重寫覆蓋了父類的getPrice()方法,增加了一個(gè)獲取源碼的方法getOriginPrice(),這顯然違背了里氏替換原則。我們修改一下代碼,不應(yīng)該覆蓋getPrice()方法,增加getDiscountPrice()方法。
public class JavaDiscountCourse extends JavaCourse{ public JavaDiscountCourse(Integer id, String name,Double price){ super(id,name,price); } public Double getDiscountPrice(){ return suer.getPrice()*0.5 } }
使用里氏替換原則有以下優(yōu)點(diǎn):
- 約束繼承泛濫,是開閉原則的一種體現(xiàn)。
- 加強(qiáng)程序的健壯性,同時(shí)變更時(shí)可以做到非常好的兼容性,提高程序的維護(hù)性、可擴(kuò)展性,降低需求變更時(shí)引入的風(fēng)險(xiǎn)。
現(xiàn)在來(lái)描述一個(gè)經(jīng)典的業(yè)務(wù)場(chǎng)景,用正方形、矩形和四邊形的關(guān)系說(shuō)明里氏替換原則,我們都知道正方形是一個(gè)特殊的長(zhǎng)方形,那么可以創(chuàng)建一個(gè)長(zhǎng)方形的父類Rectangle類,代碼如下。
public class Rectangle{ private long height; private long width; //set get方法省略 }
創(chuàng)建正方形Square類繼承長(zhǎng)方形,代碼如下:
public class Square extends Rectangle{ private long length; //length的get set方法 @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); } }
在測(cè)試類中,創(chuàng)建resize()方法。根據(jù)邏輯,長(zhǎng)方形的寬應(yīng)該大于等于高,我們讓高一直自增,直到高等于寬變成正方形,代碼如下。
public class Test { 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); } }
運(yùn)行結(jié)果:
width=20 height=11 width=20 height=12 width=20 height=13 width=20 height=14 width=20 height=15 width=20 height=16 width=20 height=17 width=20 height=18 width=20 height=19 width=20 height=20 width=20 height=21 Resize end,width=20 height=21
由運(yùn)行結(jié)果可知,高比寬還大,這在長(zhǎng)方形中是一種非常正常的情況。再來(lái)看下面的代碼,把長(zhǎng)方形替換成它的子類正方形,修改客戶端測(cè)試代碼如下。
public class Test { 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) { Square square = new Square(); square.setWidth(20); square.setHeight(10); resize(square); } }
此時(shí),運(yùn)行出現(xiàn)了死循環(huán),違背了里氏替換原則,在將父類替換為子類后,程序運(yùn)行結(jié)果沒(méi)有達(dá)到預(yù)期。因此,代碼設(shè)計(jì)是存在一定風(fēng)險(xiǎn)的。里氏替換原則只存在于父類與子類之間,約束繼承泛濫。
再來(lái)創(chuàng)建一個(gè)基于長(zhǎng)方形與正方形共同的抽象——四邊形QuardRangle接口,代碼如下。
public interface QuardRangle { long getHeight(); long getWidth(); }
修改長(zhǎng)方形Rectangle類的代碼如下。
public class Rectangle implements QuardRangle{ private long height; private long width; @Override public long getHeight() { return height; } public void setHeight(long height) { this.height = height; } @Override public long getWidth() { return width; } public void setWidth(long width) { this.width = width; } }
修改正方形Square類的代碼如下。
public class Square implements QuardRangle{ private long length; public long getLength() { return length; } public void setLength(long length) { this.length = length; } @Override public long getHeight(){ return length; } @Override public long getWidth(){ return length; } }
此時(shí),如果把resize()方法的參數(shù)換成四邊形QuardRangle類,方法內(nèi)部就會(huì)報(bào)錯(cuò)。因?yàn)檎叫我呀?jīng)沒(méi)有了setWidth()和setHeight()方法,所以,為了約束繼承泛濫,resize()方法的參數(shù)只能用長(zhǎng)方形Rectangle類。
合成復(fù)用原則
定義
合成復(fù)用原則(Composite/Aggregate Reuse Principle,CARP)指盡量使用對(duì)象組合(has-a)或?qū)ο缶酆希╟ontanis-a)的方式實(shí)現(xiàn)代碼復(fù)用,而不是用繼承關(guān)系達(dá)到代碼復(fù)用的目的。
合成復(fù)用原則可以使系統(tǒng)更加靈活,降低類與類之間的耦合度,一個(gè)類的變化對(duì)其他類造成的影響相對(duì)較小。繼承,又被稱為白箱復(fù)用,相當(dāng)于把所有實(shí)現(xiàn)細(xì)節(jié)暴露給子類。
組合/聚合又被稱為黑箱復(fù)用,對(duì)類以外的對(duì)象是無(wú)法獲取實(shí)現(xiàn)細(xì)節(jié)的。我們要根據(jù)具體的業(yè)務(wù)場(chǎng)景來(lái)做代碼設(shè)計(jì),其實(shí)也都需要遵循面向?qū)ο缶幊蹋∣bject OrientedProgramming,OOP)模型。
案例
還是以數(shù)據(jù)庫(kù)操作為例,首先創(chuàng)建DBConnection類。
public class DBConnection{ public String getConnection(){ return "數(shù)據(jù)庫(kù)連接"; } }
創(chuàng)建ProductDao類。
public class ProdcutDao{ private DBConnection dbConnection; public void setDBConnection(DBConnection dbConnection){ this.dbConnection=dbConnection; } public void addProduct(){ String conn=dbConnection.getConnection(); System.out.println("使用" + conn + "連接數(shù)據(jù)庫(kù)"); } }
這是一種非常典型的合成復(fù)用原則應(yīng)用場(chǎng)景。但是,對(duì)于目前的設(shè)計(jì)來(lái)說(shuō),DBConnection還不是一種抽象,不便于系統(tǒng)擴(kuò)展。目前的系統(tǒng)支持MySQL數(shù)據(jù)庫(kù)連接,假設(shè)業(yè)務(wù)發(fā)生變化,數(shù)據(jù)庫(kù)操作層要支持Oracle數(shù)據(jù)庫(kù)。
當(dāng)然,我們可以在DBConnection中增加對(duì)Oracle數(shù)據(jù)庫(kù)支持的方法,但是這違背了開閉原則。其實(shí),可以不必修改Dao的代碼,將DBConnection修改為abstract,代碼如下。
public abstract class DBConnection{ public abstract String getConnection(); }
然后將MySQL的邏輯抽離。
public class MySQLConnection extends DBConnection{ @Override public String getConnection(){ return "MySQL 數(shù)據(jù)庫(kù)連接"; } }
再創(chuàng)建Oracle支持的邏輯。
public class OracleConnection extends DBConnection{ @Override public String getConnection(){ return "Oracle 數(shù)據(jù)庫(kù)連接"; } }
總結(jié)
學(xué)習(xí)設(shè)計(jì)原則是學(xué)習(xí)設(shè)計(jì)模式的基礎(chǔ)。在實(shí)際開發(fā)過(guò)程中,并不是一定要求所有代碼都遵循設(shè)計(jì)原則,而是要綜合考慮人力、時(shí)間、成本、質(zhì)量,不刻意追求完美,要在適當(dāng)?shù)膱?chǎng)景遵循設(shè)計(jì)原則。這體現(xiàn)的是一種平衡取舍,可以幫助我們?cè)O(shè)計(jì)出更加優(yōu)雅的代碼結(jié)構(gòu)。
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!