史敏才
編程在20世紀(jì)60年代遇到了一個(gè)大問(wèn)題:計(jì)算機(jī)那時(shí)還沒(méi)有那么強(qiáng)大,需要以某種方式在數(shù)據(jù)結(jié)構(gòu)和進(jìn)程之間分配容量。這意味著如果擁有大量數(shù)據(jù),那么在不將計(jì)算機(jī)推向極限的情況下,很多事情將無(wú)法完成。反正,如果需要做很多事情,那就不能使用過(guò)多的數(shù)據(jù),否則計(jì)算機(jī)將永遠(yuǎn)占據(jù)空間。
按艾倫·凱大約于1966年或1967年得出理論認(rèn)為可以使用封裝的微型計(jì)算機(jī),這些微型計(jì)算機(jī)不共享數(shù)據(jù),而是通過(guò)消息傳遞進(jìn)行通信。這樣可以更加經(jīng)濟(jì)地使用計(jì)算資源。
盡管這個(gè)想法很巧妙,但直到1981年,面向?qū)ο缶幊滩懦蔀橹髁?,從那以后,它就沒(méi)有停止吸引軟件開發(fā)的新手和老手,面向?qū)ο缶幊痰某绦騿T一如既往的繁忙。
但近年來(lái),這一已有10年歷史的范式受到越來(lái)越多的質(zhì)疑。難道在面向?qū)ο蟪绦蛟O(shè)計(jì)大行其道40年之后,技術(shù)已經(jīng)超越了這種范式?
帶數(shù)據(jù)的耦合函數(shù)是否可笑
面向?qū)ο缶幊痰闹饕枷敕浅:?jiǎn)單:嘗試將一個(gè)程序分解為功能強(qiáng)大的整體。隨之而來(lái)的是,將數(shù)據(jù)片段和僅在相關(guān)數(shù)據(jù)上使用的那些函數(shù)耦合在一起。
請(qǐng)注意,這僅涵蓋封裝的概念。也就是說(shuō),位于對(duì)象內(nèi)部的數(shù)據(jù)和函數(shù)對(duì)于外部是不可見的,一個(gè)人只能通過(guò)消息(通常稱為getter和setter函數(shù))與對(duì)象的內(nèi)容進(jìn)行交互。
繼承和多態(tài)并沒(méi)有包含在最初的想法中,但是對(duì)于當(dāng)今的面向?qū)ο缶幊潭?,這是必需的。繼承基本上意味著開發(fā)人員可以定義具有其父類所有屬性的子類,不過(guò)直到1976年———面向?qū)ο蟮某绦蛟O(shè)計(jì)概念問(wèn)世10年后,才將其引入。
10年后,多態(tài)進(jìn)入了面向?qū)ο蟮某绦蛟O(shè)計(jì),這意味著方法或?qū)ο罂梢杂米髌渌0?。從某種意義上說(shuō),這是繼承的概括,因?yàn)椴⒎窃挤椒ɑ驅(qū)ο蟮乃袑傩远夹枰獋鬏斀o新實(shí)體,相反,可以選擇覆蓋屬性。
多態(tài)的特殊之處在于,即使2個(gè)實(shí)體在源代碼中相互依賴,被調(diào)用實(shí)體的工作方式也更像插件。這使開發(fā)人員的工作更加輕松,他們不必再擔(dān)心運(yùn)行時(shí)的依賴關(guān)系。
值得一提的是,繼承和多態(tài)性并不是面向?qū)ο缶幊趟?dú)有的。真正的區(qū)別在于封裝數(shù)據(jù)及其所屬的方法。在那個(gè)計(jì)算資源比今天稀缺得多的時(shí)代,這是一個(gè)天才般的想法。面向?qū)ο蟮木幊滩⒉豢尚?,它使編碼變得更加容易。
面向?qū)ο缶幊讨械奈宕髥?wèn)題
面向?qū)ο缶幊桃粏?wèn)世便改變了開發(fā)人員查看代碼的方式。在1980年代以前,面向過(guò)程編程通常以機(jī)器為中心,開發(fā)人員需要非常了解計(jì)算機(jī)是如何工作的,才能編寫好的代碼。
通過(guò)封裝數(shù)據(jù)和方法,面向?qū)ο蟮木幊淌管浖_發(fā)更加以人為中心,與人類的直覺(jué)相符。例如,方法drive()屬于數(shù)據(jù)組car,但不屬于teddybear組,當(dāng)繼承產(chǎn)生時(shí)也很直觀,Hyundai是car的一個(gè)子類,并且具有相同的屬性,但PooTheBear卻不是,這是完全合理的。
這聽起來(lái)像是一臺(tái)強(qiáng)大的機(jī)器,但問(wèn)題在于,只懂面向?qū)ο蟠a的程序員會(huì)用這種思維方式思考他們所做的一切。就像人們到處看到釘子一樣,因?yàn)樗麄冎挥绣N子。正如將在下面看到的那樣,當(dāng)你的工具箱只有錘子時(shí),可能會(huì)導(dǎo)致致命的問(wèn)題。
1.大猩猩叢林香蕉問(wèn)題
如果你正在設(shè)置一個(gè)新程序,并且正在考慮設(shè)計(jì)一個(gè)新類。你可能會(huì)回想起為另一個(gè)項(xiàng)目創(chuàng)建的簡(jiǎn)潔的小類,并且意識(shí)到這對(duì)當(dāng)前正在嘗試的工作非常適合。沒(méi)問(wèn)題!可以將舊項(xiàng)目中的類重用于新項(xiàng)目。
除了該類實(shí)際上可能是另一個(gè)類的子類之外,因此現(xiàn)在還需要把父類包括在內(nèi)。然后你意識(shí)到父類也依賴于其他類,并且最終包含了代碼堆。
Erlang的創(chuàng)建者Joe Armstrong的這句話非常著名:“面向?qū)ο缶幊陶Z(yǔ)言的問(wèn)題在于,它們具有隨身攜帶的所有隱式環(huán)境,可能你只想要香蕉,但是得到的是一只拿著香蕉的大猩猩和整個(gè)叢林?!?/p>
這句話對(duì)此方法進(jìn)行了很好的說(shuō)明??梢灾赜妙悾瑢?shí)際上,這可能是面向?qū)ο缶幊痰闹饕獌?yōu)點(diǎn),但不要走極端,有時(shí)最好編寫一個(gè)新類,而不是為了寫重復(fù)代碼而添加大量依賴項(xiàng)。要靈活變通,不要死板地遵從某個(gè)范式。
2.脆弱的基類問(wèn)題
如果已經(jīng)成功地將另一個(gè)項(xiàng)目中的類重用于新代碼,那么基類會(huì)發(fā)生怎樣的變化?
它可能會(huì)破壞整個(gè)代碼,而你甚至可能都沒(méi)有碰過(guò)它。也許有一天你手上的項(xiàng)目熠熠生輝,而第二天卻被打回原形,因?yàn)橛腥烁牧嘶愔械囊粋€(gè)細(xì)微細(xì)節(jié),而該細(xì)節(jié)可能對(duì)項(xiàng)目至關(guān)重要。
使用繼承的次數(shù)越多,潛在的維護(hù)工作就越多。因此,即使在短期內(nèi)重用代碼似乎非常有效,但從長(zhǎng)遠(yuǎn)來(lái)看,它可能會(huì)有很大的代價(jià)。
3.鉆石問(wèn)題
繼承是一件可愛的小事,可以在其中繼承一類的屬性并將其轉(zhuǎn)移給其他類。但該如何組合2個(gè)不同類的屬性?
這也許做不到,至少?zèng)]辦法以簡(jiǎn)潔的方式做到,例如Copier類。復(fù)印機(jī)掃描文檔的內(nèi)容并將其打印在空白紙上,它應(yīng)該是Scanner還是Printer的子類?
根本沒(méi)有標(biāo)準(zhǔn)的答案,即使這個(gè)問(wèn)題不會(huì)破壞代碼,但只要它經(jīng)常出現(xiàn)就足以令人沮喪。
4.層次問(wèn)題
在鉆石問(wèn)題中,問(wèn)的是Copier是哪個(gè)類的子類,但其實(shí)話沒(méi)說(shuō)完,有一個(gè)簡(jiǎn)單的解決方案,假設(shè)Copier是父類,而Scanner和Printer是繼承屬性子集的子類,這就變得很簡(jiǎn)單。但如果Copier只是黑白復(fù)印,而Printer還可以彩色打印怎么辦?從這個(gè)意義上說(shuō),打印機(jī)不是包括復(fù)印機(jī)的嗎?如果打印機(jī)連接到WiFi但復(fù)印機(jī)沒(méi)有連接怎么辦?
在類上堆積的屬性越多,建立適當(dāng)?shù)膶哟谓Y(jié)構(gòu)就越困難。確實(shí),在處理屬性集群時(shí),其中Copier共享了Printer的部分但不是全部屬性,反之亦然。而且,如果嘗試將其置于層次結(jié)構(gòu)中,并且是一個(gè)大型復(fù)雜項(xiàng)目,則可能會(huì)導(dǎo)致混亂。總之,不要混淆層次結(jié)構(gòu),否則可能會(huì)陷入混亂。
5.參考問(wèn)題
有人也許會(huì)說(shuō)那么將進(jìn)行沒(méi)有層次結(jié)構(gòu)的面向?qū)ο缶幊?。其?shí)相反,我們可以使用屬性集群,并根據(jù)需要繼承、擴(kuò)展或覆蓋屬性。這會(huì)有些混亂,但這將是對(duì)當(dāng)前問(wèn)題的準(zhǔn)確表現(xiàn)。
還有一個(gè)問(wèn)題。封裝的全部目的是使數(shù)據(jù)片段彼此之間保持安全,從而使計(jì)算效率更高,沒(méi)有嚴(yán)格的層次結(jié)構(gòu),是行不通的。
如果一個(gè)對(duì)象A通過(guò)與另一個(gè)對(duì)象B交互來(lái)覆蓋層次結(jié)構(gòu),會(huì)發(fā)生什么?A與B的關(guān)系并不重要,除了B不是直接的父類。然后,A必須包含對(duì)B的私有引用,否則,將無(wú)法交互。但是,如果A包含B的子代也具有的信息,則可以在多個(gè)位置修改該信息。因此,有關(guān)B的信息已不再安全,并且封裝被破壞。
盡管許多面向?qū)ο蟮某绦騿T都使用這種架構(gòu)來(lái)構(gòu)建程序,但這并不是面向?qū)ο蟮木幊?,只是一團(tuán)糟。
單一范式的危險(xiǎn)
上面5個(gè)問(wèn)題的共同點(diǎn)是他們?cè)诓皇亲罴呀鉀Q方案的地方實(shí)現(xiàn)了繼承。由于繼承甚至沒(méi)有包含在面向?qū)ο缶幊痰脑夹问街校虼诉@里不會(huì)將這些問(wèn)題稱為面向?qū)ο蠊逃械膯?wèn)題,他們只是太過(guò)教條式的例子。
但是,不僅面向?qū)ο蟮木幊炭赡軙?huì)被夸大,在純函數(shù)式編程中,處理用戶輸入或在屏幕上打印消息也極為困難,出于這些目的,面向?qū)ο蠡蜻^(guò)程編程要好得多。
仍然有一些開發(fā)人員嘗試將這些東西實(shí)現(xiàn)為純函數(shù),并將其代碼分解為數(shù)十行,沒(méi)人能理解。使用另一種范式,他們可以輕松地將代碼簡(jiǎn)化為幾行可讀的代碼。
范式有點(diǎn)像宗教,他們都具有一定的合理性,耶穌、穆罕默德和佛陀說(shuō)了一些很酷的話,但是,如果一直遵循教條,可能最終會(huì)使自己和周圍人的生活痛苦不堪,編程范式也是如此。毫無(wú)疑問(wèn),函數(shù)式編程正逐漸受到人們的歡迎,而在過(guò)去的幾年中,面向?qū)ο蟮木幊淘獾搅艘恍﹪?yán)厲的批評(píng)。
了解新的編程范式并在適當(dāng)?shù)臅r(shí)候使用是有意義的。如果面向?qū)ο缶幊淌情_發(fā)人員無(wú)論走到哪里都能看到釘子的錘子,那這是把錘子扔出窗戶的原因嗎?不是。你可在工具箱中添加一把螺絲刀、一把刀或一把剪刀,根據(jù)當(dāng)前問(wèn)題選擇工具。
函數(shù)式編程和面向?qū)ο缶幊痰某绦騿T都不要像對(duì)待宗教那樣對(duì)待編程范式。它們是工具,都可以在某處使用,所使用的內(nèi)容僅取決于待解決的問(wèn)題。
我們是否正處于一場(chǎng)新革命的風(fēng)口浪尖上
歸根結(jié)底,關(guān)于函數(shù)式編程和面向?qū)ο缶幊痰臓?zhēng)論(相當(dāng)激烈)可以歸結(jié)為一點(diǎn):是否可以邁入面向?qū)ο缶幊虝r(shí)代的盡頭?
函數(shù)式編程通常是更有效的選擇,越來(lái)越多的問(wèn)題出現(xiàn)。如數(shù)據(jù)分析、機(jī)器學(xué)習(xí)和并行編程,對(duì)這些領(lǐng)域的投入越多,就會(huì)越喜歡函數(shù)式編程。但看看現(xiàn)狀,有十多種面向?qū)ο缶幊痰某绦騿T提供的產(chǎn)品,還有一種針對(duì)函數(shù)式編碼器的產(chǎn)品。這并不意味著你不會(huì)喜歡這份工作,如今,函數(shù)式編程開發(fā)人員仍然非常稀缺。
最有可能的情況是,面向?qū)ο蟮木幊虒⒗^續(xù)存在10年左右。函數(shù)式編程會(huì)越來(lái)越受歡迎,但這并不意味著應(yīng)該放棄面向?qū)ο缶幊?,把面向?qū)ο缶幊套鳛楸A艏寄苋匀环浅S袃?yōu)勢(shì)。
因此,在接下來(lái)的幾年中,不要將面向?qū)ο蟮木幊虂G到工具箱外,但是請(qǐng)確保它不是你唯一的工具。