一、單一職責原則(Single Responsibility Principle)
定義:一個類只負責一個功能領域中的相應職責,或者可以定義為:就一個類而言,應該只有一個引起它變化的原因。
問題由來:類T負責兩個不同的職責:職責P1,職責P2。當由于職責P1需求發(fā)生改變而需要修改類T時,有
可能會導致原本運行正常的職責P2功能發(fā)生故障。
單一職責原則告訴我們:一個類不能太“累”!在軟件系統(tǒng)中,一個類(大到模塊,小到方法)承擔的職責越多,它被復用的可能性就越小,而且一個類承擔的職責過多,就相當于將這些職責耦合在一起,當其中一個職責變化時,可能會影響其他職責的運作,因此要將這些職責進行分離,將不同的職責封裝在不同的類中,即將不同的變化原因封裝在不同的類中,如果多個職責總是同時發(fā)生改變則可將它們封裝在同一類中。
單一職責原則是實現(xiàn)高內聚、低耦合的指導方針,它是最簡單但又最難運用的原則,需要設計人員發(fā)現(xiàn)類的不同職責并將其分離,而發(fā)現(xiàn)類的多重職責需要設計人員具有較強的分析設計能力和相關實踐經驗。
SRP在類或接口中的使用:
/**
上面的的類圖對應的接口入下
*/
public interface IPhone{
//撥通電話
public void dial(String phoneNumber);
//通話
public void chat(Object o);
//掛斷電話
public void hangup();
}
在看到這個接口的時候,我們都會認為這樣的設計是沒有問題的,撥通電話,通話,掛斷電話寫在同一個接口里面并沒有什么錯。但是,我們仔細分析,這個接口真的沒有問題嗎?單一職責原則要求一個接口或類只有一個原因引起變化,也就是說一個接口或一個類只有一個原則,它就只負責一件事。 但我們分析上面這個接口,卻發(fā)現(xiàn)它包含了兩個職責:一個時協(xié)議管理,一個是數(shù)據(jù)傳送。dial()和hangup()兩個方法實現(xiàn)的是協(xié)議管理,分別是撥通電話和掛機。chat()實現(xiàn)的是數(shù)據(jù)傳送,把我們說的話轉換成模擬信號或數(shù)字信號傳遞給對方,然后再把對方傳遞過來的信號還原成我們聽得懂的語言。這里的協(xié)議接通和數(shù)據(jù)傳送的變化都會引起該接口或實現(xiàn)類的變化。我們想一想,這兩個職責會相互影響嗎?不管是什么協(xié)議,協(xié)議接通只負責將電話接通就行,而數(shù)據(jù)傳輸只需要傳輸數(shù)據(jù),不必要去管協(xié)議是如何接通的。所以通過分析,IPhone接口包含了兩個職責,而且這兩個職責的變化不互相影響,這就可以考慮分成兩個接口。
籠統(tǒng)地講:是否需要拆分取決于變化:
當變化發(fā)生,只影響其中一個職責,那就需要拆分
如果變化都影響到這兩個職責,那就不需要拆分。
SRP也適用于方法:
其實,單一職責原則不僅適用于類,接口,同樣適用于方法中。這要舉一個例子了,比如我們做項目的時候會遇到修改用戶信息這樣的功能模塊,我們一般的想法是將用戶的所有數(shù)據(jù)都接收過來,比如用戶名,信息,密碼,家庭地址等等,然后統(tǒng)一封裝到一個User對象中提交到數(shù)據(jù)庫,我們一般都是這么干的,就如下面這樣:
其實這樣的方法是不可取的,因為職責不明確,方法不明確,你到底是要修改密碼,還是修改用戶名,還是修改地址,還是都要修改?這樣職責不明確的話在與其他項目成員溝通的時候會產生很多麻煩,正確的設計如下:
循單一職責原的優(yōu)點有:
1.可以降低類的復雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多;
2. 提高類的可讀性,提高系統(tǒng)的可維護性;
3.變更引起的風險降低,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。
二.開閉原則(Open-Closed Principle, OCP)
定義:一個軟件實體應當對擴展開放,對修改關閉。即軟件實體應盡量在不修改原有代碼的情況下進行擴展
問題由來:任何軟件都需要面臨一個很重要的問題,即它們的需求會隨時間的推移而發(fā)生變化。因為變化,升級和維護等原因,如果需要對軟件原有代碼進行修改,可能會給舊代碼引入錯誤,也有可能會使我們不得不對整個功能進行重構,并且需要原有代碼經過重新測試,所以當軟件需要變化時,盡量通過擴展軟件實體的行為來實現(xiàn)變化,而不是通過修改已有的代碼來實現(xiàn)使我們需要的。
為了滿足開閉原則,需要對系統(tǒng)進行抽象化設計,抽象化是開閉原則的關鍵。在Java、C#等編程語言中,可以為系統(tǒng)定義一個相對穩(wěn)定的抽象層,而將不同的實現(xiàn)行為移至具體的實現(xiàn)層中完成。如果需要修改系統(tǒng)的行為,無須對抽象層進行任何改動,只需要增加新的具體類來實現(xiàn)新的業(yè)務功能即可,實現(xiàn)在不修改已有代碼的基礎上擴展系統(tǒng)的功能,達到開閉原則的要求。
舉例:
實現(xiàn)畫圖表的功能,如餅狀圖和柱狀圖等,為了支持多種圖表顯示方式,原始設計方案如圖下圖所示:
在ChartDisplay類的display()方法中存在如下代碼片段:
......
if (type.equals("pie")) {
PieChart chart = new PieChart();
chart.display();
}
else if (type.equals("bar")) {
BarChart chart = new BarChart();
chart.display();
}
......
在該代碼中,如果需要增加一個新的圖表類,如折線圖LineChart,則需要修改ChartDisplay類的display()方法的源代碼,增加新的判斷邏輯,違反了開閉原則。
現(xiàn)對該系統(tǒng)進行重構,使之符合開閉原則。
(1) 增加一個抽象圖表類AbstractChart,將各種具體圖表類作為其子類;
(2) ChartDisplay類針對抽象圖表類進行編程,由客戶端來決定使用哪種具體圖表。
重構后結構如圖2所示:
我們引入了抽象圖表類AbstractChart,且ChartDisplay針對抽象圖表類進行編程,并通過setChart()方法由客戶端來設置實例化的具體圖表對象,在ChartDisplay的display()方法中調用chart對象的display()方法顯示圖表。如果需要增加一種新的圖表,如折線圖LineChart,只需要將LineChart也作為AbstractChart的子類,在客戶端向ChartDisplay中注入一個LineChart對象即可,無須修改現(xiàn)有類庫的源代碼。
為什么使用開閉原則:
第一:開閉原則非常有名,只要是面向對象編程,在開發(fā)時都會強調開閉原則
第二:開閉原則是最基礎的設計原則,其它的五個設計原則都是開閉原則的具體形態(tài),也就是說其它的五個設計原則是指導設計的工具和方法,而開閉原則才是其精神領袖。依照Java語言的稱謂,開閉原則是抽象類,而其它的五個原則是具體的實現(xiàn)類。
第三:開閉原則可以提高復用性
在面向對象的設計中,所有的邏輯都是從原子邏輯組合而來,而不是在一個類中獨立實現(xiàn)一套業(yè)務邏輯。只有這樣的代碼才可以復用,邏輯粒度越小,被復用的可能性越大。為什么要復用呢?復用可以減少代碼的重復,避免相同的邏輯分散在多個角落,減少維護人員的工作量以及系統(tǒng)變化時產生bug的機會。怎么才能提高復用率呢?設計者需要縮小邏輯粒度,直到一個邏輯不可以分為止。
第四:開閉原則可以提高維護性
一款軟件量產后,維護人員的工作不僅僅對數(shù)據(jù)進行維護,還可能要對程序進行擴展,維護人員最樂意的事是擴展一個類,而不是修改一個類。讓維護人員讀懂原有代碼,再進行修改,是一件非常痛苦的事情,不要讓他在原有的代碼海洋中游蕩后再修改,那是對維護人員的折磨和摧殘。
第五:面向對象開發(fā)的要求
萬物皆對象,我們要把所有的事物抽象成對象,然后針對對象進行操作,但是萬物皆發(fā)展變化,有變化就要有策略去應對,怎么快速應對呢?這就需要在設計之初考慮到盡可能多變化的因素,然后留下接口,等待“可能”轉變?yōu)?ldquo;現(xiàn)實”。
如何使用開閉原則
第一:抽象約束
抽象是對一組事物的通用描述,沒有具體的實現(xiàn),也就表示它可以有非常多的可能性,可以跟隨需求的變化而變化。因此,通過接口或抽象類可以約束一組可能變化的行為,并且能夠實現(xiàn)對擴展開放,其包含三層含義:
1.通過接口或抽象類約束擴散,對擴展進行邊界限定,不允許出現(xiàn)在接口或抽象類中不存在的public方法。
2.參數(shù)類型,引用對象盡量使用接口或抽象類,而不是實現(xiàn)類,這主要是實現(xiàn)里氏替換原則的一個要求
3.抽象層盡量保持穩(wěn)定,一旦確定就不要修改
第二:元數(shù)據(jù)(metadata)控件模塊行為
編程是一個很苦很累的活,那怎么才能減輕壓力呢?答案是盡量使用元數(shù)據(jù)來控制程序的行為,減少重復開發(fā)。什么是元數(shù)據(jù)?用來描述環(huán)境和數(shù)據(jù)的數(shù)據(jù),通俗的說就是配置參數(shù),參數(shù)可以從文件中獲得,也可以從數(shù)據(jù)庫中獲得。
第三:制定項目章程
在一個團隊中,建立項目章程是非常重要的,因為章程是所有開發(fā)人員都必須遵守的約定,對項目來說,約定優(yōu)于配置。這比通過接口或抽象類進行約束效率更高,而擴展性一點也沒有減少
第四:封裝變化
對變化封裝包含兩層含義:
(1)將相同的變化封裝到一個接口或抽象類中
(2)將不同的變化封裝到不同的接口或抽象類中,不應該有兩個不同的變化出現(xiàn)在同一個接口或抽象類中。 封裝變化,也就是受保護的變化,找出預計有變化或不穩(wěn)定的點,我們?yōu)檫@些變化點創(chuàng)建穩(wěn)定的接口。
三、里氏代換原則(Liskov Substitution Principle, LSP)
定義:里氏代換原則(Liskov Substitution Principle, LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象。
繼承優(yōu)點
代碼共享,減少創(chuàng)建類的工作量,每個子類都擁有父類的方法和屬性;提高代碼的重用性;子類可以形似父類,但又異于父類;提高代碼的可擴展性,實現(xiàn)父類的方法就可以“為所欲為”了;提高產品或項目的開放性。
繼承缺點
繼承是侵入性的。只要繼承,就必須擁有父類的所有屬性和方法;降低代碼的靈活性。子類必須擁有父類的屬性和方法;
增強了耦合性。當父類的常量、變量和方法被修改時,必需要考慮子類的修改,而且在缺乏規(guī)范的環(huán)境下,這種修改可能帶來非常糟糕的結果;大片的代碼需要重構。
克服繼承的缺點——里氏替換原則
從整體上來看,利大于弊。
里氏代換原則告訴我們,在軟件中將一個基類對象替換成它的子類對象,程序將不會產生任何錯誤和異常,反過來則不成立,如果一個軟件實體使用的是一個子類對象的話,那么它不一定能夠使用基類對象。例如:我喜歡動物,那我一定喜歡狗,因為狗是動物的子類;但是我喜歡狗,不能據(jù)此斷定我喜歡動物,因為我并不喜歡老鼠,雖然它也是動物。
里氏代換原則是實現(xiàn)開閉原則的重要方式之一,由于使用基類對象的地方都可以使用子類對象,因此在程序中盡量使用基類類型來對對象進行定義,而在運行時再確定其子類類型,用子類對象來替換父類對象。
在使用里氏代換原則時需要注意如下幾個問題:
(1)子類的所有方法必須在父類中聲明,或子類必須實現(xiàn)父類中聲明的所有方法。根據(jù)里氏代換原則,為了保證系統(tǒng)的擴展性,在程序中通常使用父類來進行定義,如果一個方法只存在子類中,在父類中不提供相應的聲明,則無法在以父類定義的對象中使用該方法。
(2) 我們在運用里氏代換原則時,盡量把父類設計為抽象類或者接口,讓子類繼承父類或實現(xiàn)父接口,并實現(xiàn)在父類中聲明的方法,運行時,子類實例替換父類實例,我們可以很方便地擴展系統(tǒng)的功能,同時無須修改原有子類的代碼,增加新的功能可以通過增加一個新的子類來實現(xiàn)。里氏代換原則是開閉原則的具體實現(xiàn)手段之一。
(3) Java語言中,在編譯階段,Java編譯器會檢查一個程序是否符合里氏代換原則,這是一個與實現(xiàn)無關的、純語法意義上的檢查,但Java編譯器的檢查是有局限的
系統(tǒng)需要提供一個發(fā)送Email的功能,客戶(Customer)可以分為VIP客戶(VIPCustomer)和普通客戶(CommonCustomer)兩類,原始設計方案如圖所示:
在對系統(tǒng)進行進一步分析后發(fā)現(xiàn),無論是普通客戶還是VIP客戶,發(fā)送郵件的過程都是相同的,也就是說兩個send()方法中的代碼重復,而且在本系統(tǒng)中還將增加新類型的客戶。為了讓系統(tǒng)具有更好的擴展性,同時減少代碼重復,使用里氏代換原則對其進行重構。
在本實例中,可以考慮增加一個新的抽象客戶類Customer,而將CommonCustomer和VIPCustomer類作為其子類,郵件發(fā)送類EmailSender類針對抽象客戶類Customer編程,根據(jù)里氏代換原則,能夠接受基類對象的地方必然能夠接受子類對象,因此將EmailSender中的send()方法的參數(shù)類型改為Customer,如果需要增加新類型的客戶,只需將其作為Customer類的子類即可。重構后的結構如圖所示:
里氏代換原則是實現(xiàn)開閉原則的重要方式之一。在本實例中,在傳遞參數(shù)時使用基類對象,除此以外,在定義成員變量、定義局部變量、確定方法返回類型時都可使用里氏代換原則。針對基類編程,在程序運行時再確定具體子類。
四、依賴倒置原則(Dependence Inversion Principle,DIP)
定義:
高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;抽象不應該依賴細節(jié),細節(jié)應該依賴抽象,其核心思想是:要面向接口編程,不要面向實現(xiàn)編程。
依賴倒轉原則要求我們在程序代碼中傳遞參數(shù)時或在關聯(lián)關系中,盡量引用層次高的抽象層類,即使用接口和抽象類進行變量類型聲明、參數(shù)類型聲明、方法返回類型聲明,以及數(shù)據(jù)類型的轉換等,而不要用具體類來做這些事情。為了確保該原則的應用,一個具體類應當只實現(xiàn)接口或抽象類中聲明過的方法,而不要給出多余的方法,否則將無法調用到在子類中增加的新方法。
在引入抽象層后,系統(tǒng)將具有很好的靈活性,在程序中盡量使用抽象層進行編程,而將具體類寫在配置文件中,這樣一來,如果系統(tǒng)行為發(fā)生變化,只需要對抽象層進行擴展,并修改配置文件,而無須修改原有系統(tǒng)的源代碼,在不修改的情況下來擴展系統(tǒng)的功能,滿足開閉原則的要求。
在實現(xiàn)依賴倒轉原則時,我們需要針對抽象層編程,而將具體類的對象通過依賴注入(DependencyInjection, DI)的方式注入到其他對象中,依賴注入是指當一個對象要與其他對象發(fā)生依賴關系時,通過抽象來注入所依賴的對象。常用的注入方式有三種,分別是:構造注入,設值注入(Setter注入)和接口注入。構造注入是指通過構造函數(shù)來傳入具體類的對象,設值注入是指通過Setter方法來傳入具體類的對象,而接口注入是指通過在接口中聲明的業(yè)務方法來傳入具體類的對象。這些方法在定義時使用的是抽象類型,在運行時再傳入具體類型的對象,由子類對象來覆蓋父類對象。
依賴倒置原則的作用
(1)依賴倒置原則可以降低類間的耦合性。
(2)依賴倒置原則可以提高系統(tǒng)的穩(wěn)定性。
(3)依賴倒置原則可以減少并行開發(fā)引起的風險。
(4)依賴倒置原則可以提高代碼的可讀性和可維護性。
依賴倒置原則的實現(xiàn)方法
依賴倒置原則的目的是通過要面向接口的編程來降低類間的耦合性,所以我們在實際編程中只要遵循以下4點,就能在項目中滿足這個規(guī)則。
(1)每個類盡量提供接口或抽象類,或者兩者都具備。
(2)變量的聲明類型盡量是接口或者是抽象類。
(3)任何類都不應該從具體類派生。
(4)使用繼承時盡量遵循里氏替換原則
例子:
現(xiàn)需要將存儲在TXT或Excel文件中的客戶信息轉存到數(shù)據(jù)庫中,因此需要進行數(shù)據(jù)格式轉換。在客戶數(shù)據(jù)操作類中將調用數(shù)據(jù)格式轉換類的方法實現(xiàn)格式轉換和數(shù)據(jù)庫插入操作,初始設計方案結構如圖所示:
在編碼實現(xiàn)圖所示結構時,Sunny軟件公司開發(fā)人員發(fā)現(xiàn)該設計方案存在一個非常嚴重的問題,由于每次轉換數(shù)據(jù)時數(shù)據(jù)來源不一定相同,因此需要更換數(shù)據(jù)轉換類,如有時候需要將TXTDataConvertor改為ExcelDataConvertor,此時,需要修改CustomerDAO的源代碼,而且在引入并使用新的數(shù)據(jù)轉換類時也不得不修改CustomerDAO的源代碼,系統(tǒng)擴展性較差,違反了開閉原則,現(xiàn)需要對該方案進行重構。
在本實例中,由于CustomerDAO針對具體數(shù)據(jù)轉換類編程,因此在增加新的數(shù)據(jù)轉換類或者更換數(shù)據(jù)轉換類時都不得不修改CustomerDAO的源代碼。我們可以通過引入抽象數(shù)據(jù)轉換類解決該問題,在引入抽象數(shù)據(jù)轉換類DataConvertor之后,CustomerDAO針對抽象類DataConvertor編程,而將具體數(shù)據(jù)轉換類名存儲在配置文件中,符合依賴倒轉原則。根據(jù)里氏代換原則,程序運行時,具體數(shù)據(jù)轉換類對象將替換DataConvertor類型的對象,程序不會出現(xiàn)任何問題。更換具體數(shù)據(jù)轉換類時無須修改源代碼,只需要修改配置文件;如果需要增加新的具體數(shù)據(jù)轉換類,只要將新增數(shù)據(jù)轉換類作為DataConvertor的子類并修改配置文件即可,原有代碼無須做任何修改,滿足開閉原則。重構后的結構如圖所示:
在上述重構過程中,我們使用了開閉原則、里氏代換原則和依賴倒轉原則,在大多數(shù)情況下,這三個設計原則會同時出現(xiàn),開閉原則是目標,里氏代換原則是基礎,依賴倒轉原則是手段,它們相輔相成,相互補充,目標一致,只是分析問題時所站角度不同而已。
五、接口隔離原則(Interface Segregation Principle, ISP)
定義:使用多個專門的接口,而不使用單一的總接口,即客戶端不應該依賴那些它不需要的接口。
根據(jù)接口隔離原則,當一個接口太大時,我們需要將它分割成一些更細小的接口,使用該接口的客戶端僅需知道與之相關的方法即可。每一個接口應該承擔一種相對獨立的角色,不干不該干的事,該干的事都要干。這里的“接口”往往有兩種不同的含義:一種是指一個類型所具有的方法特征的集合,僅僅是一種邏輯上的抽象;另外一種是指某種語言具體的“接口”定義,有嚴格的定義和結構,比如Java語言中的interface。對于這兩種不同的含義,ISP的表達方式以及含義都有所不同:
(1) 當把“接口”理解成一個類型所提供的所有方法特征的集合的時候,這就是一種邏輯上的概念,接口的劃分將直接帶來類型的劃分??梢园呀涌诶斫獬山巧?,一個接口只能代表一個角色,每個角色都有它特定的一個接口,此時,這個原則可以叫做“角色隔離原則”。
(2) 如果把“接口”理解成狹義的特定語言的接口,那么ISP表達的意思是指接口僅僅提供客戶端需要的行為,客戶端不需要的行為則隱藏起來,應當為客戶端提供盡可能小的單獨的接口,而不要提供大的總接口。在面向對象編程語言中,實現(xiàn)一個接口就需要實現(xiàn)該接口中定義的所有方法,因此大的總接口使用起來不一定很方便,為了使接口的職責單一,需要將大接口中的方法根據(jù)其職責不同分別放在不同的小接口中,以確保每個接口使用起來都較為方便,并都承擔某一單一角色。接口應該盡量細化,同時接口中的方法應該盡量少,每個接口中只包含一個客戶端(如子模塊或業(yè)務邏輯類)所需的方法即可,這種機制也稱為“定制服務”,即為不同的客戶端提供寬窄不同的接口。
接口隔離原則和單一職責都是為了提高類的內聚性、降低它們之間的耦合性,體現(xiàn)了封裝的思想,但兩者是不同的:
(1)單一職責原則注重的是職責,而接口隔離原則注重的是對接口依賴的隔離。
(2)單一職責原則主要是約束類,它針對的是程序中的實現(xiàn)和細節(jié);接口隔離原則主要約束接口,主要針對抽象和程序整體框架的構建。
接口隔離原則的優(yōu)點
接口隔離原則是為了約束接口、降低類對接口的依賴性,遵循接口隔離原則有以下 5 個優(yōu)點。
(1)將臃腫龐大的接口分解為多個粒度小的接口,可以預防外來變更的擴散,提高系統(tǒng)的靈活性和可維護性。
(2)接口隔離提高了系統(tǒng)的內聚性,減少了對外交互,降低了系統(tǒng)的耦合性。
(3)如果接口的粒度大小定義合理,能夠保證系統(tǒng)的穩(wěn)定性;但是,如果定義過小,則會造成接口數(shù)量過多,使設計復雜化;如果定義太大,靈活性降低,無法提供定制服務,給整體項目帶來無法預料的風險。
(4)使用多個專門的接口還能夠體現(xiàn)對象的層次,因為可以通過接口的繼承,實現(xiàn)對總接口的定義。
(5)能減少項目工程中的代碼冗余。過大的大接口里面通常放置許多不用的方法,當實現(xiàn)這個接口的時候,被迫設計冗余的代碼。
接口隔離原則的實現(xiàn)方法
在具體應用接口隔離原則時,應該根據(jù)以下幾個規(guī)則來衡量。
(1)接口盡量小,但是要有限度。一個接口只服務于一個子模塊或業(yè)務邏輯。
(2)為依賴接口的類定制服務。只提供調用者需要的方法,屏蔽不需要的方法。
(3)了解環(huán)境,拒絕盲從。每個項目或產品都有選定的環(huán)境因素,環(huán)境不同,接口拆分的標準就不同深入了解業(yè)務邏輯。
(4)提高內聚,減少對外交互。使接口用最少的方法去完成最多的事情。
下面通過一個簡單實例來加深對接口隔離原則的理解:
Sunny軟件公司開發(fā)人員針對某CRM系統(tǒng)的客戶數(shù)據(jù)顯示模塊設計了如圖1所示接口,其中方法dataRead()用于從文件中讀取數(shù)據(jù),方法transformToXML()用于將數(shù)據(jù)轉換成XML格式,方法createChart()用于創(chuàng)建圖表,方法displayChart()用于顯示圖表,方法createReport()用于創(chuàng)建文字報表,方法displayReport()用于顯示文字報表。
在實際使用過程中發(fā)現(xiàn)該接口很不靈活,例如如果一個具體的數(shù)據(jù)顯示類無須進行數(shù)據(jù)轉換(源文件本身就是XML格式),但由于實現(xiàn)了該接口,將不得不實現(xiàn)其中聲明的transformToXML()方法(至少需要提供一個空實現(xiàn));如果需要創(chuàng)建和顯示圖表,除了需實現(xiàn)與圖表相關的方法外,還需要實現(xiàn)創(chuàng)建和顯示文字報表的方法,否則程序編譯時將報錯。
現(xiàn)使用接口隔離原則對其進行重構。
在圖中,由于在接口CustomerDataDisplay中定義了太多方法,即該接口承擔了太多職責,一方面導致該接口的實現(xiàn)類很龐大,在不同的實現(xiàn)類中都不得不實現(xiàn)接口中定義的所有方法,靈活性較差,如果出現(xiàn)大量的空方法,將導致系統(tǒng)中產生大量的無用代碼,影響代碼質量;另一方面由于客戶端針對大接口編程,將在一定程序上破壞程序的封裝性,客戶端看到了不應該看到的方法,沒有為客戶端定制接口。因此需要將該接口按照接口隔離原則和單一職責原則進行重構,將其中的一些方法封裝在不同的小接口中,確保每一個接口使用起來都較為方便,并都承擔某一單一角色,每個接口中只包含一個客戶端(如模塊或類)所需的方法即可。
通過使用接口隔離原則,本實例重構后的結構如圖所示:
在使用接口隔離原則時,我們需要注意控制接口的粒度,接口不能太小,如果太小會導致系統(tǒng)中接口泛濫,不利于維護;接口也不能太大,太大的接口將違背接口隔離原則,靈活性較差,使用起來很不方便。一般而言,接口中僅包含為某一類用戶定制的方法即可,不應該強迫客戶依賴于那些它們不用的方法。
六、迪米特法則(Law of Demeter, LoD)
定義:迪米特法則(Law of Demeter, LoD):一個軟件實體應當盡可能少地與其他實體發(fā)生相互作用。
迪米特法則(Law of Demeter,LoD)又叫作最少知識原則(Least Knowledge Principle,LKP),產生于 1987 年美國東北大學(Northeastern University)的一個名為迪米特(Demeter)的研究項目,由伊恩·荷蘭(Ian Holland)提出,被 UML 創(chuàng)始者之一的布奇(Booch)普及,后來又因為在經典著作《程序員修煉之道》(The Pragmatic Programmer)提及而廣為人知。
迪米特法則的定義是:只與你的直接朋友交談,不跟“陌生人”說話(Talk only to your immediate friends and not to strangers)。其含義是:如果兩個軟件實體無須直接通信,那么就不應當發(fā)生直接的相互調用,可以通過第三方轉發(fā)該調用。其目的是降低類之間的耦合度,提高模塊的相對獨立性。
迪米特法則中的“朋友”是指:當前對象本身、當前對象的成員對象、當前對象所創(chuàng)建的對象、當前對象的方法參數(shù)等,這些對象同當前對象存在關聯(lián)、聚合或組合關系,可以直接訪問這些對象的方法。
在應用迪米特法則時,一個對象只能與直接朋友發(fā)生交互,不要與“陌生人”發(fā)生直接交互,這樣做可以降低系統(tǒng)的耦合度,一個對象的改變不會給太多其他對象帶來影響。
迪米特法則要求我們在設計系統(tǒng)時,應該盡量減少對象之間的交互,如果兩個對象之間不必彼此直接通信,那么這兩個對象就不應當發(fā)生任何直接的相互作用,如果其中的一個對象需要調用另一個對象的某一個方法的話,可以通過第三者轉發(fā)這個調用。簡言之,就是通過引入一個合理的第三者來降低現(xiàn)有對象之間的耦合度。
在將迪米特法則運用到系統(tǒng)設計中時,要注意下面的幾點:在類的劃分上,應當盡量創(chuàng)建松耦合的類,類之間的耦合度越低,就越有利于復用,一個處在松耦合中的類一旦被修改,不會對關聯(lián)的類造成太大波及;在類的結構設計上,每一個類都應當盡量降低其成員變量和成員函數(shù)的訪問權限;在類的設計上,只要有可能,一個類型應當設計成不變類;在對其他類的引用上,一個對象對其他對象的引用應當降到最低。
迪米特法則的優(yōu)點
迪米特法則要求限制軟件實體之間通信的寬度和深度,正確使用迪米特法則將有以下兩個優(yōu)點。
降低了類之間的耦合度,提高了模塊的相對獨立性。
由于親合度降低,從而提高了類的可復用率和系統(tǒng)的擴展性。
但是,過度使用迪米特法則會使系統(tǒng)產生大量的中介類,從而增加系統(tǒng)的復雜性,使模塊之間的通信效率降低。所以,在釆用迪米特法則時需要反復權衡,確保高內聚和低耦合的同時,保證系統(tǒng)的結構清晰
迪米特法則的實現(xiàn)方法
從迪米特法則的定義和特點可知,它強調以下兩點:
從依賴者的角度來說,只依賴應該依賴的對象。
從被依賴者的角度說,只暴露應該暴露的方法。
所以,在運用迪米特法則時要注意以下 6 點。
在類的劃分上,應該創(chuàng)建弱耦合的類。類與類之間的耦合越弱,就越有利于實現(xiàn)可復用的目標。
在類的結構設計上,盡量降低類成員的訪問權限。
在類的設計上,優(yōu)先考慮將一個類設置成不變類。
在對其他類的引用上,將引用其他對象的次數(shù)降到最低。
不暴露類的屬性成員,而應該提供相應的訪問器(set 和 get 方法)。
謹慎使用序列化(Serializable)功能。
下面通過一個簡單實例來加深對迪米特法則的理解:
Sunny軟件公司所開發(fā)CRM系統(tǒng)包含很多業(yè)務操作窗口,在這些窗口中,某些界面控件之間存在復雜的交互關系,一個控件事件的觸發(fā)將導致多個其他界面控件產生響應,例如,當一個按鈕(Button)被單擊時,對應的列表框(List)、組合框(ComboBox)、文本框(TextBox)、文本標簽(Label)等都將發(fā)生改變,在初始設計方案中,界面控件之間的交互關系可簡化為如圖所示結構:
在圖中,由于界面控件之間的交互關系復雜,導致在該窗口中增加新的界面控件時需要修改與之交互的其他控件的源代碼,系統(tǒng)擴展性較差,也不便于增加和刪除新控件。
現(xiàn)使用迪米特對其進行重構。
在本實例中,可以通過引入一個專門用于控制界面控件交互的中間類(Mediator)來降低界面控件之間的耦合度。引入中間類之后,界面控件之間不再發(fā)生直接引用,而是將請求先轉發(fā)給中間類,再由中間類來完成對其他控件的調用。當需要增加或刪除新的控件時,只需修改中間類即可,無須修改新增控件或已有控件的源代碼,重構后結構如圖所示:
總結
單一職責原則告訴我們實現(xiàn)類要職責單一
里氏替換原則告訴我們不要破壞繼承體系
依賴倒置原則告訴我們要面向接口編程
接口隔離原則告訴我們在設計接口的時候要精簡單一
迪米特原則告訴我們要降低耦合
開閉原則是總綱,告訴我們要對擴展開放,對修改關閉