面向?qū)ο缶幊淌怯?jì)算機(jī)科學(xué)的最大錯(cuò)誤
C++和Java可能是計(jì)算機(jī)科學(xué)中最嚴(yán)重的錯(cuò)誤。兩者都受到了OOP創(chuàng)始人Alan Kay本人以及其他許多著名計(jì)算機(jī)科學(xué)家的嚴(yán)厲批評(píng)。然而,C++和Java為最臭名昭著的編程范式--現(xiàn)代OOP鋪平了道路。
它的普及是非常不幸的,它對(duì)現(xiàn)代經(jīng)濟(jì)造成了極大的破壞,造成了數(shù)萬(wàn)億美元至數(shù)萬(wàn)億美元的間接損失。成千上萬(wàn)人的生命因OOP而喪失。在過(guò)去的三十年里,沒(méi)有一個(gè)行業(yè)不受潛伏的OO危機(jī)的影響,它就在我們眼前展開(kāi)。
為什么OOP如此危險(xiǎn)?讓我們找出答案。
想象一下,在一個(gè)美麗的周日下午,帶著家人出去兜風(fēng)。外面的天氣很好,陽(yáng)光明媚。你們所有人都進(jìn)入車內(nèi),走的是已經(jīng)開(kāi)過(guò)一百萬(wàn)次的同一條高速公路。
然而這次卻有些不一樣了--車子一直不受控制地加速,即使你松開(kāi)油門踏板也是如此。剎車也不靈了,似乎失去了動(dòng)力。為了挽救局面,你鋌而走險(xiǎn),拉起了緊急剎車。這樣一來(lái),在你的車撞上路邊的路堤之前,就在路上留下了一個(gè)150英尺長(zhǎng)的滑痕。
聽(tīng)起來(lái)像一場(chǎng)噩夢(mèng)?然而這正是2007年9月讓-布克特在駕駛豐田凱美瑞時(shí)發(fā)生的事情。這并不是唯一的此類事件。這是眾多與所謂的“意外加速”有關(guān)的事件之一。“意外加速”已困擾豐田汽車十多年,造成近百人死亡。汽車制造商很快就將矛頭指向了“粘性踏板”、駕駛員失誤,甚至地板墊等方面。然而,一些專家早就懷疑可能是有問(wèn)題的軟件在作怪。
為了幫助解決這個(gè)問(wèn)題,請(qǐng)來(lái)了美國(guó)宇航局的軟件專家,結(jié)果一無(wú)所獲。直到幾年后,在調(diào)查Bookout事件的過(guò)程中,另一個(gè)軟件專家團(tuán)隊(duì)才找到了真兇。他們花了近18個(gè)月的時(shí)間來(lái)研究豐田的代碼,他們將豐田的代碼庫(kù)描述為“意大利面條代碼”——程序員的行話,意思是混亂的代碼。
軟件專家已經(jīng)演示了超過(guò)1000萬(wàn)種豐田軟件導(dǎo)致意外加速的方法。最終,豐田被迫召回了900多萬(wàn)輛汽車,并支付了超過(guò)30億美元的和解費(fèi)和罰款。
意大利面條代碼有問(wèn)題嗎?
Photo by Andrea Piacquadio from Pexels
某些軟件故障造成的100條生命是太多了,真正令人恐懼的是,豐田代碼的問(wèn)題不是唯一的。
兩架波音737 Max飛機(jī)墜毀,造成346人死亡,損失超過(guò)600億美元。這一切都是因?yàn)橐粋€(gè)軟件bug, 100%肯定是意大利面條式代碼造成的。
意大利面條式的代碼困擾著世界上太多的代碼庫(kù)。飛機(jī)上的電腦,醫(yī)療設(shè)備,核電站運(yùn)行的代碼。
程序代碼不是為機(jī)器編寫的,而是為人類編寫的。正如馬丁·福勒(Martin Fowler)所說(shuō):“任何傻瓜都可以編寫計(jì)算機(jī)可以理解的代碼。好的程序員編寫人類可以理解的代碼。”
如果代碼不能運(yùn)行,那么它就是壞的。然而如果人們不能理解代碼,那么它就會(huì)被破壞。很快就會(huì)。
我們繞個(gè)彎子,說(shuō)說(shuō)人腦。人腦是世界上最強(qiáng)大的機(jī)器。然而,它也有自己的局限性。我們的工作記憶是有限的,人腦一次只能思考5件事情。這就意味著,程序代碼的編寫要以不壓垮人腦為前提。
意大利面條代碼使人腦無(wú)法理解代碼庫(kù)。這具有深遠(yuǎn)的影響--不可能看到某些改變是否會(huì)破壞其他東西,對(duì)缺陷的詳盡測(cè)試變得不可能。
是什么導(dǎo)致意大利面條代碼?
Photo by Craig Adderley from Pexels
為什么代碼會(huì)隨著時(shí)間的推移變成意大利面條代碼?因?yàn)殪?-宇宙中的一切最終都會(huì)變得無(wú)序、混亂。就像電纜最終會(huì)變得糾纏不清一樣,我們的代碼最終也會(huì)變得糾纏不清。除非有足夠的約束條件。
為什么我們要在道路上限速?是的,有些人總會(huì)討厭它們,但它們可以防止我們撞死人。為什么我們要在馬路上設(shè)置標(biāo)線?為了防止人們走錯(cuò)路,防止事故的發(fā)生。
類似的方法在編程時(shí)完全有意義。這樣的約束不應(yīng)該讓人類程序員去實(shí)施。它們應(yīng)該由工具自動(dòng)執(zhí)行,或者最好由編程范式本身執(zhí)行。
為什么OOP是萬(wàn)惡之源?
Photo by NeONBRAND on Unsplash
我們?nèi)绾螆?zhí)行足夠的約束以防止代碼變成意大利面條?兩個(gè)選擇--手動(dòng),或者自動(dòng)。手動(dòng)方式容易出錯(cuò),人總會(huì)出錯(cuò)。因此,自動(dòng)執(zhí)行這種約束是符合邏輯的。
不幸的是,OOP并不是我們一直在尋找的解決方案。它沒(méi)有提供任何約束來(lái)幫助解決代碼糾纏的問(wèn)題。人們可以精通各種OOP的最佳實(shí)踐,比如依賴注入、測(cè)試驅(qū)動(dòng)開(kāi)發(fā)、領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)等(確實(shí)有幫助)。然而,這些都不是編程范式本身所能強(qiáng)制執(zhí)行的(而且也沒(méi)有這樣的工具可以強(qiáng)制執(zhí)行最佳實(shí)踐)。
內(nèi)置的OOP功能都無(wú)助于防止意大利面條代碼——封裝只是將狀態(tài)隱藏并分散在程序中,這只會(huì)讓事情變得更糟。繼承性增加了更多的混亂,OOP多態(tài)性再次讓事情變得更加混亂——在運(yùn)行時(shí)不知道程序到底要走什么執(zhí)行路徑是沒(méi)有好處的,尤其是涉及到多級(jí)繼承的時(shí)候。
OOP進(jìn)一步加劇了意大利面條代碼的問(wèn)題
缺乏適當(dāng)?shù)募s束(以防止代碼變得混亂)不是OOP的唯一缺點(diǎn)。
在大多數(shù)面向?qū)ο蟮恼Z(yǔ)言中,默認(rèn)情況下所有內(nèi)容都是通過(guò)引用共享的。實(shí)際上把一個(gè)程序變成了一個(gè)巨大的全局狀態(tài)的blob,這與OOP的初衷直接沖突。OOP的創(chuàng)造者Alan Kay有生物學(xué)的背景,他有一個(gè)想法,就是想用一種類似生物細(xì)胞的方式來(lái)編寫計(jì)算機(jī)程序的語(yǔ)言(Simula),他想讓獨(dú)立的程序(細(xì)胞)通過(guò)互相發(fā)送消息來(lái)進(jìn)行交流。獨(dú)立程序的狀態(tài)絕不會(huì)與外界共享(封裝)。
Alan Kay從未打算讓“細(xì)胞”直接進(jìn)入其他細(xì)胞的內(nèi)部進(jìn)行改變。然而,這正是現(xiàn)代OOP中所發(fā)生的事情,因?yàn)樵诂F(xiàn)代OOP中,默認(rèn)情況下,所有東西都是通過(guò)引用來(lái)共享的。這也意味著,回歸變得不可避免。改變程序的一個(gè)部分往往會(huì)破壞其他地方的東西(這在其他編程范式,如函數(shù)式編程中就不那么常見(jiàn)了)。
我們可以清楚地看到,現(xiàn)代OOP存在著根本性的缺陷。它是每天工作中會(huì)折磨你的“怪物”,而且它還會(huì)在晚上纏著你。
讓我們來(lái)談?wù)効深A(yù)測(cè)性
Photo by samsommer on Unsplash
意大利面代碼是個(gè)大問(wèn)題,面向?qū)ο蟮拇a特別容易意大利化。
意大利面條代碼使軟件無(wú)法維護(hù),然而這只是問(wèn)題的一部分。我們也希望軟件是可靠的。但這還不夠,軟件(或任何其他系統(tǒng))被期望是可預(yù)測(cè)的。
任何系統(tǒng)的用戶無(wú)論如何都應(yīng)該有同樣的可預(yù)測(cè)的體驗(yàn)。踩汽車油門踏板的結(jié)果總是汽車加速。按下剎車應(yīng)該總是導(dǎo)致汽車減速。用計(jì)算機(jī)科學(xué)的行話來(lái)說(shuō),我們希望汽車是確定性的。
汽車出現(xiàn)隨機(jī)行為是非常不可取的,比如油門無(wú)法加速,或者剎車無(wú)法制動(dòng)(豐田問(wèn)題),即使這樣的問(wèn)題在萬(wàn)億次中只出現(xiàn)一次。
然而大多數(shù)軟件工程師的心態(tài)是“軟件應(yīng)該足夠好,讓我們的客戶繼續(xù)使用”。我們真的不能做得更好嗎?當(dāng)然,我們可以,而且我們應(yīng)該做得更好!最好的開(kāi)始是解決我們方案的非確定性。
非確定性101
在計(jì)算機(jī)科學(xué)中,非確定性算法是相對(duì)于確定性算法而言的,即使對(duì)于相同的輸入,也可以在不同的運(yùn)行中表現(xiàn)出不同的行為。
——維基百科關(guān)于非確定性算法的文章
如果上面維基百科上關(guān)于非確定性的引用你聽(tīng)起來(lái)不順耳,那是因?yàn)榉谴_定性沒(méi)有任何好處。我們來(lái)看看一個(gè)簡(jiǎn)單調(diào)用函數(shù)的代碼樣本。
- console.log( 'result', computea(2) );
- console.log( 'result', computea(2) );
- console.log( 'result', computea(2) );
- // output:
- // result 4
- // result 4
- // result 4
我們不知道這個(gè)函數(shù)的作用,但似乎在給定相同輸入的情況下,這個(gè)函數(shù)總是返回相同的輸出。現(xiàn)在,讓我們看一下另一個(gè)示例,該示例調(diào)用另一個(gè)函數(shù) computeb:
- console.log( 'result', computeb(2) );
- console.log( 'result', computeb(2) );
- console.log( 'result', computeb(2) );
- console.log( 'result', computeb(2) );
- // output:
- // result 4
- // result 4
- // result 4
- // result 2 <= not good
這次,函數(shù)為相同的輸入返回了不同的值。兩者之間有什么區(qū)別?前者的函數(shù)總是在給定相同的輸入的情況下產(chǎn)生相同的輸出,就像數(shù)學(xué)中的函數(shù)一樣。換句話說(shuō),函數(shù)是確定性的。后一個(gè)函數(shù)可能會(huì)產(chǎn)生預(yù)期值,但這是不保證的。或者換句話說(shuō),這個(gè)函數(shù)是不確定的。
是什么使函數(shù)具有確定性或不確定性?
- 不依賴外部狀態(tài)的函數(shù)是100%確定性的。
- 僅調(diào)用其他確定性函數(shù)的函數(shù)是確定性的。
- function computea(x) {
- return x * x;
- }
- function computeb(x) {
- returnMath.random() < 0.9
- ? x * x
- : x;
- }
在上面的例子中,computea 是確定性的,在給定相同輸入的情況下,它總是會(huì)給出相同的輸出。因?yàn)樗妮敵鲋蝗Q于它的參數(shù) x 。
另一方面,computeb 是非確定性的,因?yàn)樗{(diào)用了另一個(gè)非確定性函數(shù) Math.random()。我們?cè)趺粗繫ath.random()是非確定性的?在內(nèi)部,它依賴于系統(tǒng)時(shí)間(外部狀態(tài))來(lái)計(jì)算隨機(jī)值。它也不接受任何參數(shù)--這是一個(gè)依賴于外部狀態(tài)的函數(shù)的致命漏洞。
確定性與可預(yù)測(cè)性有什么關(guān)系?確定性的代碼是可預(yù)測(cè)的代碼,非確定性代碼是不可預(yù)測(cè)的代碼。
從確定性到非確定性
我們來(lái)看看一個(gè)加法函數(shù):
- function add(a, b) {
- return a + b;
- };
我們始終可以確定,給定 (2, 2) 的輸入,結(jié)果將始終等于 4。我們?cè)趺茨苓@么肯定呢?在大多數(shù)編程語(yǔ)言中,加法運(yùn)算都是在硬件上實(shí)現(xiàn)的,換句話說(shuō),CPU負(fù)責(zé)計(jì)算的結(jié)果要始終保持不變。除非我們處理的是浮點(diǎn)數(shù)的比較,(但這是另一回事,與非確定性問(wèn)題無(wú)關(guān))。現(xiàn)在,讓我們把重點(diǎn)放在整數(shù)上。硬件是非常可靠的,可以肯定的是,加法的結(jié)果永遠(yuǎn)是正確的。
現(xiàn)在,讓我們將值 2 裝箱:
- const box = value => ({ value });
- const two = box(2);
- const twoPrime = box(2);
- function add(a, b) {
- return a.value + b.value;
- }
- console.log("2 + 2' == " + add(two, twoPrime));
- console.log("2 + 2' == " + add(two, twoPrime));
- console.log("2 + 2' == " + add(two, twoPrime));
- // output:
- // 2 + 2' == 4
- // 2 + 2' == 4
- // 2 + 2' == 4
到目前為止,函數(shù)是確定性的!
現(xiàn)在,我們對(duì)函數(shù)的主體進(jìn)行一些小的更改:
- function add(a, b) {
- a.value += b.value;
- return a.value;
- }
- console.log("2 + 2' == " + add(two, twoPrime));
- console.log("2 + 2' == " + add(two, twoPrime));
- console.log("2 + 2' == " + add(two, twoPrime));
- // output:
- // 2 + 2' == 4
- // 2 + 2' == 6
- // 2 + 2' == 8
怎么了?突然間,函數(shù)的結(jié)果不再是可預(yù)測(cè)的了!它第一次工作正常,但在隨后的每次運(yùn)行中,它的結(jié)果開(kāi)始變得越來(lái)越不可預(yù)測(cè)。它第一次運(yùn)行得很好,但在隨后的每一次運(yùn)行中,它的結(jié)果開(kāi)始變得越來(lái)越不可預(yù)測(cè)。換句話說(shuō),這個(gè)函數(shù)不再是確定性的。
為什么它突然變得不確定了?該函數(shù)修改了其范圍外的值,引起了副作用。
讓我們回顧一下
確定性程序可確保 2 + 2 == 4,換句話說(shuō),給定輸入 (2, 2),函數(shù) add 始終應(yīng)得到 4 的輸出。不管你調(diào)用函數(shù)多少次,不管你是否并行調(diào)用函數(shù),也不管函數(shù)外的世界是什么樣子。
非確定性程序正好相反,在大多數(shù)情況下,調(diào)用 add(2, 2) 將返回 4 。但偶爾,函數(shù)可能會(huì)返回3、5,甚至1004。在程序中,非確定性是非常不可取的,希望你現(xiàn)在能明白為什么。
非確定性代碼的后果是什么?軟件缺陷,也就是通常所說(shuō)的 “bug”。錯(cuò)誤使開(kāi)發(fā)人員浪費(fèi)了寶貴的調(diào)試時(shí)間,如果他們進(jìn)入生產(chǎn)領(lǐng)域,會(huì)大大降低客戶體驗(yàn)。
為了使我們的程序更可靠,我們應(yīng)該首先解決非確定性問(wèn)題。
副作用
Photo by Igor Yemelianov on Unsplash
這給我們帶來(lái)了副作用的問(wèn)題。
什么是副作用?如果你正在服用治療頭痛的藥物,但這種藥物讓你惡心,那么惡心就是一種副作用。簡(jiǎn)單來(lái)說(shuō),就是一些不理想的東西。
想象一下,你已經(jīng)購(gòu)買了一個(gè)計(jì)算器,你把它帶回家,開(kāi)始使用,然后突然發(fā)現(xiàn)這不是一個(gè)簡(jiǎn)單的計(jì)算器。你給自己弄了個(gè)扭曲的計(jì)算器!您輸入 10 * 11,它將輸出 110,但它同時(shí)還向您大喊一百和十。這是副作用。接下來(lái),輸入 41+1,它會(huì)打印42,并注釋“42,生命的意義”。還有副作用!你很困惑,然后開(kāi)始和你的另一半說(shuō)你想要點(diǎn)披薩。計(jì)算器聽(tīng)到了對(duì)話,大聲說(shuō)“ok”,然后點(diǎn)了一份披薩。還有副作用!
讓我們回到加法函數(shù):
- function add(a, b) {
- a.value += b.value;
- return a.value;
- }
是的,該函數(shù)執(zhí)行了預(yù)期的操作,將 a 添加到 b。然而,它也引入了一個(gè)副作用,調(diào)用 a.value += b.value 導(dǎo)致對(duì)象 a 發(fā)生變化。函數(shù)參數(shù) a 引用的是對(duì)象 2,因此是 2,value 不再等于 2。第一次調(diào)用后,其值變?yōu)?4,第二次調(diào)用后,其值為 6,依此類推。
純度
在討論了確定性和副作用之后,我們準(zhǔn)備談?wù)劶兒瘮?shù),純函數(shù)是指既具有確定性,又沒(méi)有副作用的函數(shù)。
再一次,確定性意味著可預(yù)測(cè)--在給定相同輸入的情況下,函數(shù)總是返回相同的結(jié)果。而無(wú)副作用意味著該函數(shù)除了返回一個(gè)值之外,不會(huì)做任何其他事情,這樣的函數(shù)才是純粹的。
純函數(shù)有什么好處?正如我已經(jīng)說(shuō)過(guò)的,它們是可以預(yù)測(cè)的。這使得它們非常容易測(cè)試,對(duì)純函數(shù)進(jìn)行推理很容易——不像OOP,不需要記住整個(gè)應(yīng)用程序的狀態(tài)。您只需要關(guān)心正在處理的當(dāng)前函數(shù)。
純函數(shù)可以很容易地組合(因?yàn)樗鼈儾粫?huì)改變其作用域之外的任何東西)。純函數(shù)非常適合并發(fā),因?yàn)楹瘮?shù)之間不共享任何狀態(tài)。重構(gòu)純函數(shù)是一件非常有趣的事情——只需復(fù)制粘貼,不需要復(fù)雜的IDE工具。
簡(jiǎn)而言之,純函數(shù)將歡樂(lè)帶回到編程中。
面向?qū)ο缶幊痰募兌热绾?
為了舉例說(shuō)明,我們來(lái)討論一下OOP的兩個(gè)功能:getter和setter。
getter的結(jié)果依賴于外部狀態(tài)——對(duì)象狀態(tài)。多次調(diào)用getter可能會(huì)導(dǎo)致不同的輸出,這取決于系統(tǒng)的狀態(tài)。這使得getter具有內(nèi)在的不確定性。
現(xiàn)在說(shuō)說(shuō)setter,Setters的目的是改變對(duì)象的狀態(tài),這使得它們本身就具有副作用。
這意味著OOP中的所有方法(也許除了靜態(tài)方法)要么是非確定性的,要么會(huì)引起副作用,兩者都不好。因此,面向?qū)ο蟮某绦蛟O(shè)計(jì)絕不是純粹的,它與純粹完全相反。
有一個(gè)銀彈
但是我們很少有人敢嘗試。
Photo by Mohamed Nohassi on Unsplash
無(wú)知不是恥辱,而是不愿學(xué)習(xí)。
— Benjamin Franklin
在軟件失敗的陰霾世界中,仍有一線希望,那將會(huì)解決大部分問(wèn)題,即使不是所有問(wèn)題。一個(gè)真正的銀彈。但前提是你愿意學(xué)習(xí)和應(yīng)用——大多數(shù)人都不愿意。
銀彈的定義是什么?可以用來(lái)解決我們所有問(wèn)題的東西。數(shù)學(xué)是靈丹妙藥嗎?如果說(shuō)有什么區(qū)別的話,那就是它幾乎是一顆銀彈。
我們應(yīng)該感謝成千上萬(wàn)的聰明的男人和女人,幾千年來(lái)他們辛勤工作,為我們提供數(shù)學(xué)。歐幾里得,畢達(dá)哥拉斯,阿基米德,艾薩克·牛頓,萊昂哈德·歐拉,阿朗佐·丘奇,還有很多很多其他人。
如果不確定性(即不可預(yù)測(cè))的事物成為現(xiàn)代科學(xué)的支柱,你認(rèn)為我們的世界會(huì)走多遠(yuǎn)?可能不會(huì)太遠(yuǎn),我們會(huì)停留在中世紀(jì)。這在醫(yī)學(xué)界確實(shí)發(fā)生過(guò)——在過(guò)去,沒(méi)有嚴(yán)格的試驗(yàn)來(lái)證實(shí)某種特定治療或藥物的療效。人們依靠醫(yī)生的意見(jiàn)來(lái)治療他們的健康問(wèn)題(不幸的是,這在俄羅斯等國(guó)家仍然發(fā)生)。在過(guò)去,放血等無(wú)效的技術(shù)一直很流行。像砷這樣不安全的物質(zhì)被廣泛使用。
不幸的是,今天的軟件行業(yè)與過(guò)去的醫(yī)藥太相似了。它不是建立在堅(jiān)實(shí)的基礎(chǔ)上。相反,現(xiàn)代軟件業(yè)大多是建立在一個(gè)薄弱的搖搖欲墜的基礎(chǔ)上,稱為面向?qū)ο蟮木幊獭H绻说纳苯右蕾囉谲浖琌OP早就消失了,就像放血和其他不安全的做法一樣,被人遺忘了。
堅(jiān)實(shí)的基礎(chǔ)
Photo by Zoltan Tasi on Unsplash
有沒(méi)有其他選擇?在編程的世界里,我們能不能有像數(shù)學(xué)一樣可靠的東西?是的,可以!許多數(shù)學(xué)概念可以直接轉(zhuǎn)化為編程,并為所謂的函數(shù)式編程奠定基礎(chǔ)。
是什么讓它如此穩(wěn)健?它是基于數(shù)學(xué),特別是Lambda微積分。
來(lái)做個(gè)比較,現(xiàn)代的OOP是基于什么呢?是的,真正的艾倫·凱是基于生物細(xì)胞的。然而,現(xiàn)代的Java/C# OOP是基于一組荒謬的思想,如類、繼承和封裝,它沒(méi)有天才Alan Kay所發(fā)明的原始思想,剩下的只是一套創(chuàng)可貼,用來(lái)彌補(bǔ)其劣等思想的缺陷。
函數(shù)式編程呢?它的核心構(gòu)建塊是一個(gè)函數(shù),在大多數(shù)情況下是一個(gè)純函數(shù),純函數(shù)是確定性的,這使它們可預(yù)測(cè),這意味著由純函數(shù)組成的程序?qū)⑹强深A(yù)測(cè)的。它們會(huì)永遠(yuǎn)沒(méi)有bug嗎?不,但是如果程序中有一個(gè)錯(cuò)誤,它也是確定的——相同的輸入總是會(huì)出現(xiàn)相同的錯(cuò)誤,這使得它更容易修復(fù)。
我怎么到這里了?
在過(guò)去,在過(guò)程/函數(shù)出現(xiàn)之前 goto 語(yǔ)句在編程語(yǔ)言中被廣泛使用。goto 語(yǔ)句只是允許程序在執(zhí)行期間跳轉(zhuǎn)到代碼的任何部分。這讓開(kāi)發(fā)人員真的很難回答 “我是怎么執(zhí)行到這一步的?” 的問(wèn)題。是的,這也造成了大量的BUG。
如今,一個(gè)非常類似的問(wèn)題正在發(fā)生。只不過(guò)這次的難題是 “我怎么會(huì)變成這個(gè)樣子”,而不是 “我怎么會(huì)變成這個(gè)執(zhí)行點(diǎn)”。
OOP(以及一般的命令式編程)使得回答 “我是如何達(dá)到這個(gè)狀態(tài)的?” 這個(gè)問(wèn)題變得很難。在OOP中,所有的東西都是通過(guò)引用傳遞的。這在技術(shù)上意味著,任何對(duì)象都可以被任何其他對(duì)象突變(OOP沒(méi)有任何限制來(lái)阻止這一點(diǎn))。而且封裝也沒(méi)有任何幫助--調(diào)用一個(gè)方法來(lái)突變某個(gè)對(duì)象字段并不比直接突變它好。這意味著,程序很快就會(huì)變成一團(tuán)亂七八糟的依賴關(guān)系,實(shí)際上使整個(gè)程序成為一個(gè)全局狀態(tài)的大塊頭。
有什么辦法可以讓我們不再問(wèn) “我怎么會(huì)變成這樣” 的問(wèn)題?你可能已經(jīng)猜到了,函數(shù)式編程。
過(guò)去很多人都抵制停止使用 goto 的建議,就像今天很多人抵制函數(shù)式編程,和不可變狀態(tài)的理念一樣。
但是等等,意大利面條代碼呢?
在OOP中,它被認(rèn)為是 “優(yōu)先選擇組成而不是繼承” 的最佳實(shí)踐。從理論上講,這種最佳做法應(yīng)該對(duì)意大利面條代碼有所幫助。不幸的是,這只是一種 “最佳實(shí)踐”。面向?qū)ο蟮木幊谭妒奖旧聿](méi)有為執(zhí)行這樣的最佳實(shí)踐設(shè)置任何約束。這取決于你團(tuán)隊(duì)中的初級(jí)開(kāi)發(fā)人員是否遵循這樣的最佳實(shí)踐,以及這些實(shí)踐是否在代碼審查中得到執(zhí)行(這并不總是發(fā)生)。
那函數(shù)式編程呢?在函數(shù)式編程中,函數(shù)式組成(和分解)是構(gòu)建程序的唯一方法。這意味著,編程范式本身就強(qiáng)制執(zhí)行組成。這正是我們一直在尋找的東西!
函數(shù)調(diào)用其他函數(shù),大的函數(shù)總是由小的函數(shù)組成,就是這樣。與OOP中不同的是,函數(shù)式編程中的組成是自然的。此外,這使得像重構(gòu)這樣的過(guò)程變得極為簡(jiǎn)單——只需簡(jiǎn)單地剪切代碼,并將其粘貼到一個(gè)新的函數(shù)中。不需要管理復(fù)雜的對(duì)象依賴關(guān)系,不需要復(fù)雜的工具(如Resharper)。
可以清楚地看到,OOP對(duì)于代碼組織來(lái)說(shuō)是一個(gè)較差的選擇。這是函數(shù)式編程的明顯勝利。
但是OOP和FP是相輔相成的!
抱歉讓您失望,它們不是互補(bǔ)的。
面向?qū)ο缶幊膛c函數(shù)式編程完全相反。說(shuō)OOP和FP是互補(bǔ)的,可能就等于說(shuō)放血和抗生素是互補(bǔ)的,是嗎?
OOP違反了許多基本的FP原則:
- FP提倡純凈,而OOP提倡雜質(zhì)。
- FP代碼基本上是確定性的,因此是可預(yù)測(cè)的。OOP代碼本質(zhì)上是不確定性的,因此是不可預(yù)測(cè)的。
- 組合在FP中是自然的,在OOP中不是自然的。
- OOP通常會(huì)導(dǎo)致錯(cuò)誤百出的軟件和意大利面條式的代碼。FP產(chǎn)生了可靠、可預(yù)測(cè)和可維護(hù)的軟件。
- 在FP中很少需要調(diào)試,而簡(jiǎn)單的單元測(cè)試往往不需要調(diào)試。另一方面,OOP程序員生活在調(diào)試器中。
- OOP程序員把大部分時(shí)間花在修復(fù)bug上。FP程序員把大部分時(shí)間花在交付結(jié)果上。
歸根結(jié)底,函數(shù)式編程是軟件世界的數(shù)學(xué)。如果數(shù)學(xué)已經(jīng)為現(xiàn)代科學(xué)打下了堅(jiān)實(shí)的基礎(chǔ),那么它也可以以函數(shù)式編程的形式為我們的軟件打下堅(jiān)實(shí)的基礎(chǔ)。
采取行動(dòng),為時(shí)已晚
OOP是一個(gè)非常大且代價(jià)高昂的錯(cuò)誤,讓我們最終都承認(rèn)吧。
想到我坐的車運(yùn)行著用OOP編寫的軟件,我就害怕。知道帶我和我的家人去度假的飛機(jī)使用面向?qū)ο蟮拇a并沒(méi)有讓我感到更安全。
現(xiàn)在是我們大家最終采取行動(dòng)的時(shí)候了。我們都應(yīng)該從一小步開(kāi)始,認(rèn)識(shí)到面向?qū)ο缶幊痰奈kU(xiǎn),并開(kāi)始努力學(xué)習(xí)函數(shù)式編程。這不是一個(gè)快速的過(guò)程,至少需要十年的時(shí)間,我們大多數(shù)人才能實(shí)現(xiàn)轉(zhuǎn)變。我相信,在不久的將來(lái),那些一直使用OOP的人將會(huì)被視為 “恐龍”,就像今天的COBOL程序員一樣,被淘汰。C ++和Java將會(huì)消亡, C#將死亡,TypeScript也將很快成為歷史。
我希望你今天就行動(dòng)起來(lái)——如果你還沒(méi)有開(kāi)始學(xué)習(xí)函數(shù)式編程,就開(kāi)始學(xué)習(xí)吧。成為真正的好手,并傳播這個(gè)詞。F#、ReasonML和Elixir都是入門的好選擇。
巨大的軟件革命已經(jīng)開(kāi)始。你們會(huì)加入,還是會(huì)被甩在后面?