前面我們談到了幾種類與類之間的關(guān)系,現(xiàn)在我們來深入一下對(duì)象與對(duì)象之間的通信問題.
為什么要深入對(duì)象與對(duì)象之間的通信呢,其根本在于,系統(tǒng)中不會(huì)存在唯一的對(duì)象,不同的對(duì)象勢必要相互進(jìn)行交流.
初學(xué)者的問題
在我們剛開始學(xué)習(xí)編程的時(shí)候竟痰,通常會(huì)將所有的方法都聲明為公有(public),但隨著我們代碼量的增加,我們都會(huì)遇到一個(gè)典型的問題:
在調(diào)用某個(gè)對(duì)象的方法時(shí)模庐,我們發(fā)現(xiàn)編譯器提示這個(gè)對(duì)象所有的方法舍败,這意味著該對(duì)象處在不安全的狀態(tài)威沫。為什么這么說呢尊搬?如果我們將這個(gè)對(duì)象比作一個(gè)人绩卤,那么這個(gè)人在別人面前是赤裸的途样,沒有任何隱私,這讓別人有機(jī)會(huì)觀察你的一切行為濒憋,并某刻致命一擊何暇。除此之外,這個(gè)完全暴露的人凛驮,也會(huì)讓別人不知所措裆站。
這顯然不是我們想要的,因此我們需要某種機(jī)制來限制的對(duì)象信息的公開:哪些信息是可以公開的黔夭,哪些是不可以公開的宏胯,在java中,我們通過方法的權(quán)限來實(shí)現(xiàn)這一點(diǎn)本姥,比如private修飾的方法只有對(duì)象自己內(nèi)部可以調(diào)用肩袍,public修飾的方法是公開給其他對(duì)象的等。
現(xiàn)在婚惫,你可能已經(jīng)明白氛赐,java的設(shè)計(jì)者為什么要“多此一舉”的為方法設(shè)計(jì)權(quán)限了魂爪。那么有人會(huì)問,我該怎么確定哪個(gè)方法應(yīng)該被設(shè)計(jì)成公有的艰管,哪些又應(yīng)該被設(shè)計(jì)成私有的呢滓侍?
當(dāng)你心里有這個(gè)疑問的時(shí)候,說明你已經(jīng)開始關(guān)注我們經(jīng)常提到的面向?qū)ο缶幊痰脑瓌t之一:封裝牲芋,即如何劃分對(duì)象的結(jié)構(gòu)撩笆。
我們都知道對(duì)象的結(jié)構(gòu)的可被劃分為靜態(tài)屬性和動(dòng)態(tài)屬性,所謂的靜態(tài)屬性就是值對(duì)象固有的屬性缸浦,比如任何一個(gè)生命體都有年齡夕冲,而動(dòng)態(tài)屬性也稱為行為屬性,指的是對(duì)象所表現(xiàn)出來的行為餐济,比如袋鼠能跳耘擂,能呼吸等。而這靜態(tài)屬性和動(dòng)態(tài)屬性又可以細(xì)分為可公開的靜態(tài)屬性絮姆,可公開的動(dòng)態(tài)屬性等。也就是說秩霍,劃分對(duì)象的結(jié)構(gòu)實(shí)則就是確定某個(gè)對(duì)象的動(dòng)態(tài)屬性和靜態(tài)屬性篙悯,在此基礎(chǔ)上再來確定屬性是否可公開等。
不難發(fā)現(xiàn)铃绒,這個(gè)過程和我們的認(rèn)知的思維過程很類似:大腦試圖從各種各樣的的物體中抽取特征鸽照。比如,我們看到貓颠悬,狗矮燎,仙人掌,為了能區(qū)分它們赔癌,我們的大腦會(huì)對(duì)這三者進(jìn)行特征抽取诞外,比如貓和狗都可以移動(dòng),有眼睛灾票,會(huì)叫峡谊,有爪子,而仙人掌則是不可移動(dòng)刊苍,有刺既们,不能叫等,通過這種特種抽取正什,我們能區(qū)分出動(dòng)物和植物的區(qū)別啥纸。換言之,我們之所以能區(qū)分出不同的物體婴氮,都是因?yàn)槲覀兊拇竽X已經(jīng)默默的為我們做了特征抽取的工作斯棒,這個(gè)過程如果由我們主動(dòng)去做就稱之為抽象編程馒索。
揭秘迪米特法則
迪米特法則(Law of demeter,縮寫是LOD)要求:一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象保持最少了解, 通縮的講就是一個(gè)類對(duì)自己依賴的類知道的越少越好名船,也就是對(duì)于被依賴的類绰上,向外公開的方法應(yīng)該盡可能的少。
迪米特法則還有一種解釋:Only talk to your immediate friends渠驼,即只與直接朋友通信.首先來解釋編程中的朋友:兩個(gè)對(duì)象之間的耦合關(guān)系稱之為朋友,通常有依賴,關(guān)聯(lián),聚合和組成等.而直接朋友則通常表現(xiàn)為關(guān)聯(lián),聚合和組成關(guān)系,即兩個(gè)對(duì)象之間聯(lián)系更為緊密,通常以成員變量,方法的參數(shù)和返回值的形式出現(xiàn).
那么為什么說是要與直接朋友通信呢?觀察直接朋友出現(xiàn)的地方,我們發(fā)現(xiàn)在直接朋友出現(xiàn)的地方,大部分情況下可以接口或者父類來代替,可以增加靈活性.
(需要注意,在考慮這個(gè)問題的時(shí)候,我們只考慮新增的類,而忽視java為我們提供的基礎(chǔ)類.)
實(shí)例演示
不難發(fā)現(xiàn),迪米特法則強(qiáng)調(diào)了一下兩點(diǎn):
- 第一要義:從被依賴者的角度來說:只暴露應(yīng)該暴露的方法或者屬性蜈块,即在編寫相關(guān)的類的時(shí)候確定方法/屬性的權(quán)限
- 第二要義:從依賴者的角度來說,只依賴應(yīng)該依賴的對(duì)象
先來解釋第一點(diǎn)迷扇,我們使用計(jì)算機(jī)來說明,以關(guān)閉計(jì)算機(jī)為例:
當(dāng)我們按下計(jì)算機(jī)的關(guān)機(jī)按鈕的時(shí)候百揭,計(jì)算機(jī)會(huì)執(zhí)行一些列的動(dòng)作會(huì)被執(zhí)行:比如保存當(dāng)前未完成的任務(wù),然后是關(guān)閉相關(guān)的服務(wù)蜓席,接著是關(guān)閉顯示器器一,最后是關(guān)閉電源,這一系列的操作以此完成后厨内,計(jì)算機(jī)才會(huì)正式被關(guān)閉祈秕。
現(xiàn)在,我們來用簡單的代碼表示這個(gè)過程,在不考慮迪米特法則情況下雏胃,我們可能寫出以下代碼
//計(jì)算機(jī)類
public class Computer{
public void saveCurrentTask(){
//do something
}
public void closeService(){
//do something
}
public void closeScreen(){
//do something
}
public void closePower(){
//do something
}
public void close(){
saveCurrentTask();
closeService();
closeScreen();
closePower();
}
}
//人
public class Person{
private Computer c;
...
public void clickCloseButton(){
//現(xiàn)在你要開始關(guān)閉計(jì)算機(jī)了请毛,正常來說你只需要調(diào)用close()方法即可,
//但是你發(fā)現(xiàn)Computer所有的方法都是公開的瞭亮,該怎么關(guān)閉呢方仿?于是你寫下了以下關(guān)閉的流程:
c.saveCurrentTask();
c.closePower();
c.close();
//亦或是以下的操作
c.closePower();
//還可能是以下的操作
c.close();
c.closePower();
}
}
發(fā)現(xiàn)上面的代碼中的問題了沒?
我們觀察clickCloseButton()方法,我們發(fā)現(xiàn)這個(gè)方法無法編寫:c是一個(gè)完全暴露的對(duì)象,其方法是完全公開的统翩,那么對(duì)于Person來說仙蚜,當(dāng)他想要執(zhí)行關(guān)閉的時(shí)候,卻發(fā)現(xiàn)不知道該怎么操作:該調(diào)用什么方法?靠運(yùn)氣猜么?如果Person的對(duì)象是個(gè)不按常理出牌的,那這個(gè)Computer的對(duì)象豈不是要被搞壞么?
迪米特法則第一要義
現(xiàn)在我們來看看迪米特法則的第一點(diǎn):從被依賴者的角度厂汗,只應(yīng)該暴露應(yīng)該暴露的方法委粉。那么這里的c對(duì)象應(yīng)該哪些方法應(yīng)該是被暴露的呢?很顯然面徽,對(duì)于Person來說艳丛,只需要關(guān)注計(jì)算機(jī)的關(guān)閉操作,而不關(guān)心計(jì)算機(jī)會(huì)如何處理這個(gè)關(guān)閉操作趟紊,因此只需要暴露close()
方法即可氮双。
那么上述的代碼應(yīng)該被修改為:
//計(jì)算機(jī)類
public class Computer{
private void saveCurrentTask(){
//do something
}
private void closeService(){
//do something
}
private void closeScreen(){
//do something
}
private void closePower(){
//do something
}
public void close(){
saveCurrentTask();
closeService();
closeScreen();
closePower();
}
}
//人
public class Person{
private Computer c;
...
public void clickCloseButton(){
c.close();
}
}
看一下它的類圖:
接下來,我們繼續(xù)來看迪米特法則的第二層含義:從依賴者的角度來說霎匈,只依賴應(yīng)該依賴的對(duì)象戴差。
這句話令人有點(diǎn)困惑,什么叫做應(yīng)該依賴的對(duì)象呢铛嘱?我們還是用上面“關(guān)閉計(jì)算機(jī)”的例子來說明:
準(zhǔn)確的說暖释,計(jì)算機(jī)包括操作系統(tǒng)和相關(guān)硬件袭厂,我們可以將其劃分為System對(duì)象和Container對(duì)象。當(dāng)我們關(guān)閉計(jì)算機(jī)的時(shí)候球匕,本質(zhì)上是向操作系統(tǒng)發(fā)出了關(guān)機(jī)指令纹磺,而實(shí)則我們只是按了一下關(guān)機(jī)按鈕,也就是我們并沒有依賴System的對(duì)象亮曹,而是依賴了Container橄杨。這里Container就是我們上面所說的直接朋友---只和直接朋友通信.
我們就這點(diǎn),繼續(xù)深入討論一下:
only talk to your immedate friends
這句話只說明了要和直接朋友通信,但是我覺得這還不完整,我更愿意將其補(bǔ)充為:
make sure your friends,only talk to your immedate friends,don't speak to strangers.
大意是:確定你真正的朋友,并只和他們通信,并且不要和陌生人講話.這樣做有個(gè)很大的好處就是,能夠簡化對(duì)象與對(duì)象之間的通信,進(jìn)而減輕依賴,提供更高的靈活性,當(dāng)然也可以提供一定的安全性.
現(xiàn)在來想想現(xiàn)實(shí)世界中的這么一種情況:你是一個(gè)令人矚目的公眾人物,周圍的每個(gè)人都知道你的名字,當(dāng)你獨(dú)自走在大街上的時(shí)候會(huì)是怎么樣的一種場景?每個(gè)人都想要和你聊天!,和你交換信息!!接著,你發(fā)現(xiàn)自己已經(jīng)寸步難行了.如果這時(shí)候你有一個(gè)經(jīng)紀(jì)人,來幫你應(yīng)對(duì)周圍的人,而你就只和這個(gè)經(jīng)紀(jì)人通信,這樣就大大減輕了你的壓力,不是么?此時(shí),這個(gè)經(jīng)濟(jì)人就相當(dāng)于你的直接朋友.
迪米特法則第二要義
現(xiàn)在,我們?cè)倩仡?關(guān)機(jī)計(jì)算機(jī)"這個(gè)操作,前面的代碼只是遵從了"暴漏應(yīng)該暴漏的方法"這一點(diǎn),現(xiàn)在我們結(jié)合第二點(diǎn)來進(jìn)行改進(jìn):System和Container相比,System并非Person的直接朋友,而Container才是(Person直接打交道的是Container).因此我們需要將原有的Computer拆分成System和Cotainer,然后使Person只與Container通信,因此代碼修改為:
//操作系統(tǒng)
public class System{
private void saveCurrentTask(){
//do something
}
private void closeService(){
//do something
}
private void closeScreen(){
//do something
}
private void closePower(){
//do something
}
public void close(){
saveCurrentTask();
closeService();
closeScreen();
closePower();
}
}
//硬件設(shè)備容器
public class Container{
private System mSystem;
public void sendCloseCommand(){
mSystem.close();
}
}
//人
ublic class Person{
private Container c;
....
public void clickCloseButton(){
c.sendCloseCommand();
}
}
來看一下它的類圖:
重構(gòu),改善既有設(shè)計(jì)
在上文中,我們還提到,直接朋友出現(xiàn)的地方,我們可以采用其接口或者父類來代替.那么在這里,我們就可以為Container和System提供相應(yīng)的接口
//System interface
public interface ISystem{
void close();
}
//System
public class System implements ISystem{
private void saveCurrentTask(){
//do something
}
private void closeService(){
//do something
}
private void closeScreen(){
//do something
}
private void closePower(){
//do something
}
@override
public void close(){
saveCurrentTask();
closeService();
closeScreen();
closePower();
}
}
//IContainer interface
public interface IContainer{
void sendCloseCommand();
}
//Contanier
public class Container implements IContainer{
private System mSystem;
@override
public void sendCloseCommand(){
mSystem.close();
}
}
//Person
ublic class Person{
private IContainer c;
....
public void clickCloseButton(){
c.sendCloseCommand();
}
}
來看一下它的類圖:
對(duì)比這兩種方案,明顯這種方案二的解耦程度更高,靈活大大增強(qiáng).不難發(fā)現(xiàn),這應(yīng)用了我們前面提到的依賴倒置,即面向接口編程.
除此之外,我們發(fā)現(xiàn)隨著不斷的改進(jìn),類的數(shù)量也在不斷的增加,從2個(gè)增加到5個(gè),這意味著為了解耦和提高靈活性通常要編寫的類的數(shù)量會(huì)翻倍.因此,你需要在這做一個(gè)權(quán)衡,切莫刻意為了追求設(shè)計(jì),而導(dǎo)致整個(gè)系統(tǒng)非常的冗余,最終可能得不償失.
總結(jié)
有人會(huì)覺得Container像是一個(gè)中介(代理).沒錯(cuò),我們確實(shí)可以稱其為中介,但這并不能否認(rèn)他是我們的直接朋友:在很多情況下,中介可以說是我們的一種代表,因此將其定義為直接朋友是沒有任何問題的.比如,當(dāng)你想要租房的時(shí)候,你可以找房屋中介,對(duì)方會(huì)按照你的標(biāo)準(zhǔn)為你尋找合適的住房.但是問題來了:那么做一件事情需要多少中介呢?總不能是我委托一個(gè)中介A幫我找房子,但中介A又委托了中介B,中介B又委托了中介C....等等,如果真的是這樣,那還不如我自己去找房子效率更高.在實(shí)際開發(fā)中,委托的層次要控制在6層以下,多余6層以上的會(huì)使得系統(tǒng)過分的冗余和并切會(huì)委托層次過多而導(dǎo)致開發(fā)人員無法正確的理解流程,產(chǎn)生風(fēng)險(xiǎn)的可能會(huì)大大提高.
到目前,我們已經(jīng)徹底的了解了迪米特法則.