面向對象編程—價值萬億美元的災難

為什麼要從OOP繼續前進

面向對象編程—價值萬億美元的災難

Photo by Jungwoo Hong on Unsplash

OOP被許多人視為計算機科學的皇冠上的明珠。 代碼組織的最終解決方案。 我們所有問題的終結。 編寫程序的唯一真實方法。 編程本身的一位真神賦予我們……

直到……事實並非如此,人們才開始沉迷於抽象的概念,以及混雜共享的可變對象的複雜圖。人們花費大量的時間和精力思考“抽象”和“設計模式”,而不是解決現實世界中的問題。

許多人批評了面向對象的編程,其中包括非常傑出的軟件工程師。 赫克,甚至是OOP的發明者本人也是現代OOP的著名批評家!

每個軟件開發人員的最終目標應該是編寫可靠的代碼。 如果代碼有錯誤且不可靠,則無所謂。 編寫可靠代碼的最佳方法是什麼? 簡單。 簡單與複雜相反。 因此,作為軟件開發人員,我們的首要職責是降低代碼複雜性。

面向對象編程—價值萬億美元的災難

老實說,我不是狂熱的面向對象。 當然,本文將有偏見。 但是,我有充分的理由不喜歡OOP。

我也瞭解批評OOP是一個非常敏感的話題-我可能會冒犯許多讀者。 但是,我在做我認為正確的事情。 我的目標不是冒犯,而是提高對OOP引入的問題的認識。

我並不是在批評Alan Kay的OOP,他是個天才。 我希望以他設計的方式實施OOP。 我批評現代的Java / C#OOP方法。

我認為OOP被很多人視為代碼組織的實際標準是不對的,包括那些處於非常高級技術職位的人。除了OOP之外,許多主流語言都沒有提供任何其他替代代碼組織的方式,這也是不可接受的。

地獄,我在OOP項目上工作時經常會掙扎。 而且我沒有一個線索可以說明我為什麼要為此付出如此多的努力。 也許我還不夠好? 我不得不學習更多的設計模式(我認為)! 最終,我完全精疲力盡了。

這篇文章總結了我從面向對象編程到函數式編程的第一手長達十年的旅程。 不幸的是,無論我多麼努力,我都找不到OOP的用例。 我親眼看到OOP項目失敗,因為它們變得太複雜而無法維護。

TLDR

提供面向對象的程序作為正確程序的替代方案。

—計算機科學先驅Edsger W. Dijkstra

面向對象編程—價值萬億美元的災難

Photo by Sebastian Herrmann on Unsplash

創建面向對象的程序時要牢記一個目標-管理過程代碼庫的複雜性。 換句話說,它應該改善代碼組織。 沒有客觀公開的證據表明OOP比普通的程序編程要好。

嚴酷的事實是,OOP在它原本打算解決的唯一任務上失敗了。 在紙上看起來很好—我們具有乾淨的動物,狗,人等的層次結構。但是,一旦應用程序的複雜性開始增加,它就會變得平坦。 與其降低複雜性,不如鼓勵可變狀態的混雜共享,並以其眾多的設計模式引入額外的複雜性。 OOP使得不必要的通用開發實踐(如重構和測試)變得困難。

有些人可能不同意我的看法,但事實是,現代Java / C#OOP從未經過適當的設計。 它從來沒有來自適當的研究機構(與Haskell / FP相反)。 Lambda演算為函數式編程提供了完整的理論基礎。 OOP沒有什麼可比擬的。

在短期內使用OOP似乎是無辜的,尤其是在新建項目中。 但是使用OOP的長期後果是什麼? OOP是定時炸彈,當代碼庫變得足夠大時,它會在將來的某個時候爆炸。

項目被拖延,錯過了最後期限,開發人員精疲力盡,添加新功能幾乎是不可能的。 該組織將代碼庫標記為"遺留代碼庫",並且開發團隊計劃進行重寫。

OOP對人腦而言並不自然,我們的思維過程圍繞"做"的事情進行-漫步,與朋友交談,吃披薩。 我們的大腦已經進化為能夠做事,而不是將世界組織成抽象對象的複雜層次。

OOP代碼不是確定性的-與函數式編程不同,我們不能保證在輸入相同的情況下獲得相同的輸出。 這使得對該程序進行推理非常困難。 舉一個簡化的例子,2 + 2或Calculator.Add(2,2)的輸出通常等於4,但是有時它可能等於3、5,甚至1004。Calculator對象的依存關係可能會改變 微妙而深刻的計算結果。 這太糟糕了...


彈性框架的需求

無論編程範例如何,優秀的程序員都會編寫出良好的代碼,糟糕的程序員會編寫出不良的代碼。 但是,編程範例應限制不良的程序員進行過多的破壞。 當然,不是您本人,因為您已經在閱讀本文並努力學習。 糟糕的程序員永遠都沒有時間學習,他們只會瘋狂地按下鍵盤上的隨機按鈕。 無論您是否喜歡,您都將與糟糕的程序員一起工作,其中有些人真的非常糟糕。 而且,不幸的是,OOP沒有足夠的約束來阻止不良的程序員造成太大的損失。 糟糕...

我不認為自己是一個糟糕的程序員,但是即使沒有強大的框架來完成我的工作,我也無法編寫出優質的代碼。是的,有些框架本身會遇到一些非常特殊的問題(例如Angular或ASP.Net)。

我不是在談論軟件框架。 我說的是框架的更抽象的字典定義:"基本的支持結構",即與代碼組織和處理代碼複雜性等更抽象的東西相關的框架。 儘管面向對象的編程和函數式編程都是編程範例,但它們都是非常高級的框架。

限制我們的選擇

C ++是一種可怕的[面向對象]語言…而且將項目限制為C意味著人們不會用任何愚蠢的"對象模型" c&@ p搞砸了。 — Linux的創建者Linus Torvalds

Linus Torvalds以對C ++和OOP的公開批評而聞名。 他100%正確的一件事是限制程序員可以做出的選擇。 實際上,程序員選擇的次數越少,代碼的彈性就越大。 在上面的引用中,Linus Torvalds強烈建議有一個良好的框架作為我們代碼的基礎。

面向對象編程—價值萬億美元的災難

Photo by specphotops on Unsplash

許多人不喜歡道路上的速度限制,但這對於防止人們墜毀致死至關重要。 同樣,一個好的編程框架應該提供防止我們做愚蠢的事情的機制。

一個好的編程框架可以幫助我們編寫可靠的代碼。 首先,它應該通過提供以下內容來幫助降低複雜性:

  • 模塊化和可重用性
  • 正確的狀態隔離
  • 高信噪比

不幸的是,OOP為開發人員提供了太多的工具和選擇,而沒有施加適當的限制。 即使OOP承諾解決模塊化問題並提高可重用性,但它仍無法兌現其承諾(稍後會詳細介紹)。 OOP代碼鼓勵使用共享的可變狀態,這種狀態一次又一次被證明是不安全的。 OOP通常需要很多樣板代碼(低信噪比)。

函數式編程

函數編程到底是什麼?有些人認為它是一個高度複雜的編程範例,僅適用於學術界,不適合“現實世界”。這離事實還遠!

是的,函數式編程具有強大的數學基礎,並且紮根於lambda演算。 但是,它的大多數想法是對更主流的編程語言中的弱點的回應。 函數是函數式編程的核心抽象。 如果使用得當,函數可以提供一定程度的代碼模塊化和可重用性,這在OOP中是前所未有的。 它甚至具有解決可為空問題的設計模式,並提供了一種錯誤處理的高級方法。

函數式編程確實做得很好的一件事是,它可以幫助我們編寫可靠的軟件。 對調試器的需求幾乎完全消失了。 是的,無需單步執行代碼並觀察變量。 我個人很長時間都沒有接觸過調試器。

最好的部分? 如果您已經知道如何使用函數,那麼您已經是函數程序員。 您只需要學習如何充分利用這些函數即可!

我不是在講函數編程,也不是很在意您編寫代碼所使用的編程範例。 我只是試圖傳達函數式編程提供的機制,以解決OOP /命令式編程固有的問題。


關於OOP,我們全都錯了

很抱歉,我很久以前就為該主題創造了"對象"一詞,因為它使許多人專注於較小的想法。 大想法是消息傳遞。-OOP的發明者艾倫·凱(Alan Kay)

Erlang通常不被認為是一種面向對象的語言。但是可能Erlang是那裡唯一的主流面嚮對象語言。是的,Smalltalk當然是一種正確的OOP語言-但是,它並未得到廣泛使用。 Smalltalk和Erlang都按照其發明者Alan Kay最初的意圖使用OOP。

訊息傳遞

艾倫·凱(Alan Kay)在1960年代創造了"面向對象程序設計"一詞。 他具有生物學背景,並試圖使計算機程序以與活細胞相同的方式進行通信。

面向對象編程—價值萬億美元的災難

Photo by Muukii on Unsplash

艾倫·凱(Alan Kay)的主要想法是讓相互獨立的程序(單元)通過相互發送消息進行通信。 獨立程序的狀態永遠不會與外界共享(封裝)。

而已。 OOP從未打算具有繼承,多態性," new"關鍵字以及無數的設計模式之類的東西。

最純正的OOP

Erlang是最純粹的OOP。 與更主流的語言不同,它專注於OOP的核心概念-消息傳遞。 在Erlang中,對象通過在對象之間傳遞不可變消息進行通信。

有沒有證據表明不可變消息是比方法調用更好的方法?

當然好! Erlang可能是世界上最可靠的語言。 它為世界上大多數電信(以及互聯網)基礎設施提供動力。 用Erlang編寫的某些系統的可靠性為99.9999999%(您沒看錯-九個九)。

代碼複雜度

使用受OOP影響的編程語言,計算機軟件變得更冗長,可讀性更差,描述性更強,並且難以修改和維護。

—理查德·曼斯菲爾德(Richard Mansfield)

軟件開發的最重要方面是降低代碼複雜度。 句號!如果代碼庫變得無法維護,那麼任何精美的功能都不重要。 如果代碼庫變得過於複雜和不可維護,那麼即使100%的測試覆蓋率也毫無價值。

是什麼使代碼庫變得複雜? 有很多事情要考慮,但是在我看來,最主要的犯罪者是:共享的可變狀態錯誤的抽象以及低信噪比(通常由樣板代碼引起)。 所有這些在OOP中都很普遍。

狀態問題

面向對象編程—價值萬億美元的災難

Photo by Mika Baumeister on Unsplash

什麼是狀態?簡而言之,狀態是存儲在內存中的任何臨時數據。考慮一下OOP中的變量或字段/屬性。命令式編程(包括OOP)根據程序狀態和對該狀態的更改來描述計算。聲明式(函數式)編程改為描述所需的結果,而不明確指定對狀態的更改。

可變狀態-精神雜耍的行為

我認為,當您構建可變對象的大對象圖時,大型面向對象的程序會越來越複雜。 您知道,嘗試理解並牢記調用方法時會發生什麼以及副作用是什麼。

— Clojure的創建者Rich Hickey

面向對象編程—價值萬億美元的災難

Image source:

狀態本身是無害的。 但是,易變的狀態是罪魁禍首。 特別是如果共享。 到底什麼是可變狀態? 可以更改的任何狀態。 考慮OOP中的變量或字段。

請提供真實示例!

您有一張空白的紙,在上面寫了一個便箋,最後得到的是同一張紙,但狀態不同(文本)。 您實際上已經改變了這張紙的狀態。

在現實世界中,這是完全可以的,因為沒人會關心那張紙。 除非這張紙是蒙娜麗莎的原始畫。

人腦的侷限性

為什麼可變狀態這麼大的問題? 人腦是已知宇宙中最強大的機器。 但是,我們的大腦在處理狀態上確實很不好,因為我們一次只能記住5個項目。 如果僅考慮代碼的功能,而不考慮代碼在代碼庫中更改的變量,則對代碼片段進行推理就容易得多。

以可變狀態進行編程是一種精神上的雜耍️。 我不認識你,但我可能會打兩個球。 給我三個或三個以上的球,我一定會丟掉的。 那麼,為什麼我們每天都要在工作中嘗試這種精神雜耍的行為?

不幸的是,對易變狀態的心理處理是OOP的核心。 在對象上存在方法的唯一目的是使同一對象發生突變。

分散狀態

面向對象編程—價值萬億美元的災難

Photo by Markus Spiske on Unsplash

OOP通過分散整個程序的狀態,使代碼組織的問題更加嚴重。 然後,分散狀態在各種對象之間混雜地共享。

請提供真實示例!

讓我們先忘了我們都是成年人,並假裝我們正在嘗試組裝一輛超酷的樂高卡車。

但是,有一個陷阱-所有卡車零件都與其他樂高玩具中的零件隨機混合。 然後將它們隨機放入50個不同的盒子中。 而且,您不允許將卡車零件歸為一類–您必須將頭部放在各種卡車零件所在的位置,並且只能一一取出。

是的,您最終會組裝那輛卡車,但是要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是隔離的。 您總是知道某些狀態從何而來。 狀態永遠不會分散在您的不同職能中。 在OOP中,每個對象都有其自己的狀態,並且在構建程序時,必須牢記當前正在使用的所有對象的狀態。

為了使我們的生活更輕鬆,最好只讓一小部分代碼庫處理狀態。 讓應用程序的核心部分成為無狀態且純淨的。 這實際上是前端(也稱為Redux)上的flux模式取得巨大成功的主要原因。

混雜共享狀態

似乎由於分散的易變狀態,我們的生活還不夠艱難,OOP向前邁進了一步!

請提供真實示例!

現實世界中的可變狀態幾乎從來都不是問題,因為事物是保密的,並且永遠不會共享。 這就是工作中的"正確封裝"。 想象一個畫家正在創作下一張《蒙娜麗莎》的作品。 他獨自從事繪畫工作,完成後將其傑作賣給數百萬。

現在,他厭倦了所有的錢,決定做一些不同的事情。 他認為舉辦繪畫派對是個好主意。 他邀請他的朋友精靈,甘道夫,警察和殭屍來幫助他。 團隊合作! 他們都開始同時在同一塊畫布上繪畫。 當然,這並沒有帶來什麼好處-這幅畫是一場徹底的災難!

共享的可變狀態在現實世界中毫無意義。但這正是OOP程序中發生的事情-狀態在各種對象之間混雜地共享,並且它們以他們認為合適的任何方式對其進行變異。反過來,隨著代碼庫的不斷增長,這使得對程序的推理越來越難

併發問題

OOP代碼中可變狀態的混雜共享使得幾乎不可能並行化此類代碼。 為了解決這個問題,已經發明瞭複雜的機制。 發明了線程鎖定,互斥和許多其他機制。 當然,這種複雜的方法有其自身的缺點-死鎖,缺乏可組合性,調試多線程代碼非常困難且耗時。 我什至沒有在談論由於使用這種併發機制而導致的複雜性增加。

並非所有狀態都是邪惡的

所有狀態都是邪惡的嗎? 不,艾倫·凱狀態可能並不邪惡! 如果狀態可變是真正隔離的(不是" OOP方式"隔離的),則可能很好。

具有不變的數據傳輸對象也是完全可以的。 這裡的關鍵是"不變的"。 然後使用此類對象在函數之間傳遞數據。

但是,此類對象也將使OOP方法和屬性完全多餘。 如果對象無法突變,在其上具有方法和屬性有什麼用?

可變性是OOP固有的

有人可能會認為可變狀態是OOP中的設計選擇,而不是義務。 該聲明有問題。 這不是設計選擇,而是幾乎唯一的選擇。 是的,可以將不可變的對象傳遞給Java / C#中的方法,但是很少這樣做,因為大多數開發人員默認使用數據可變。 即使開發人員試圖在其OOP程序中正確使用不變性,這些語言也沒有提供內置機制來實現不變性以及有效處理不變數據(即持久性數據結構)。

是的,我們可以確保對象僅通過傳遞不可變消息進行通信,而從不傳遞任何引用(很少這樣做)。這樣的程序將比主流的OOP更可靠。但是,一旦收到消息,對象仍然必須改變其自身的狀態。消息是一種副作用,其唯一目的是引起更改。如果消息無法改變其他對象的狀態,它們將毫無用處。

在不引起狀態變化的情況下不可能使用OOP。


封裝特洛伊木馬

面向對象編程—價值萬億美元的災難

Photo by Jamie McInall from Pexels

我們被告知封裝是OOP的最大優點之一。 可以保護對象的內部狀態不受外部訪問。 不過,這有一個小問題。 沒用!

封裝是OOP的特洛伊木馬。 它通過使它看起來安全來出售共享可變狀態的想法。 封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫中,從而使代碼庫從內部腐爛。

全局狀態問題

有人告訴我們,全局狀態是萬惡之源。應不惜一切代價避免這種情況。我們從未被告知的是,封裝實際上是特殊的全局狀態。

為了提高代碼的效率,對象傳遞的依據不是其值,而是其引用。 這就是"依賴注入"落空的地方。

讓我解釋。 每當我們在OOP中創建對象時,都會將對其依賴項的引用傳遞給構造函數。 這些依賴項也有自己的內部狀態。 新創建的對象在其內部狀態中愉快地存儲了對這些依賴項的引用,然後很樂意以自己喜歡的任何方式對其進行修改。 它還會將這些引用傳遞給可能最終使用的其他任何內容。

這會創建一個複雜的圖形,其中包含混雜共享的對象,這些對象最終都會改變彼此的狀態。 反過來,這又引起了巨大的問題,因為幾乎看不到是什麼導致了程序狀態的改變。 嘗試調試此類狀態更改可能會浪費很多時間。 而且,如果您不必處理併發性,那麼您會很幸運(稍後會詳細介紹)。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。 通過使用奇特的屬性或方法來改變對象的狀態都沒關係-結果相同:改變狀態。

現實世界建模中的問題

面向對象編程—價值萬億美元的災難

Photo by Markus Spiske on Unsplash

有人說OOP試圖模擬現實世界。 事實並非如此-OOP與現實世界無關。 嘗試將程序建模為對象可能是最大的OOP錯誤之一。

現實世界不是分層的

OOP嘗試將所有事物建模為對象的層次結構。 不幸的是,事實並非如此。 現實世界中的對象使用消息彼此交互,但是它們大多彼此獨立。

現實世界中的繼承

OOP繼承不是以真實世界為模型的。 現實世界中的父對象無法在運行時更改子對象的行為。 即使您從父母那裡繼承了您的DNA,他們也無法隨意改變您的DNA。 您不會從父母那裡繼承"行為",而是會發展自己的行為。 而且您無法"凌駕"父母的行為。

現實世界沒有方法

您寫的紙上有"寫"方法嗎? 沒有! 您拿一張空紙,拿起筆,然後寫一些文字。 作為一個人,您也沒有"寫"方法-您是根據外部事件或內部想法決定寫一些文本的。

名詞王國

對象將函數和數據結構以不可分割的單位綁定在一起。 我認為這是一個基本錯誤,因為函數和數據結構屬於完全不同的世界。

— Erlang的創建者Joe Armstrong

面向對象編程—價值萬億美元的災難

Photo by Cederic X on Unsplash

對象(或名詞)是OOP的核心。 OOP的基本侷限性是它迫使一切都變成名詞。 並非所有事物都應建模為名詞。 操作(功能)不應建模為對象。 當我們只需要一個將兩個數字相乘的函數時,為什麼要強制創建Multiplier類? 只需具有一個乘法函數,讓數據成為數據,讓函數成為函數!

在非OOP語言中,完成瑣碎的事情(例如將數據保存到文件中)非常簡單-與以普通英語描述動作的方式非常相似。

請提供真實示例!

當然,回到畫家的例子,畫家擁有一個PaintingFactory。 他聘請了專用的BrushManager,ColorManager,CanvasManager和MonaLisaProvider。 他的好朋友殭屍利用了BrainConsumingStrategy策略。 這些對象依次定義以下方法:CreatePainting,FindBrush,PickColor,CallMonaLisa和ConsumeBrainz。

當然,這是愚蠢的,在現實世界中不可能發生。 簡單的繪畫行為已經產生了多少不必要的複雜性?

當允許它們與對象分開存在時,無需發明奇怪的概念來保留您的功能。

單元測試

面向對象編程—價值萬億美元的災難

Photo by Ani Kolleshi on Unsplash

自動化測試是開發過程中的重要組成部分,並且在防止迴歸(即將錯誤引入現有代碼中)方面有很大幫助。 單元測試在自動化測試過程中扮演著重要角色。

有些人可能會不同意,但是眾所周知,OOP代碼很難進行單元測試。 單元測試假定測試是獨立進行的,並使方法可單元測試:

· 它的依賴關係必須提取到一個單獨的類中。

· 為新創建的類創建一個接口。

· 聲明字段以保存新創建的類的實例。

· 利用模擬框架模擬依賴項。

· 利用依賴項注入框架來注入依賴項。

為了使一段代碼可測試,還必須創建多少複雜性? 僅使一些代碼可測試就浪費了多少時間?

PS,我們還必須實例化整個類以測試單個方法。 這還將從其所有父類中引入代碼。

使用OOP,為遺留代碼編寫測試變得更加困難-幾乎不可能。 已圍繞測試舊版OOP代碼創建了整個公司(TypeMock)。

樣板代碼

當涉及信噪比時,樣板代碼可能是最大的違法者。 樣板代碼是使程序編譯所需的"噪聲"。 樣板代碼需要花費一些時間來編寫代碼,並且由於增加的噪音而使代碼庫的可讀性降低。

雖然在OOP中建議"對接口編程,而不對實現編程",但並非所有內容都應該成為接口。 出於可測試性的唯一目的,我們不得不在整個代碼庫中使用接口。 我們可能還必須使用依賴項注入,這進一步引入了不必要的複雜性。

測試私有方法

有人說不應測試私有方法……我傾向於不同意,單元測試之所以被稱為"單元",是因為有一個原因-孤立地測試小的代碼單元。 然而,在OOP中測試私有方法幾乎是不可能的。 我們不應僅出於可測試性而創建內部私有方法。

為了實現私有方法的可測試性,通常必須將它們提取到單獨的對象中。 反過來,這引入了不必要的複雜性和樣板代碼。


重構

重構是開發人員日常工作的重要組成部分。具有諷刺意味的是,眾所周知,OOP代碼難以重構。重構應該使代碼更簡單,更易於維護。相反,重構的OOP代碼變得更加複雜-為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建接口。即使那樣,如果沒有諸如Resharper之類的專用工具,重構OOP代碼確實非常困難。

<code>// before refactoring:
public class CalculatorForm {
private string aText, bText;

private bool IsValidInput(string text) => true;

private void btnAddClick(object sender, EventArgs e) {
if ( !IsValidInput(bText) || !IsValidInput(aText) ) {
return;
}
}
}


// after refactoring:
public class CalculatorForm {
private string aText, bText;

private readonly IInputValidator _inputValidator;

public CalculatorForm(IInputValidator inputValidator) {
_inputValidator = inputValidator;
}

private void btnAddClick(object sender, EventArgs e) {
if ( !_inputValidator.IsValidInput(bText)
|| !_inputValidator.IsValidInput(aText) ) {
return;
}

}
}

public interface IInputValidator {
bool IsValidInput(string text);
}

public class InputValidator : IInputValidator {
public bool IsValidInput(string text) => true;
}

public class InputValidatorFactory {
public IInputValidator CreateInputValidator() => new InputValidator();
}/<code>


在上面的簡單示例中,僅提取一種方法,行數就增加了一倍以上。 當重構代碼以首先降低複雜性時,為什麼重構會帶來更大的複雜性?

在上面的簡單示例中,僅提取一種方法,行數就增加了一倍以上。當重構代碼以首先降低複雜性時,為什麼重構會帶來更大的複雜性? 將此與JavaScript中非OOP代碼的類似重構進行對比:

<code>// before refactoring:

// calculator.js:
const isValidInput = text => true;

const btnAddClick = (aText, bText) => {
if (!isValidInput(aText) || !isValidInput(bText)) {
return;
}
}


// after refactoring:

// inputValidator.js:

export const isValidInput = text => true;

// calculator.js:
import { isValidInput } from './inputValidator';

const btnAddClick = (aText, bText, _isValidInput = isValidInput) => {
if (!_isValidInput(aText) || !_isValidInput(bText)) {
return;
}
}/<code>

代碼從字面上保持不變-我們只是將isValidInput函數移至另一個文件,並添加了一行以導入該函數。 為了便於測試,我們還向函數簽名添加了_isValidInput。

這是一個簡單的示例,但是在實踐中,隨著代碼庫變大,複雜度呈指數增長。

不僅如此。 重構OOP代碼非常危險。 複雜的依賴關係圖和狀態分散在整個OOP代碼庫中,使人腦無法考慮所有潛在問題。

創可貼

面向對象編程—價值萬億美元的災難

Image source: Photo by Pixabay from Pexels

當某事不起作用時我們該怎麼辦? 很簡單,我們只有兩個選擇-丟棄它或嘗試修復它。 OOP是不容易被拋棄的東西,數百萬的開發人員接受了OOP培訓。 全球數以百萬計的組織都在使用OOP。

您可能現在已經看到OOP不能真正起作用,它使我們的代碼變得複雜且不可靠。 而且您並不孤單! 數十年來,人們一直在努力解決OOP代碼中普遍存在的問題。 他們提出了無數的設計模式。

設計模式

OOP提供了一組指導原則,從理論上講應允許開發人員逐步構建越來越大的系統:SOLID原理,依賴項注入,設計模式等。

不幸的是,設計模式只不過是創可貼。 它們的存在僅僅是為了解決OOP的缺點。 關於該主題的書籍甚至很多。 如果他們不負責為我們的代碼庫引入巨大的複雜性,那麼它們並不會那麼糟糕。

問題工廠

實際上,不可能編寫良好且可維護的面向對象代碼。

一方面,我們擁有一個不一致的OOP代碼庫,並且似乎沒有遵循任何標準。 在頻譜的另一端,我們有一堆過度設計的代碼,一堆錯誤的抽象是彼此疊加構建的。 設計模式對於構建這樣的抽象塔非常有幫助。

很快,添加新的功能,甚至理解所有的複雜性,變得越來越困難。 代碼庫將充滿諸如SimpleBeanFactoryAwareAspectInstanceFactory,AbstractInterceptorDrivenBeanDefinitionDecorator,TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactory之類的內容。

必須浪費寶貴的腦力去理解開發人員自己創建的抽象塔。 在許多情況下,缺少結構要比結構不好(如果您問我)要好。

面向對象編程—價值萬億美元的災難

Image source:

進一步閱讀:FizzBuzzEnterpriseEdition

OOP四個支柱的陷落

OOP的四個支柱是:抽象,繼承,封裝和多態。

讓我們一一看清它們的真實含義。

面向對象編程—價值萬億美元的災難

繼承

我認為缺乏可重用性的是面向對象的語言,而不是函數式語言。 因為面嚮對象語言的問題在於它們擁有了它們所伴隨的所有隱式環境。 您想要香蕉,但是得到的是一隻大猩猩,拿著香蕉和整個叢林。

— Erlang的創建者Joe Armstrong

OOP繼承與現實世界無關。 實際上,繼承是實現代碼可重用性的一種次等方式。 四人幫派明確建議優先選擇組成而不是繼承。 一些現代的編程語言完全避免繼承。

繼承存在一些問題:

  • 引入大量您班級甚至不需要的代碼(香蕉和叢林問題)。
  • 將類的某些部分定義在其他地方會使代碼難以推理,尤其是在具有多個繼承級別的情況下。
  • 在大多數編程語言中,甚至無法實現多重繼承。 大多數情況下,繼承不能用作代碼共享機制。

OOP多態性

多態性很棒,它允許我們在運行時更改程序行為。 但是,它是計算機編程中非常基本的概念。 我不太確定為什麼OOP會如此關注多態性。 OOP多態性可以完成工作,但又導致精神錯亂。 這使代碼庫變得更加複雜,並且要推理出要調用的具體方法變得非常困難。

另一方面,函數式編程使我們能夠以更加優雅的方式實現相同的多態性……只需簡單地傳入定義所需運行時行為的函數即可。 還有什麼比這更簡單? 無需在多個文件(和接口)中定義一堆重載的抽象虛擬方法。

封裝

如前所述,封裝是OOP的特洛伊木馬。 它實際上是一種美化的全局可變狀態,使不安全的代碼顯得安全。 不安全的編碼實踐是OOP程序員在日常工作中所依賴的支柱……

抽象

OOP中的抽象試圖通過向程序員隱藏不必要的細節來解決複雜性。 從理論上講,它應該允許開發人員推理代碼庫而不必考慮隱藏的複雜性。

我什至不知道該說些什麼……一個簡單的概念的奇特詞。 在過程/函數式語言中,我們可以簡單地"隱藏"相鄰文件中的實現細節。 無需將此基本行為稱為"抽象"。

有關OOP支柱下降的更多詳細信息,請閱讀

為什麼OOP主導了軟件行業?

答案很簡單,外星人種族與美國國家安全局(以及俄羅斯人)密謀將我們的程序員折磨致死……

面向對象編程—價值萬億美元的災難

Photo by Gaetano Cessati on Unsplash

但是說真的,Java可能就是答案。

自從MS-DOS以來,Java是計算中最令人困擾的事情。

-面向對象編程的發明者艾倫·凱(Alan Kay)

Java很簡單

與其他語言相比,Java在1995年首次引入時是一種非常簡單的編程語言。 當時,編寫桌面應用程序的入門門檻很高。 開發桌面應用程序需要使用C編寫底層Win32 API,並且開發人員還必須考慮手動內存管理。 另一種選擇是Visual Basic,但是許多人可能不想將自己鎖定在Microsoft生態系統中。

引入Java時,它是免費的,並且可以在所有平臺上使用,因此對於許多開發人員而言,這是輕而易舉的事。 諸如內置的垃圾收集,友好命名的API(與神秘的win32 API相比),適當的名稱空間以及熟悉的類似於C的語法等使Java更加易於使用。

GUI編程也變得越來越流行,而且似乎各種UI組件都可以很好地映射到類。 IDE中的方法自動完成功能還使人們聲稱OOP API更易於使用。

如果Java不強制對開發人員進行OOP,那麼Java可能還不錯。 關於Java的其他一切似乎都還不錯。 其他主流編程語言所缺乏的垃圾回收,可移植性,異常處理功能在1995年確實很棒,

然後C#出現了

最初,微軟一直嚴重依賴Java。 當事情開始變得不對勁時(在與Sun Microsystems就Java許可展開長期法律鬥爭之後),微軟決定投資自己的Java版本。 那就是C#1.0誕生的時候。 C#作為一種語言一直被認為是"更好的Java"。 但是,存在一個巨大的問題-它是相同的OOP語言,但存在相同的缺陷,但隱藏在稍微改進的語法中。

微軟一直在對其.NET生態系統進行大量投資,其中還包括良好的開發人員工具。 多年來,Visual Studio可能一直是最好的IDE之一。 反過來,這導致了.NET框架的廣泛採用,尤其是在企業中。

最近,Microsoft通過推銷其TypeScript一直在瀏覽器生態系統中進行大量投資。 TypeScript很棒,因為它可以編譯純JavaScript,並添加諸如靜態類型檢查之類的內容。 沒什麼大不了的是,它沒有對函數結構的適當支持-沒有內置的不變數據結構,沒有函數組成,沒有適當的模式匹配。 TypeScript是OOP優先的,對於瀏覽器來說大多數是C#。 Anders Hejlsberg甚至負責C#和TypeScript的設計。

函數式語言

另一方面,函數式語言從來沒有像Microsoft這樣的大公司支持。 由於投資很小,因此F#不算在內。 函數式語言的開發主要是社區驅動的。 這可能解釋了OOP和FP語言在流行度方面的差異。

是時候向前看了?

現在我們知道OOP是一個失敗的實驗。 現在該繼續前進了。 現在,我們作為一個社區承認這個想法使我們失敗了,我們必須放棄它。

-勞倫斯·克魯伯納

面向對象編程—價值萬億美元的災難

Photo by SpaceX on Unsplash

為什麼我們堅持使用根本上不是組織程序的次優方式的東西? 這是無知嗎? 我對此表示懷疑,從事軟件工程的人並不愚蠢。 我們是否更擔心通過使用一些奇特的OOP術語(例如"設計模式","抽象","封裝","多態性"和"接口隔離")來面對同行,"看上去很聰明"? 可能不會。

我認為繼續使用我們幾十年來一直在使用的東西真的很容易。 大多數人從未真正嘗試過函數式編程。 那些擁有(像我自己)的人永遠不會回到編寫OOP代碼的過程。

亨利·福特曾經有句著名的話:"如果我問人們他們想要什麼,他們會說更快的馬"。 在軟件世界中,大多數人可能希望使用"更好的OOP語言"。 人們可以輕鬆地描述他們所遇到的問題(使代碼庫井井有條,而且不太複雜),但不是最佳解決方案。

有哪些選擇?

劇透警告:函數式編程。

面向對象編程—價值萬億美元的災難

Photo by Harley-Davidson on Unsplash

如果像函數子和Monad之類的詞使您有些不安,那麼您並不孤單! 如果函數式編程的某些概念使用更直觀的名稱,它們就不會那麼嚇人。 函數子? 這就是我們可以使用函數list.map進行轉換的東西。 單子? 可以鏈接的簡單計算!

試用函數式編程將使您成為更好的開發人員。 您最終將有時間編寫解決實際問題的真實代碼,而不必花費大量時間思考抽象和設計模式。

您可能沒有意識到這一點,但是您已經是一名函數程序員。 您是否在日常工作中使用函數? 是? 那麼您已經是一名函數式程序員! 您只需要學習如何充分利用這些函數即可。

Elixir和Elm是兩種具有非常柔和的學習曲線的強大函數式語言。 他們讓開發人員專注於最重要的事情–編寫可靠的軟件,同時消除了傳統函數式語言所具有的所有複雜性。

還有哪些其他選擇? 您的組織已經在使用C#嗎? 嘗試F#—它是一種了不起的函數式語言,並且與現有的.NET代碼具有出色的互操作性。 使用Java? 然後使用Scala或Clojure都是非常好的選擇。 使用JavaScript? 在正確的指導和支持下,JavaScript可以成為一種很好的函數式語言。


OOP的捍衛者

面向對象編程—價值萬億美元的災難

Photo by Ott Maidre from Pexels

我希望OOP的捍衛者會做出某種反應。 他們會說這篇文章充滿了錯誤。 有些甚至可能開始呼叫姓名。 他們甚至可以稱我為沒有實際OOP經驗的"初級"開發人員。 有人可能會說我的假設是錯誤的,例子是沒有用的。 隨你。

他們有權發表自己的意見。 但是,他們在辯護OOP方面的論點通常很薄弱。 具有諷刺意味的是,其中大多數人可能從未真正使用過真正的函數式語言進行編程。 如果您從未真正嘗試過兩者,那麼有人怎麼能在兩者之間進行比較? 這樣的比較不是很有用。

得墨耳定律不是很有用-它無法解決不確定性問題,共享可變狀態仍然是共享可變狀態,無論您如何訪問或更改該狀態。 a.total()並不比a.getB().getC().total()好多少。 它只是簡單地解決了問題。

域驅動設計? 這是一種有用的設計方法,它對複雜性有所幫助。 但是,它仍然無法解決共享可變狀態的根本問題。

只是工具箱中的工具...

我經常聽到人們說OOP只是工具箱中的另一個工具。 是的,它既是工具箱中的工具,又是馬和汽車都是運輸工具……畢竟,它們都具有相同的目的,對嗎? 當我們可以繼續騎好老馬時,為什麼要使用汽車呢?

歷史總是重演

這實際上使我想起了一些東西。 20世紀初,汽車開始取代馬匹。 1900年,紐約的道路上只有幾輛汽車,人們一直在用馬來運輸。 1917年,馬路上再也沒有馬匹留下。 馬業是一個巨大的產業。 圍繞糞便清潔之類的事物已經創建了整個企業。

人們抵制變化。 他們稱汽車為另一種最終消失的"時尚"。 畢竟,馬匹已經在這裡居住了幾個世紀了! 有些甚至要求政府幹預。

這有什麼關係? 軟件行業以OOP為中心。 數以百萬計的人接受過OOP培訓,數百萬公司在其代碼中使用OOP。 當然,他們會嘗試抹黑任何可能威脅到他們麵包和黃油的東西! 這只是常識。

我們清楚地看到了歷史在重演-在20世紀是馬與汽車,在21世紀是面向對象與函數式編程。


下一步是什麼?

· 再見,面向對象編程

· 面向對象編程超賣

· Eric Elliott的OOP被遺忘的歷史

· 為什麼OO很爛Erlang的作者Joe Armstrong

· 面向對象程序設計很糟糕,作者Brian Will

· 史蒂夫·耶格(Steve Yegge)在名詞王國中的處決

· 面向對象編程是否失敗?


(本文翻譯自Ilya Suzdalnitski的文章《Object-Oriented Programming — The Trillion Dollar Disaster》,參考:https://medium.com/better-programming/object-oriented-programming-the-trillion-dollar-disaster-92a4b666c7c7)


分享到:


相關文章: