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