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