面向?qū)ο缶幊淌怯嬎銠C科學的最大錯誤
C ++和Java可能是計算機科學中最嚴重的錯誤。OOP的創(chuàng)建者艾倫·凱(Alan Kay)和許多其他著名的計算機科學家都對兩者都提出了嚴厲的批評。然而,C ++和Java為最臭名昭著的編程范例-現(xiàn)代OOP鋪平了道路。
它的普及是非常不幸的,它對現(xiàn)代經(jīng)濟造成了巨大破壞,造成了數(shù)萬億美元的間接損失。OOP導致數(shù)千人喪生。在過去的三十年中,沒有任何一個行業(yè)因潛在的OO危機而受到影響。
為什么OOP如此危險?讓我們找出答案。
想象一下,帶您的家人在一個美麗的星期天下午乘車兜風。外面很好,陽光明媚。所有人都進入了汽車,行駛了與上百萬次完全相同的高速公路。
但是這次卻有所不同–即使您放開油門踏板,汽車仍會繼續(xù)不受控制地加速行駛。剎車也不起作用,似乎它們失去了動力。為了挽救局勢,您緊急拉了緊急制動器。在您的汽車撞到路邊的路堤之前,這會在道路上留下150英尺長的防滑痕跡。
聽起來像一場噩夢?但這正是2007年9月讓·布克特(Jean Bookout)駕駛豐田凱美瑞(Camry)時發(fā)生的事情。這不是唯一的此類事件。這是與所謂的"意外加速"有關(guān)的眾多事件之一,該事件困擾了豐田汽車十多年,造成近100人死亡。汽車制造商很快就將手指對準了"粘性踏板",駕駛員失誤甚至地板墊。但是,一些專家長期以來一直懷疑有問題的軟件可能正在發(fā)揮作用。
為了解決這個問題,美國宇航局的軟件專家已被征召入伍,一無所獲。僅僅幾年后,在對Bookout事件進行調(diào)查的過程中,真正的罪魁禍首是由另一個軟件專家團隊發(fā)現(xiàn)的。他們花了將近18個月的時間來研究豐田代碼。他們將Toyota代碼庫描述為"意大利面條代碼",這是一種用于纏結(jié)代碼的程序員術(shù)語。
軟件專家已經(jīng)演示了超過1000萬種豐田軟件導致意外加速的方法。最終,豐田被迫召回了超過900萬輛汽車,并支付了超過30億美元的和解費和罰款。
意大利面條代碼有問題嗎?
某些軟件故障造成的100條生命是太多了。真正令人恐懼的是,豐田代碼的問題不是唯一的。
兩架波音737 Max飛機墜毀,造成346人死亡,損失超過600億美元。都是由于軟件錯誤,由意大利細面條代碼引起的100%的確定性。
在全球范圍內(nèi),意大利面條式代碼困擾著太多的代碼庫。機載計算機,醫(yī)療設(shè)備,在核電站上運行的代碼。
程序代碼不是為機器編寫的,而是為人類編寫的。正如馬丁·福勒(Martin Fowler)所說:"任何傻瓜都可以編寫計算機可以理解的代碼。好的程序員編寫人類可以理解的代碼。"
如果代碼沒有運行,則說明它已損壞。但是,如果人們不理解該代碼,那么它將被破壞。不久。
讓我們快速繞道,談?wù)撊说拇竽X。人腦是世界上最強大的機器。但是,它有其自身的局限性。我們的工作記憶是有限的,人腦一次只能思考5件事。這意味著,程序代碼的編寫方式不應(yīng)使人的大腦不知所措。
意大利面條代碼使人腦無法理解代碼庫。這產(chǎn)生了深遠的影響-無法看到某些變化是否會破壞其他東西。對缺陷進行詳盡的測試變得不可能。沒有人甚至不能確定這樣的系統(tǒng)是否正常工作。如果確實有效,那為什么還要有效呢?
是什么導致意大利面條代碼?
為什么隨著時間的流逝,代碼變成意大利面條式代碼?由于熵-宇宙中的一切最終變得混亂無序,混亂。就像電纜最終將變得混亂一樣,我們的代碼最終也將變得混亂不堪。除非有足夠的約束。
為什么我們在道路上有速度限制?是的,有些人會永遠恨他們,但它們可以防止我們墜毀甚至死亡。為什么在路上有標記?為了防止人們走錯路,防止事故發(fā)生。
在編程時,類似的方法將完全有意義。這樣的約束不應(yīng)留給程序員來擺放。它們應(yīng)該通過工具自動實現(xiàn),或者理想情況下通過編程范例本身來實現(xiàn)。
為什么OOP是萬惡之源?
> Photo by NeONBRAND on Unsplash
我們?nèi)绾螆?zhí)行足夠的約束來防止代碼變成意大利面條?兩個選項-手動或自動。手動方法容易出錯,人類總是會犯錯誤。因此,自動執(zhí)行此類約束是合乎邏輯的。
不幸的是,OOP并不是我們一直在尋找的解決方案。它沒有提供任何約束來幫助解決代碼糾纏問題。一個人可以精通各種OOP最佳實踐,例如依賴注入,測試驅(qū)動的開發(fā),域驅(qū)動的設(shè)計等(確實有幫助)。但是,這些都不是由編程范例本身來強制執(zhí)行的(并且不存在可以強制執(zhí)行最佳實踐的此類工具)。
內(nèi)置的OOP功能都無法幫助防止意大利面條式代碼-封裝只是在程序中隱藏和分散狀態(tài),這只會使情況變得更糟。繼承增加了更多的混亂。OOP多態(tài)性再次使事情變得更加混亂-不知道程序在運行時將采用哪種確切的執(zhí)行路徑?jīng)]有任何好處。特別是在涉及多個繼承級別時。
OOP進一步加劇了意粉代碼問題
缺乏適當?shù)募s束(以防止代碼變得混亂)不是OOP的唯一缺點。
在大多數(shù)面向?qū)ο蟮恼Z言中,默認情況下所有內(nèi)容都是通過引用共享的。有效地將一個程序變成一個龐大的全球狀態(tài)。這與OOP的原始思想直接沖突。OOP的創(chuàng)建者Alan Kay具有生物學背景。他想到了一種語言(Simula),可以用類似于生物細胞的方式編寫計算機程序。他希望有獨立的程序(單元)通過相互發(fā)送消息進行通信。獨立程序的狀態(tài)永遠不會與外界共享(封裝)。
艾倫·凱(Alan Kay)從未打算讓"細胞"直接進入其他細胞的內(nèi)部進行更改。但這正是現(xiàn)代OOP中發(fā)生的事情,因為在現(xiàn)代OOP中,默認情況下,所有內(nèi)容都是通過引用共享的。這也意味著回歸成為必然。更改程序的一部分通常會破壞其他地方的功能(這在其他編程范例(如功能編程)中很少見)。
我們可以清楚地看到,現(xiàn)代OOP從根本上來說是有缺陷的。每天都會在工作中折磨您的"怪物"。而且它也會在晚上困擾您。
讓我們談?wù)効深A測性
> Photo by samsommer on Unsplash
意大利面條代碼是個大問題。面向?qū)ο蟮拇a特別容易意大利化。
意大利面條代碼使軟件無法維護。但這只是問題的一部分。我們還希望軟件可靠。但是,這還不夠,軟件(或與此相關(guān)的任何其他系統(tǒng))可以預見。
任何系統(tǒng)的用戶無論如何都應(yīng)具有相同的可預測的體驗。踩下汽車油門踏板始終會導致汽車加速。踩剎車總是會導致汽車減速。用計算機科學術(shù)語來說,我們希望汽車具有確定性。
汽車表現(xiàn)出隨機行為是非常不希望的,例如加速器無法加速或制動器未能制動(豐田問題)。即使此類問題僅在一萬億次發(fā)生一次。
然而,大多數(shù)軟件工程師的心態(tài)是"該軟件應(yīng)足以讓我們的客戶繼續(xù)使用它"。我們真的可以做得更好嗎?當然可以,我們應(yīng)該做得更好!最好的起點是解決我們程序的不確定性。
非確定性101
在計算機科學中,與確定性算法相反,非確定性算法是一種即使對于相同的輸入也可以在不同的運行中表現(xiàn)出不同行為的算法。
—有關(guān)非確定性算法的維基百科文章 |
如果上述Wikipedia上關(guān)于非確定性的說法對您來說聽起來不太好,那是因為非確定性沒有任何好處。讓我們看一下僅調(diào)用函數(shù)的代碼示例:
我們不知道該函數(shù)的功能,但是在給定相同輸入的情況下,該函數(shù)似乎總是返回相同的輸出?,F(xiàn)在,讓我們看一下另一個示例,該示例調(diào)用另一個函數(shù)computeb:
這次,函數(shù)為同一輸入返回了不同的值。兩者有什么區(qū)別?給定相同的輸入,前一個函數(shù)總是產(chǎn)生相同的輸出,就像數(shù)學中的函數(shù)一樣。換句話說,功能是確定性的。后一種功能可能會產(chǎn)生期望值,但這不能保證。換句話說,函數(shù)是不確定的。
是什么使函數(shù)具有確定性或不確定性?
- 不依賴外部狀態(tài)的功能是100%確定性的。
- 僅調(diào)用其他確定性函數(shù)的函數(shù)是確定性的。
在上面的示例中,computea是確定性的,并且在給定相同輸入的情況下,將始終提供相同的輸出。因為其輸出僅取決于其參數(shù)x。
另一方面,computeb是不確定性的,因為它調(diào)用了另一個不確定性函數(shù)Math.random()。我們?nèi)绾沃繫ath.random()是不確定的?在內(nèi)部,它取決于系統(tǒng)時間(外部狀態(tài))來計算隨機值。它還不帶任何參數(shù)-依賴于外部狀態(tài)的函數(shù)的無用贈品。
確定性與可預測性有什么關(guān)系?確定性代碼是可預測的代碼。非確定性代碼是不可預測的代碼。
從確定論到非確定論
> Photo by Annie Spratt on Unsplash
讓我們看一下附加功能:
我們始終可以確定,給定(2,2)的輸入,結(jié)果將始終等于4。我們怎么能這么確定?在大多數(shù)編程語言中,加法運算是在硬件上實現(xiàn)的,換句話說,CPU負責計算結(jié)果始終保持不變。除非我們要處理浮點數(shù)的比較,否則(但這是另一回事,與不確定性問題無關(guān))?,F(xiàn)在,讓我們關(guān)注整數(shù)。硬件非??煽?,可以安全地假定加法結(jié)果始終正確。
現(xiàn)在,讓我們將值2裝箱:到目前為止,功能是確定的!
現(xiàn)在,我們對函數(shù)主體進行一些小的更改:
- 發(fā)生了什么?突然函數(shù)的結(jié)果不再可預測!第一次運行良好,但是在隨后的每次運行中,其結(jié)果開始變得越來越不可預測。換句話說,該功能不再是確定性的。
- 為什么突然變成不確定的?該函數(shù)通過修改超出其范圍的值而引起了副作用。
讓我們回顧一下
確定性程序可確保2 + 2 == 4。換句話說,給定輸入(2,2),函數(shù)add始終應(yīng)得到4的輸出。無論您調(diào)用該函數(shù)多少次,無論您是否并行調(diào)用該函數(shù),以及該函數(shù)外部的外觀如何。
非確定性程序正好相反。在大多數(shù)情況下,對add(2,2)的調(diào)用將返回4。但是有時,該函數(shù)可能會返回3、5甚至1004。不確定性在程序中是非常不可取的,我希望您現(xiàn)在可以理解為什么。
非確定性代碼的后果是什么?軟件缺陷或通常被稱為"錯誤"的缺陷。錯誤使開發(fā)人員浪費了寶貴的調(diào)試時間,并且如果將其投入生產(chǎn),則會大大降低客戶體驗。
為了使我們的程序更可靠,我們應(yīng)該首先解決不確定性問題。
副作用
> Photo by Igor Yemelianov on Unsplash
這給我們帶來了副作用的問題。
什么是副作用?如果您因頭痛而服用藥物,但這種藥物使您惡心,那么惡心是一種副作用。簡而言之,這是不可取的。
想象一下,您已經(jīng)購買了一個計算器。您將其帶回家,開始使用它,然后突然意識到這不是一個簡單的計算器。您自己有了一個扭曲的計算器!您輸入10 * 11,它將輸出110作為輸出,但同時還會向您大喊一百和十。這是一個副作用。接下來,輸入41 + 1,它打印42,并注釋" 42,生命的意義"。副作用也一樣!您感到困惑,然后開始與您想訂購披薩的重要對象進行交談。計算器會偷聽對話,大聲說"好",然后下達比薩訂單。副作用也一樣!
讓我們回到加法功能:
是的,該函數(shù)執(zhí)行了預期的操作,將a添加到b。但是,這也會帶來副作用。對a.value + = b.value的調(diào)用導致對象a發(fā)生更改。函數(shù)參數(shù)a引用對象2,因此two.value不再等于2。第一次調(diào)用后,其值變?yōu)?,第二次調(diào)用后,其值為6,依此類推。
純度
> Photo by yann bervas on Unsplash
在討論了確定性和副作用之后,我們準備討論純度。純函數(shù)是既具有確定性又沒有副作用的函數(shù)。
再一次,確定性意味著可預測的—給定相同的輸入,該函數(shù)將始終返回相同的結(jié)果。而且沒有副作用意味著該函數(shù)除了返回值外什么也不做。這樣的功能是純粹的。
純函數(shù)有什么好處?正如我已經(jīng)說過的,它們是可以預測的。這使得它們非常易于測試(無需模擬和存根)。關(guān)于純函數(shù)的推理很容易-與OOP不同,無需牢記整個應(yīng)用程序狀態(tài)。您只需要擔心當前正在使用的功能。
純函數(shù)可以輕松組成(因為它們不會在其范圍之外進行任何更改)。純函數(shù)對于并發(fā)非常有用,因為函數(shù)之間沒有共享狀態(tài)。重構(gòu)純函數(shù)是純粹的樂趣-只需復制和粘貼,無需復雜的IDE工具。
簡而言之,純函數(shù)將歡樂帶回到編程中。
面向?qū)ο缶幊痰募兌热绾?
為了舉例說明,我們來討論一下OOP的兩個功能:getter和setter。
吸氣劑的結(jié)果取決于外部狀態(tài)-對象狀態(tài)。多次調(diào)用getter可能會導致不同的輸出,具體取決于系統(tǒng)的狀態(tài)。這使得吸氣劑本質(zhì)上是不確定的。
現(xiàn)在,二傳手。設(shè)置器用于更改對象的狀態(tài),從而使它們固有地具有副作用。
這意味著OOP中的所有方法(除了靜態(tài)方法之外)都是不確定性的,或者會帶來副作用,但每種方法都不是好方法。因此,面向?qū)ο缶幊滩皇羌兇獾臇|西,它與pure完全相反。
有一個銀彈。
但是我們很少有人敢嘗試。
> Photo by Mohamed Nohassi on Unsplash
無知并不過分,因為不愿學習。
本杰明·富蘭克林 |
在令人沮喪的軟件故障世界中,存在著一線希望,這將解決大多數(shù)(如果不是全部)問題。真正的銀彈。但是只有當您愿意學習和應(yīng)用時,大多數(shù)人才不會。
銀彈的定義是什么?可以用來解決我們所有問題的東西。數(shù)學是靈丹妙藥嗎?如果有的話,它幾乎是銀彈。
我們要歸功于成千上萬的千百年來為我們提供數(shù)學知識的聰明才智的男女。歐幾里得,畢達哥拉斯,阿基米德,艾薩克·牛頓,萊昂哈德·歐拉,阿隆佐教堂等等。
如果不確定性(即不可預測)的東西成為現(xiàn)代科學的支柱,您認為我們的世界將會走多遠?可能不會很遠,我們會留在中世紀。這實際上在醫(yī)學界已經(jīng)發(fā)生過-過去,沒有嚴格的試驗來證實特定治療或藥物的功效。人們依靠醫(yī)生的意見來治療自己的健康問題(不幸的是,這種問題在俄羅斯等國家仍然存在)。過去,無效的技術(shù)(例如放血)已廣為流行。像砷一樣不安全的東西被廣泛使用。
不幸的是,當今的軟件行業(yè)與過去的醫(yī)學太相似了。它不是基于堅實的基礎(chǔ)。取而代之的是,現(xiàn)代軟件行業(yè)主要基于脆弱的搖擺不定的基礎(chǔ),即所謂的面向?qū)ο缶幊?。如果人類的生活直接取決于軟件,那么就像放血和其他不安全的做法一樣,面向?qū)ο蟮牟僮鲗⒃缫严Р⒈贿z忘。
堅實的基礎(chǔ)
> Photo by Zoltan Tasi on Unsplash
有其他選擇嗎?在編程世界中,我們可以擁有像數(shù)學一樣可靠的東西嗎?是!許多數(shù)學概念直接轉(zhuǎn)化為編程,并奠定了稱為函數(shù)式編程的基礎(chǔ)。
函數(shù)式編程是編程的數(shù)學-一個極其牢固和健壯的基礎(chǔ),可用于構(gòu)建可靠和健壯的程序。是什么使它如此強大?它基于數(shù)學,尤其是Lambda微積分。
為了進行比較,現(xiàn)代OOP是基于什么?是的,正確的Alan Kay OOP是基于生物細胞的。但是,現(xiàn)代的Java / C#OOP是基于一組荒謬的思想(例如類,繼承和封裝)的,它沒有Alan Kay的天才發(fā)明的原始思想。其余的只是一組創(chuàng)可貼,以解決其劣等思想的缺點。
函數(shù)編程呢?它的核心組成部分是一個函數(shù),在大多數(shù)情況下是一個純函數(shù)。純函數(shù)是確定性的,這使它們可預測。這意味著由純函數(shù)組成的程序?qū)⑹强深A測的。他們會永遠沒有錯誤嗎?不會,但是如果程序中存在錯誤,也將是確定性的-對于相同的輸入始終會發(fā)生相同的錯誤,因此更易于修復。
我怎么到這里了?
過去,在過程/功能出現(xiàn)之前,goto語句已廣泛用于編程語言中。goto語句僅允許程序在執(zhí)行過程中跳至代碼的任何部分。這使得開發(fā)人員很難回答"我如何到達執(zhí)行點?"這一問題。是的,這已導致大量錯誤。
如今,一個非常相似的問題正在發(fā)生。僅在這一次,困難的問題是"我如何到達此狀態(tài)"而不是"我如何到達此執(zhí)行點"。
OOP(通常是命令式編程)回答"我如何達到這種狀態(tài)?"的問題。硬。在OOP中,所有內(nèi)容均通過引用傳遞。從技術(shù)上講,這意味著任何對象都可以被任何其他對象所突變(OOP對此沒有任何約束)。封裝根本沒有幫助-調(diào)用一種方法來更改某些對象字段并不比直接對其進行更改更好。這意味著程序很快就會變成一堆依賴關(guān)系,從而使整個程序成為全局狀態(tài)的一大塊。
有什么解決方案可以使我們停止問"我如何到達此狀態(tài)"?您可能已經(jīng)猜到了函數(shù)式編程。
過去,許多人都拒絕了停止使用goto的建議,就像今天的許多人都反對函數(shù)式編程和不可變狀態(tài)的想法一樣。
但是等等,意大利面條代碼呢?
> Photo by Andrea Piacquadio from Pexels
在OOP中,"優(yōu)先于繼承而不是繼承"被認為是最佳實踐。從理論上講,此類最佳做法應(yīng)有助于意大利面條式代碼。不幸的是,這僅僅是"最佳實踐"。面向?qū)ο蟮木幊谭独旧韺?zhí)行此類最佳實踐沒有任何限制。您的團隊中的初級開發(fā)人員必須遵循此類最佳做法,并在代碼審查中強制實施(并非總是如此)。
函數(shù)編程呢?在函數(shù)式編程中,函數(shù)組合(和分解)是構(gòu)建程序的唯一方法。這意味著編程范例本身會強制執(zhí)行組合。正是我們一直在尋找的東西!
函數(shù)調(diào)用其他函數(shù),較大的函數(shù)始終由較小的函數(shù)組成。就是這樣。與OOP不同,函數(shù)式編程中的組合是自然的。此外,這使得重構(gòu)等過程非常容易-只需剪切代碼,然后將其粘貼到新函數(shù)中即可。無需管理復雜的對象依賴項,也不需要復雜的工具(例如Resharper)。
可以清楚地看到,OOP是代碼組織的次等選擇。函數(shù)式編程的明顯勝利。
但是OOP和FP是互補的!
抱歉讓您失望。它們不是互補的。
面向?qū)ο缶幊膛c功能編程完全相反。說OOP和FP是互補的,就等于說放血和抗生素是互補的……是嗎?
OOP違反了許多基本的FP原則:
- FP提倡純凈,而OOP提倡雜質(zhì)。
- FP代碼從根本上講是確定性的,因此是可預測的。OOP代碼本質(zhì)上是不確定的,因此是不可預測的。
- 組合在FP中是自然的,在OOP中不是自然的。
- OOP通常導致錯誤的軟件和意大利面條代碼。FP生成可靠,可預測和可維護的軟件。
- FP很少需要調(diào)試,而不是簡單的單元測試會更多。另一方面,OOP程序員住在調(diào)試器中。
- OOP程序員花費大部分時間來修復錯誤。FP程序員花費大部分時間來交付結(jié)果。
最終,函數(shù)式編程是軟件界的數(shù)學。如果數(shù)學為現(xiàn)代科學奠定了非常堅實的基礎(chǔ),那么它也可以以函數(shù)式編程的形式為我們的軟件奠定堅實的基礎(chǔ)。
采取行動,為時已晚
> Image source: https://www.pexels.com/photo/blue-sky-161148/
OOP是一個非常大且代價非常高的錯誤。讓我們最終都承認這一點。
知道我乘坐的汽車運行的是用OOP編寫的軟件,這使我感到害怕。知道帶我和家人休假的飛機使用面向?qū)ο蟮拇a并不能使我感到更安全。
現(xiàn)在我們該采取最終行動的時候了。我們所有人都應(yīng)該開始采取一些小步驟,以認識到面向?qū)ο缶幊痰奈kU,并開始努力學習函數(shù)式編程。這不是一個快速的過程,至少我們需要十年才能做出轉(zhuǎn)變。我相信,在不久的將來,那些繼續(xù)使用OOP的人將被視為"恐龍",類似于今天的COBOL程序員已過時。C ++和Java將會消亡。C#將死亡。TypeScript也將很快成為歷史。
我希望您立即采取行動-如果您還沒有這樣做,請開始學習函數(shù)式編程。變得真正擅長,并廣為傳播。F#,ReasonML和Elixir都是入門的絕佳選擇。
大型軟件革命已經(jīng)開始。您會加入還是落伍?
原文鏈接:https://suzdalnitski.medium.com/oop-will-make-you-suffer-846d072b4dce