
“面向抽象編程,面向接口編程”這句話流傳甚廣,它像一面旗幟插在每個(gè)人前進(jìn)的道路上,引導(dǎo)大家前行。每個(gè)程序員都免不了和抽象打交道,差距可能在于能否更好地提煉。這句話包含兩部分含義:“面向抽象編程”本質(zhì)上是對(duì)數(shù)據(jù)的抽象化,“面向接口編程”本質(zhì)上是對(duì)行為的抽象化。
我們先來談?wù)剶?shù)據(jù)的抽象化——面向抽象編程。
抽象最討厭的敵人:new
因?yàn)橹苯又v什么是抽象不太好講,容易描述的話那就不是抽象了,所以我們換個(gè)角度,先聊聊抽象的反面:什么是具體。在具體里,有個(gè)先鋒人物,就是我們都熟悉的 new。大家知道,new 是最簡(jiǎn)單和最常見的關(guān)鍵字,用來創(chuàng)建對(duì)象。但被創(chuàng)建出來的一定是具體的對(duì)象,所以new 代表著具體,它是抽象最討厭的敵人。
大家要有這種敏感:什么時(shí)機(jī)創(chuàng)建對(duì)象,在哪里創(chuàng)建,是很有講究的。為了闡述這個(gè)話題,我們先看下面這行代碼:
Animalanimal= new Tiger(); //Animal是抽象類
我曾經(jīng)對(duì)這句簡(jiǎn)單的賦值語句思考很久:左邊抽象,右邊具體,感覺不對(duì)等,這樣寫好不好?答案不簡(jiǎn)單啊。
接下來,我們分成兩個(gè)方向細(xì)細(xì)討論。
假設(shè)一:如果它是某個(gè)類的成員變量的定義。例如:
privateAnimal animal =newTiger();
先下結(jié)論:如果類里其他地方?jīng)]有對(duì) animal 這個(gè)變量的賦值操作,此后再?zèng)]有更改它的邏輯了,那么它基本不是好寫法(有少許例外)。那么,什么是好寫法?
哈,這里先賣個(gè)關(guān)子。
這里需要注意的是,我們討論的是左邊是抽象,右邊是具體的 new。如果 new 的兩邊是平級(jí)概念的類,例如:
Tiger tiger= new Tiger();
它左右兩邊沒有抽象之分,那么不在我們討論范圍之內(nèi)。
假設(shè)二:如果它是某個(gè)函數(shù)內(nèi)部的變量定義語句。示例如下:
voidShow() {Animal animal =newTiger();......// 出場(chǎng)前的準(zhǔn)備活動(dòng)ShowAnimal(animal);}
我曾經(jīng)疑惑:為何不直接定義成子類類型?就這樣寫:
Tiger tiger= new Tiger();
根據(jù)繼承原理,子類能調(diào)用抽象類的方法。所以也不會(huì)影響接下來的函數(shù)調(diào)用。例如:所有的animal.Eat 替換為 tiger.Eat 一定成立。
同時(shí)根據(jù)里氏替換原則,但凡出現(xiàn)animal 的地方,都可以把tiger 代替進(jìn)去,所以也不會(huì)影響我的參數(shù)傳遞。例如:ShowAnimal(animal)替換為ShowAnimal(tiger)也一定成立。
可一旦把 Tiger 類型上溯轉(zhuǎn)為抽象的 Animal 類型,那么 Tiger 自身的特殊能力(例如Hunt)在“出場(chǎng)前的準(zhǔn)備活動(dòng)”那部分就用不了,例如:
tiger.Hunt();// 老虎進(jìn)行狩獵animal.Hunt();// 不能通過編譯
也就是說,Animal animal = new Tiger();里Animal 的抽象定義,只有限制我自由的作用,而沒有帶來任何實(shí)質(zhì)的好處!這種寫法不是很糟糕嗎?
你會(huì)有一天頓悟:這種對(duì)自由的限制,恰恰是最珍貴的!大部分時(shí)候,我們?nèi)钡牟皇亲杂?,而是自律。任何人的自由,都不能以損害別人的利益為代價(jià)。
ShowAnimal(animal);之前的那段“出場(chǎng)前的準(zhǔn)備活動(dòng)”代碼,將來很有可能是別人來維護(hù)的。在架構(gòu)設(shè)計(jì)上,一定要考慮“時(shí)間”這個(gè)變量帶來的不確定性。如果你定義成:
Tiger tiger= new Tiger();
這看起來更靈活,但你沒法阻止這只老虎被別人將來使用Hunt 函數(shù)濫殺無辜。
一旦定義為:
Animal animal= new Tiger();
那么,這只老虎將會(huì)是一只溫順的老虎,只遵循普通的動(dòng)物準(zhǔn)則。
所以如果“出場(chǎng)前的準(zhǔn)備活動(dòng)”這部分的業(yè)務(wù)需求里只用到Animal 的基本功能函數(shù),那么:
Animal animal= new Tiger();
要優(yōu)于
Tiger tiger= new Tiger();
好了,等號(hào)左邊的抽象問題解決了,但等號(hào)右邊的 new 呢?這個(gè)場(chǎng)景里,Animal animal =new Tiger();是函數(shù)的局部變量,也沒有傳導(dǎo)到全局變量中。到目前為止,這個(gè)new 是完全可以接受的。面向抽象,是要在關(guān)鍵且合適的地方去抽象,如果處處都抽象,代價(jià)會(huì)非常大,得不償失。如果滿分是 100 分的話,目前能得 95 分,已經(jīng)很好了,這也是我們大多數(shù)時(shí)候的寫法。
但你還是要知道:一旦接受了這個(gè) new,好比是和魔鬼做了契約,會(huì)付出潛在代價(jià)的。此處的代價(jià)是這段代碼不能再升級(jí)成框架性的抽象代碼了。想要完美得到 100 分,則需要消滅這個(gè)new,怎么辦呢?
消滅 new 的兩件武器
上面站在理論高度“批判”了new,其實(shí)并不是說 new 真的不好,而是說很多人會(huì)濫用。就好比火是人類文明的起源,好東西,但是濫用就會(huì)造成火災(zāi)。把火源限定在特定工具才能點(diǎn)火,隔離開,用起來才安全。new 其實(shí)也一樣,下面講的本質(zhì)上不是消滅 new,而是隔離 new 的兩件武器。
控制反轉(zhuǎn)——臟活讓別人去干
還記得前面賣的關(guān)子嗎?如果 animal 是類成員變量:
privateAnimal animal =newTiger();
這并不是好寫法,那么什么是好寫法呢?這種情況下,比較簡(jiǎn)單的是對(duì)它進(jìn)行參數(shù)化改造:
void setAnimal(Animalanimal) { this.animal=animal;}
然后讓客戶去調(diào)用注入:
Tigertiger =newTiger();obj.setAnimal(tiger);
有了上面的注入代碼,private Animal animal = new Tiger();這句話反而變得可以接受了。因?yàn)榈忍?hào)右邊的 Tiger 僅僅是默認(rèn)值,默認(rèn)值當(dāng)然是具體的。
上面的參數(shù)化改造手法,我們可以稱為“依賴注入”,其核心思想是:不要調(diào)我,我會(huì)去調(diào)你!依賴注入分為屬性注入、構(gòu)造函數(shù)注入和普通函數(shù)注入。很明顯,上面的例子是屬性注入。
依賴注入和標(biāo)題的“控制反轉(zhuǎn)”還不能完全劃等號(hào)。確切地說,“依賴注入”是實(shí)現(xiàn)“控制反轉(zhuǎn)”的方式之一。
這種干脆把創(chuàng)建對(duì)象的任務(wù)甩手不干的事情,反而是個(gè)好寫法,境界高!這樣,你不知不覺把自己的代碼完全變成了只負(fù)責(zé)數(shù)據(jù)流轉(zhuǎn)的框架性代碼,具備了通用性。
在通往架構(gòu)師的道路上,你要培養(yǎng)出一種感覺:要?jiǎng)?chuàng)建一個(gè)跨作用域的實(shí)體對(duì)象(不是值對(duì)象)是一件很謹(jǐn)慎的事情(越接觸大型項(xiàng)目,你對(duì)這點(diǎn)的體會(huì)就越深),不要隨便創(chuàng)建。最好不要自己創(chuàng)建,讓別人去創(chuàng)建,傳給你去調(diào)用。那么問題來了:都不愿意去創(chuàng)建,誰去創(chuàng)建?這個(gè)丟手絹的游戲最終到底要丟給誰呢?
先把問題揣著,我們接著往下看。
工廠模式——抽象的基礎(chǔ)設(shè)施
我們回到這段Show 代碼:
voidShow() { Animal animal =newTiger();// 上面說過,這里的 new 目前是可以接受的......// 出場(chǎng)前的準(zhǔn)備活動(dòng)ShowAnimal(animal);}
但如果Show 方法里創(chuàng)建動(dòng)物的需求變得復(fù)雜,new 會(huì)變得猖狂起來:
voidShow(stringname) { Animal animal;if(name =="Tiger") animal =newTiger();elseif(name =="Lion") animal =newLion();......// 其他種類ShowAnimal(animal);}
此時(shí)將變得不可接受了。對(duì)付這么多同質(zhì)的 new(都是創(chuàng)建Animal),一般會(huì)將它們封裝進(jìn)專門生產(chǎn) animal 的工廠里:
Animal ProvideAnimal(stringname) { Animal animal;if(name =="Tiger") animal =newTiger();elseif(name =="Lion") animal =newLion();......// 其他種類}
進(jìn)而優(yōu)化了 Show 代碼:
voidShow(stringname){ Animal animal = ProvideAnimal(name);// 等號(hào)兩邊都是同級(jí)別的抽象,這下徹底舒服了ShowAnimal(animal);}
因此,依賴注入和工廠模式是消滅 new 的兩種武器。此外,它們也經(jīng)常結(jié)合使用。
上面的 ProvideAnimal 函數(shù)采用的是簡(jiǎn)單工廠模式。由于工廠模式是每個(gè)人都會(huì)遇到的基本設(shè)計(jì)模式,所以這里會(huì)對(duì)它進(jìn)行更深入的闡述,讓大家能更深入地理解它。工廠模式嚴(yán)格說來有簡(jiǎn)單工廠模式和抽象工廠模式之分,但真正算得上設(shè)計(jì)模式的,是抽象工廠模式。簡(jiǎn)單工廠模式僅僅是比較自然的簡(jiǎn)單封裝,有點(diǎn)配不上一種設(shè)計(jì)模式的稱呼。因此,很多教科書會(huì)大篇幅地介紹抽象工廠,而有意無意地忽略了簡(jiǎn)單工廠。但實(shí)際情況正好相反,抽象工廠大部分人一輩子都用不上一次(它的出現(xiàn)要依賴于對(duì)多個(gè)相關(guān)類族創(chuàng)建對(duì)象的復(fù)雜需求場(chǎng)景),而簡(jiǎn)單工廠幾乎每個(gè)人都用得上。
和一般的設(shè)計(jì)模式不一樣,有些設(shè)計(jì)模式的代碼結(jié)構(gòu)哪怕你已經(jīng)爛熟于心,卻依然很難想象它們的具體使用場(chǎng)景。工廠模式是面向抽象編程,數(shù)據(jù)的創(chuàng)建需求變復(fù)雜之后很自然的產(chǎn)物,很多人都能無師自通地去使用它。將面向抽象編程堅(jiān)持到底,會(huì)自然地把創(chuàng)建對(duì)象的任務(wù)外包出去,丟給專門的工廠去創(chuàng)建。
可見,工廠模式在整個(gè)可擴(kuò)展的架構(gòu)中扮演的不是先鋒隊(duì)角色,而是強(qiáng)有力的支持“面向抽象編程”的基礎(chǔ)設(shè)施之一。
最后調(diào)侃一下,我面試候選人的時(shí)候,很喜歡問他們一個(gè)問題:“你最常用的設(shè)計(jì)模式有哪些?”
排第一的是“單例模式”,而“工廠模式”是當(dāng)之無愧的第二名,排第三的是“觀察者模式”。這側(cè)面說明這三種模式應(yīng)該是廣大程序員最容易用到的設(shè)計(jì)模式。大家學(xué)習(xí)設(shè)計(jì)模式時(shí),首先應(yīng)該仔細(xì)研究這三種模式及其變種。
new 去哪里了呢
這里回到最開始也是最關(guān)鍵的問題:如果大家都不去創(chuàng)建,那么誰去創(chuàng)建呢?把臟活丟給別人,那別人是誰呢?下面我們從兩個(gè)方面闡述。
■ 局部變量。局部變量是指在函數(shù)內(nèi)部生產(chǎn)又在函數(shù)內(nèi)部消失的變量,外部并不知曉它的存在。在函數(shù)內(nèi)部創(chuàng)建它們就好,這也是我們遇到的大多數(shù)情況。例如:
voidShow() { Animal animal =newTiger();......// 出場(chǎng)前的準(zhǔn)備活動(dòng)ShowAnimal(animal);}
前面說過,這段代碼里的 new 能得95 分,沒有問題。
■ 跨作用域變量。對(duì)這類對(duì)象的創(chuàng)建,總是要小心一些的。
○ 如果是零散的創(chuàng)建,就讓各個(gè)客戶端自己去創(chuàng)建。這里的客戶端是泛指的概念,不是服務(wù)器對(duì)應(yīng)的客戶端。凡是調(diào)用核心模塊的發(fā)起方,均屬于客戶端。每個(gè)客戶端是知道自身具體細(xì)節(jié)的,在它內(nèi)部創(chuàng)建無可厚非。
○ 如果寫的是框架性代碼,是基于總體規(guī)則的創(chuàng)建,那就在核心模塊里采用專門的工廠去創(chuàng)建。
抽象到什么程度
前面說過,完全具體肯定不行,缺乏彈性。但緊接著另一個(gè)問題來了:越抽象就越好嗎?不見得。我們對(duì)抽象的態(tài)度沒必要過分崇拜,下面就專門討論一下抽象和具體之間如何平衡。比如Java 語言,根上的 Object 類最抽象了,但 Object 定義滿天飛顯然不是我們想要的,例如:
Objectobj =newTiger();
那樣你會(huì)被迫不停地進(jìn)行下溯轉(zhuǎn)換:
Animalanimal= (Animal)obj;
所以不是越抽象越好。抽象是有等級(jí)之分的,要抽象到什么程度呢?有一句描述美女魔鬼身材的語句是“該瘦的地方瘦,該肥的地方肥”。那么,這句話可改編一下,即可成為抽象編程的原則,即“該實(shí)的地方實(shí),該虛的地方虛”。也就是說,抽象和具體之間一定有個(gè)平衡點(diǎn),這個(gè)平衡點(diǎn)正是應(yīng)該時(shí)刻存在程序員大腦里的一件東西:用戶需求!
你需要做的是精確把握用戶需求,提供給用戶的是滿足用戶需求的最根上的那層數(shù)據(jù)。什么意思呢?我們通過下面這個(gè)例子詳細(xì)闡述。
村里的家家戶戶都要提供一種動(dòng)物去參加跑步比賽,于是每家都要實(shí)現(xiàn)一個(gè)ProvideAnimal函數(shù)。你家里今年養(yǎng)了一只老虎,老虎屬于貓科。三層繼承關(guān)系如下:
publicabstractclassAnimal{publicvoidRun();}publicclassCat:Animal{publicintJump();}publicclassTiger:Cat{publicvoidHunt(Animal animal);}
現(xiàn)在有個(gè)問題:ProvideAnimal 函數(shù)的返回類型定義為什么好呢?Animal、Cat 還是Tiger?這就要看用戶需求了。
如果此時(shí)是舉行跑步比賽,那么只需要你的動(dòng)物有跑步能力即可,此時(shí)返回Animal 類型是最好的:
publicAnimalProvideAnimal(){returnnewTiger();}
如果要舉辦跳高比賽,是Cat 層級(jí)才有的功能,那么返回Cat 類型是最好的:
publicCatProvideAnimal(){returnnewTiger();}
切記,你返回的類型,是客戶需求對(duì)應(yīng)的最根上的那個(gè)類型節(jié)點(diǎn)。這是雙贏!
如果函數(shù)返回值是最底下的 Tiger 子類型:
publicTigerProvideAnimal(){returnnewTiger();}
這會(huì)帶來如下兩個(gè)潛在的問題。
問題1:給別人造成濫用的可能
這給了組織者額外的雜亂信息。本來呢,對(duì)于跑步比賽,每一個(gè)參賽者只有一個(gè) Run 函數(shù)便清晰明了,但在老虎身上,有 Run 的同時(shí),還附帶了跳高 Jump 和捕獵 Hunt 的功能。這樣組織者需要思考一下到底應(yīng)該用哪個(gè)功能。所以提供太多無用功能,反而給別人造成了困擾。
同時(shí)也給了組織者犯錯(cuò)誤的機(jī)會(huì)。萬一,他一旦好奇,或者錯(cuò)誤操作,比賽時(shí)調(diào)用了 Hunt方法,那這只老虎就不是去參加跑步比賽,而是追捕別的小動(dòng)物吃了。
問題2:?jiǎn)适Я私怦钭訉?duì)象的機(jī)會(huì)
一旦對(duì)方在等號(hào)兩邊傻傻地按照你的子類型去定義,例如:
Tiger tiger= ProvideAnimal();
從此組織者就指名道姓地要你家的老虎了。如果比賽當(dāng)天,你的老虎生病了,你本可以換一頭獵豹去參加比賽,但因?yàn)閯e人預(yù)定了看你家的老虎,所以非去不可。結(jié)果便喪失了寶貴的解耦機(jī)會(huì)。
如果是Animal 類型,那么你并不知道是哪一種動(dòng)物會(huì)出現(xiàn),但你知道它一定會(huì)動(dòng)起來,跑成什么樣子,你并不知道。這樣的交流,是比較高級(jí)的交流。繪畫藝術(shù)上有個(gè)高級(jí)術(shù)語叫“留白”,咱們編程玩“抽象”也算是“留白”。我先保留一些東西,一開始沒必要先確定的細(xì)節(jié)就不先確定了。那這個(gè)“留白”留多少呢?根據(jù)用戶需求而定!
總結(jié)
多態(tài)這門特技,成就了人們大量采用抽象去溝通,用接口去溝通。而抽象也不負(fù)眾望地讓溝通變得更加簡(jiǎn)潔、高效;抽象也讓相互間依賴更少,架構(gòu)更靈活。
參數(shù)化和工廠模式是消滅或隔離new 的兩種武器。
用戶需求是決定抽象到何種程度的決定因素。
——本文選自《代碼里的世界觀:通往架構(gòu)師之路》

編程中有很多通用的知識(shí)點(diǎn),它們是10年甚至20年都不會(huì)淘汰的編程技術(shù),市面上也極少有將它們綜合起來并講得有意思的書。
上面這本書是一位IBM架構(gòu)師結(jié)合了自己13年編程經(jīng)驗(yàn),結(jié)合自己的理解和領(lǐng)悟,把許多知識(shí)點(diǎn)匯入到了這本書里。它們并不是潮流的知識(shí)點(diǎn),而是厚重的基礎(chǔ)知識(shí)。
點(diǎn)擊這里,看圖書目錄。