面向對象編程是計算機科學的最大錯誤

C++和Java多是計算機科學中最嚴重的錯誤。二者都受到了OOP創始人Alan Kay本人以及其餘許多著名計算機科學家的嚴厲批評。然而,C++和Java爲最臭名昭著的編程範式--現代OOP鋪平了道路。程序員

它的普及是很是不幸的,它對現代經濟形成了極大的破壞,形成了數萬億美圓至數萬億美圓的間接損失。成千上萬人的生命因OOP而喪失。在過去的三十年裏,沒有一個行業不受潛伏的OO危機的影響,它就在咱們眼前展開。算法

爲何OOP如此危險?讓咱們找出答案。編程

想象一下,在一個美麗的週日下午,帶着家人出去兜風。外面的天氣很好,陽光明媚。大家全部人都進入車內,走的是已經開過一百萬次的同一條高速公路。數組

然而此次卻有些不同了--車子一直不受控制地加速,即便你鬆開油門踏板也是如此。剎車也不靈了,彷佛失去了動力。爲了挽救局面,你鋌而走險,拉起了緊急剎車。這樣一來,在你的車撞上路邊的路堤以前,就在路上留下了一個150英尺長的滑痕。安全

聽起來像一場噩夢?然而這正是2007年9月讓-布克特在駕駛豐田凱美瑞時發生的事情。這並非惟一的此類事件。這是衆多與所謂的「意外加速」有關的事件之一。「意外加速」已困擾豐田汽車十多年,形成近百人死亡。汽車製造商很快就將矛頭指向了「粘性踏板」、駕駛員失誤,甚至地板墊等方面。然而,一些專家早就懷疑多是有問題的軟件在做怪。併發

爲了幫助解決這個問題,請來了美國宇航局的軟件專家,結果一無所得。直到幾年後,在調查Bookout事件的過程當中,另外一個軟件專家團隊才找到了真兇。他們花了近18個月的時間來研究豐田的代碼,他們將豐田的代碼庫描述爲「意大利麪條代碼」——程序員的行話,意思是混亂的代碼。dom

軟件專家已經演示了超過1000萬種豐田軟件致使意外加速的方法。最終,豐田被迫召回了900多萬輛汽車,並支付了超過30億美圓的和解費和罰款。編程語言

意大利麪條代碼有問題嗎?

Photo by Andrea Piacquadio from Pexels

某些軟件故障形成的100條生命是太多了,真正使人恐懼的是,豐田代碼的問題不是惟一的。函數式編程

兩架波音737 Max飛機墜毀,形成346人死亡,損失超過600億美圓。這一切都是由於一個軟件bug, 100%確定是意大利麪條式代碼形成的。函數

意大利麪條式的代碼困擾着世界上太多的代碼庫。飛機上的電腦,醫療設備,核電站運行的代碼。

程序代碼不是爲機器編寫的,而是爲人類編寫的。正如馬丁·福勒(Martin Fowler)所說:「任何傻瓜均可以編寫計算機能夠理解的代碼。好的程序員編寫人類能夠理解的代碼。」

若是代碼不能運行,那麼它就是壞的。然而若是人們不能理解代碼,那麼它就會被破壞。很快就會。

咱們繞個彎子,說說人腦。人腦是世界上最強大的機器。然而,它也有本身的侷限性。咱們的工做記憶是有限的,人腦一次只能思考5件事情。這就意味着,程序代碼的編寫要以不壓垮人腦爲前提。

意大利麪條代碼令人腦沒法理解代碼庫。這具備深遠的影響--不可能看到某些改變是否會破壞其餘東西,對缺陷的詳盡測試變得不可能。

是什麼致使意大利麪條代碼?

Photo by Craig Adderley from Pexels

爲何代碼會隨着時間的推移變成意大利麪條代碼?由於熵--宇宙中的一切最終都會變得無序、混亂。就像電纜最終會變得糾纏不清同樣,咱們的代碼最終也會變得糾纏不清。除非有足夠的約束條件。

爲何咱們要在道路上限速?是的,有些人總會討厭它們,但它們能夠防止咱們撞死人。爲何咱們要在馬路上設置標線?爲了防止人們走錯路,防止事故的發生。

相似的方法在編程時徹底有意義。這樣的約束不該該讓人類程序員去實施。它們應該由工具自動執行,或者最好由編程範式自己執行。

爲何OOP是萬惡之源?

Photo by NeONBRAND on Unsplash

咱們如何執行足夠的約束以防止代碼變成意大利麪條?兩個選擇--手動,或者自動。手動方式容易出錯,人總會出錯。所以,自動執行這種約束是符合邏輯的。

不幸的是,OOP並非咱們一直在尋找的解決方案。它沒有提供任何約束來幫助解決代碼糾纏的問題。人們能夠精通各類OOP的最佳實踐,好比依賴注入、測試驅動開發、領域驅動設計等(確實有幫助)。然而,這些都不是編程範式自己所能強制執行的(並且也沒有這樣的工具能夠強制執行最佳實踐)。

內置的OOP功能都無助於防止意大利麪條代碼——封裝只是將狀態隱藏並分散在程序中,這隻會讓事情變得更糟。繼承性增長了更多的混亂,OOP多態性再次讓事情變得更加混亂——在運行時不知道程序到底要走什麼執行路徑是沒有好處的,尤爲是涉及到多級繼承的時候。

OOP進一步加重了意大利麪條代碼的問題

缺少適當的約束(以防止代碼變得混亂)不是OOP的惟一缺點。

在大多數面向對象的語言中,默認狀況下全部內容都是經過引用共享的。實際上把一個程序變成了一個巨大的全局狀態的blob,這與OOP的初衷直接衝突。OOP的創造者Alan Kay有生物學的背景,他有一個想法,就是想用一種相似生物細胞的方式來編寫計算機程序的語言(Simula),他想讓獨立的程序(細胞)經過互相發送消息來進行交流。獨立程序的狀態毫不會與外界共享(封裝)。

Alan Kay從未打算讓「細胞」直接進入其餘細胞的內部進行改變。然而,這正是現代OOP中所發生的事情,由於在現代OOP中,默認狀況下,全部東西都是經過引用來共享的。這也意味着,迴歸變得不可避免。改變程序的一個部分每每會破壞其餘地方的東西(這在其餘編程範式,如函數式編程中就不那麼常見了)。

咱們能夠清楚地看到,現代OOP存在着根本性的缺陷。它是天天工做中會折磨你的「怪物」,並且它還會在晚上纏着你。

讓咱們來談談可預測性

Photo by samsommer on Unsplash

意大利麪代碼是個大問題,面向對象的代碼特別容易意大利化。

意大利麪條代碼使軟件沒法維護,然而這只是問題的一部分。咱們也但願軟件是可靠的。但這還不夠,軟件(或任何其餘系統)被指望是可預測的。

任何系統的用戶不管如何都應該有一樣的可預測的體驗。踩汽車油門踏板的結果老是汽車加速。按下剎車應該老是致使汽車減速。用計算機科學的行話來講,咱們但願汽車是肯定性的

汽車出現隨機行爲是很是不可取的,好比油門沒法加速,或者剎車沒法制動(豐田問題),即便這樣的問題在萬億次中只出現一次。

然而大多數軟件工程師的心態是「軟件應該足夠好,讓咱們的客戶繼續使用」。咱們真的不能作得更好嗎?固然,咱們能夠,並且咱們應該作得更好!最好的開始是解決咱們方案的非肯定性

非肯定性101

在計算機科學中,非肯定性算法是相對於肯定性算法而言的,即便對於相同的輸入,也能夠在不一樣的運行中表現出不一樣的行爲。

——維基百科關於非肯定性算法的文章

若是上面維基百科上關於非肯定性的引用你聽起來不順耳,那是由於非肯定性沒有任何好處。咱們來看看一個簡單調用函數的代碼樣本。

console.log( 'result', computea(2) );
console.log( 'result', computea(2) );
console.log( 'result', computea(2) );

// output:
// result 4
// result 4
// result 4

咱們不知道這個函數的做用,但彷佛在給定相同輸入的狀況下,這個函數老是返回相同的輸出。如今,讓咱們看一下另外一個示例,該示例調用另外一個函數 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

此次,函數爲相同的輸入返回了不一樣的值。二者之間有什麼區別?前者的函數老是在給定相同的輸入的狀況下產生相同的輸出,就像數學中的函數同樣。換句話說,函數是肯定性的。後一個函數可能會產生預期值,但這是不保證的。或者換句話說,這個函數是不肯定的。

是什麼使函數具備肯定性或不肯定性?

  • 不依賴外部狀態的函數是100%肯定性的。
  • 僅調用其餘肯定性函數的函數是肯定性的。
function computea(x) {
  return x * x;
}

function computeb(x) {
  return Math.random() < 0.9
          ? x * x
          : x;
}

在上面的例子中,computea 是肯定性的,在給定相同輸入的狀況下,它老是會給出相同的輸出。由於它的輸出只取決於它的參數 x

另外一方面,computeb 是非肯定性的,由於它調用了另外一個非肯定性函數 Math.random()。咱們怎麼知道Math.random()是非肯定性的?在內部,它依賴於系統時間(外部狀態)來計算隨機值。它也不接受任何參數--這是一個依賴於外部狀態的函數的致命漏洞。

肯定性與可預測性有什麼關係?肯定性的代碼是可預測的代碼,非肯定性代碼是不可預測的代碼。

從肯定性到非肯定性

咱們來看看一個加法函數:

function add(a, b) {
  return a + b;
};

咱們始終能夠肯定,給定 (2, 2) 的輸入,結果將始終等於 4。咱們怎麼能這麼確定呢?在大多數編程語言中,加法運算都是在硬件上實現的,換句話說,CPU負責計算的結果要始終保持不變。除非咱們處理的是浮點數的比較,(但這是另外一回事,與非肯定性問題無關)。如今,讓咱們把重點放在整數上。硬件是很是可靠的,能夠確定的是,加法的結果永遠是正確的。

如今,讓咱們將值 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

到目前爲止,函數是肯定性的!

如今,咱們對函數的主體進行一些小的更改:

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

怎麼了?忽然間,函數的結果再也不是可預測的了!它第一次工做正常,但在隨後的每次運行中,它的結果開始變得愈來愈不可預測。它第一次運行得很好,但在隨後的每一次運行中,它的結果開始變得愈來愈不可預測。換句話說,這個函數再也不是肯定性的。

爲何它忽然變得不肯定了?該函數修改了其範圍外的值,引發了反作用。

讓咱們回顧一下

肯定性程序可確保 2 + 2 == 4,換句話說,給定輸入 (2, 2),函數 add 始終應獲得 4 的輸出。無論你調用函數多少次,無論你是否並行調用函數,也無論函數外的世界是什麼樣子。

非肯定性程序正好相反,在大多數狀況下,調用 add(2, 2) 將返回 4 。但偶爾,函數可能會返回三、5,甚至1004。在程序中,非肯定性是很是不可取的,但願你如今能明白爲何。

非肯定性代碼的後果是什麼?軟件缺陷,也就是一般所說的 「bug」。錯誤使開發人員浪費了寶貴的調試時間,若是他們進入生產領域,會大大下降客戶體驗。

爲了使咱們的程序更可靠,咱們應該首先解決非肯定性問題。

反作用

Photo by Igor Yemelianov on Unsplash

這給咱們帶來了反作用的問題。

什麼是反作用?若是你正在服用治療頭痛的藥物,但這種藥物讓你噁心,那麼噁心就是一種反作用。簡單來講,就是一些不理想的東西。

想象一下,你已經購買了一個計算器,你把它帶回家,開始使用,而後忽然發現這不是一個簡單的計算器。你給本身弄了個扭曲的計算器!您輸入 10 * 11,它將輸出 110,但它同時還向您大喊一百和十。這是反作用。接下來,輸入 41+1,它會打印42,並註釋「42,生命的意義」。還有反作用!你很困惑,而後開始和你的另外一半說你想要點披薩。計算器聽到了對話,大聲說「ok」,而後點了一份披薩。還有反作用!

讓咱們回到加法函數:

function add(a, b) {
  a.value += b.value;
  return a.value;
}

是的,該函數執行了預期的操做,將 a 添加到 b。然而,它也引入了一個反作用,調用 a.value += b.value 致使對象 a 發生變化。函數參數 a 引用的是對象 2,所以是 2value 再也不等於 2。第一次調用後,其值變爲 4,第二次調用後,其值爲 6,依此類推。

純度

在討論了肯定性和反作用以後,咱們準備談談純函數,純函數是指既具備肯定性,又沒有反作用的函數。

再一次,肯定性意味着可預測--在給定相同輸入的狀況下,函數老是返回相同的結果。而無反作用意味着該函數除了返回一個值以外,不會作任何其餘事情,這樣的函數纔是純粹的。

純函數有什麼好處?正如我已經說過的,它們是能夠預測的。這使得它們很是容易測試,對純函數進行推理很容易——不像OOP,不須要記住整個應用程序的狀態。您只須要關心正在處理的當前函數。

純函數能夠很容易地組合(由於它們不會改變其做用域以外的任何東西)。純函數很是適合併發,由於函數之間不共享任何狀態。重構純函數是一件很是有趣的事情——只需複製粘貼,不須要複雜的IDE工具。

簡而言之,純函數將歡樂帶回到編程中。

面向對象編程的純度如何?

爲了舉例說明,咱們來討論一下OOP的兩個功能:getter和setter。

getter的結果依賴於外部狀態——對象狀態。屢次調用getter可能會致使不一樣的輸出,這取決於系統的狀態。這使得getter具備內在的不肯定性

如今說說setter,Setters的目的是改變對象的狀態,這使得它們自己就具備反作用

這意味着OOP中的全部方法(也許除了靜態方法)要麼是非肯定性的,要麼會引發反作用,二者都很差。所以,面向對象的程序設計毫不是純粹的,它與純粹徹底相反。

有一個銀彈

可是咱們不多有人敢嘗試。

Photo by Mohamed Nohassi on Unsplash

無知不是恥辱,而是不肯學習。

— Benjamin Franklin

在軟件失敗的陰霾世界中,仍有一線但願,那將會解決大部分問題,即便不是全部問題。一個真正的銀彈。但前提是你願意學習和應用——大多數人都不肯意。

銀彈的定義是什麼?能夠用來解決咱們全部問題的東西。數學是靈丹妙藥嗎?若是說有什麼區別的話,那就是它幾乎是一顆銀彈。

咱們應該感謝成千上萬的聰明的男人和女人,幾千年來他們辛勤工做,爲咱們提供數學。歐幾里得,畢達哥拉斯,阿基米德,艾薩克·牛頓,萊昂哈德·歐拉,阿朗佐·丘奇,還有不少不少其餘人。

若是不肯定性(即不可預測)的事物成爲現代科學的支柱,你認爲咱們的世界會走多遠?可能不會太遠,咱們會停留在中世紀。這在醫學界確實發生過——在過去,沒有嚴格的試驗來證明某種特定治療或藥物的療效。人們依靠醫生的意見來治療他們的健康問題(不幸的是,這在俄羅斯等國家仍然發生)。在過去,放血等無效的技術一直很流行。像砷這樣不安全的物質被普遍使用。

不幸的是,今天的軟件行業與過去的醫藥太類似了。它不是創建在堅實的基礎上。相反,現代軟件業大可能是創建在一個薄弱的風雨飄搖的基礎上,稱爲面向對象的編程。若是人的生命直接依賴於軟件,OOP早就消失了,就像放血和其餘不安全的作法同樣,被人遺忘了。

堅實的基礎

Photo by Zoltan Tasi on Unsplash

有沒有其餘選擇?在編程的世界裏,咱們能不能有像數學同樣可靠的東西?是的,能夠!許多數學概念能夠直接轉化爲編程,併爲所謂的函數式編程奠基基礎。

是什麼讓它如此穩健?它是基於數學,特別是Lambda微積分。

來作個比較,現代的OOP是基於什麼呢?是的,真正的艾倫·凱是基於生物細胞的。然而,現代的Java/C# OOP是基於一組荒謬的思想,如類、繼承和封裝,它沒有天才Alan Kay所發明的原始思想,剩下的只是一套創可貼,用來彌補其劣等思想的缺陷。

函數式編程呢?它的核心構建塊是一個函數,在大多數狀況下是一個純函數,純函數是肯定性的,這使它們可預測,這意味着由純函數組成的程序將是可預測的。它們會永遠沒有bug嗎?不,可是若是程序中有一個錯誤,它也是肯定的——相同的輸入老是會出現相同的錯誤,這使得它更容易修復。

我怎麼到這裏了?

在過去,在過程/函數出現以前 goto 語句在編程語言中被普遍使用。goto 語句只是容許程序在執行期間跳轉到代碼的任何部分。這讓開發人員真的很難回答 「我是怎麼執行到這一步的?」 的問題。是的,這也形成了大量的BUG。

現在,一個很是相似的問題正在發生。只不過此次的難題是 「我怎麼會變成這個樣子」,而不是 「我怎麼會變成這個執行點」。

OOP(以及通常的命令式編程)使得回答 「我是如何達到這個狀態的?」 這個問題變得很難。在OOP中,全部的東西都是經過引用傳遞的。這在技術上意味着,任何對象均可以被任何其餘對象突變(OOP沒有任何限制來阻止這一點)。並且封裝也沒有任何幫助--調用一個方法來突變某個對象字段並不比直接突變它好。這意味着,程序很快就會變成一團亂七八糟的依賴關係,實際上使整個程序成爲一個全局狀態的大塊頭。

有什麼辦法可讓咱們再也不問 「我怎麼會變成這樣」 的問題?你可能已經猜到了,函數式編程。

過去不少人都抵制中止使用 goto 的建議,就像今天不少人抵制函數式編程,和不可變狀態的理念同樣。

可是等等,意大利麪條代碼呢?

在OOP中,它被認爲是 「優先選擇組成而不是繼承」 的最佳實踐。從理論上講,這種最佳作法應該對意大利麪條代碼有所幫助。不幸的是,這只是一種 「最佳實踐」。面向對象的編程範式自己並無爲執行這樣的最佳實踐設置任何約束。這取決於你團隊中的初級開發人員是否遵循這樣的最佳實踐,以及這些實踐是否在代碼審查中獲得執行(這並不老是發生)。

那函數式編程呢?在函數式編程中,函數式組成(和分解)是構建程序的惟一方法。這意味着,編程範式自己就強制執行組成。這正是咱們一直在尋找的東西!

函數調用其餘函數,大的函數老是由小的函數組成,就是這樣。與OOP中不一樣的是,函數式編程中的組成是天然的。此外,這使得像重構這樣的過程變得極爲簡單——只需簡單地剪切代碼,並將其粘貼到一個新的函數中。不須要管理複雜的對象依賴關係,不須要複雜的工具(如Resharper)。

能夠清楚地看到,OOP對於代碼組織來講是一個較差的選擇。這是函數式編程的明顯勝利。

可是OOP和FP是相輔相成的!

抱歉讓您失望,它們不是互補的。

面向對象編程與函數式編程徹底相反。說OOP和FP是互補的,可能就等於說放血和抗生素是互補的,是嗎?

OOP違反了許多基本的FP原則:

  • FP提倡純淨,而OOP提倡雜質。
  • FP代碼基本上是肯定性的,所以是可預測的。OOP代碼本質上是不肯定性的,所以是不可預測的。
  • 組合在FP中是天然的,在OOP中不是天然的。
  • OOP一般會致使錯誤百出的軟件和意大利麪條式的代碼。FP產生了可靠、可預測和可維護的軟件。
  • 在FP中不多須要調試,而簡單的單元測試每每不須要調試。另外一方面,OOP程序員生活在調試器中。
  • OOP程序員把大部分時間花在修復bug上。FP程序員把大部分時間花在交付結果上。

歸根結底,函數式編程是軟件世界的數學。若是數學已經爲現代科學打下了堅實的基礎,那麼它也能夠以函數式編程的形式爲咱們的軟件打下堅實的基礎。

採起行動,爲時已晚

OOP是一個很是大且代價高昂的錯誤,讓咱們最終都認可吧。

想到我坐的車運行着用OOP編寫的軟件,我就懼怕。知道帶我和個人家人去度假的飛機使用面向對象的代碼並無讓我感到更安全。

如今是咱們你們最終採起行動的時候了。咱們都應該從一小步開始,認識到面向對象編程的危險,並開始努力學習函數式編程。這不是一個快速的過程,至少須要十年的時間,咱們大多數人才能實現轉變。我相信,在不久的未來,那些一直使用OOP的人將會被視爲 「恐龍」,就像今天的COBOL程序員同樣,被淘汰。C ++和Java將會消亡, C#將死亡,TypeScript也將很快成爲歷史。

我但願你今天就行動起來——若是你尚未開始學習函數式編程,就開始學習吧。成爲真正的好手,並傳播這個詞。F#、ReasonML和Elixir都是入門的好選擇。


巨大的軟件革命已經開始。大家會加入,仍是會被甩在後面?

相關文章
相關標籤/搜索