'面向對象編程的災難:是時候考慮更新換代了'

"
全文共13316字,預計學習時長26分鐘


"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

面向對象編程的災難:是時候考慮更新換代了


Band-aids

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

面向對象編程的災難:是時候考慮更新換代了


Band-aids

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Pixabay


當某些東西不起作用時,我們該怎麼辦?很簡單,只有兩個選擇——扔掉或者試著修理。面向對象則難以去除,數百萬開發人員接受了面向對象的培訓。全世界數百萬的組織都在使用面向對象程序。

你可能已經看到面向對象編程並不能真正起作用,它使我們的代碼變得複雜和不可靠。然並非只有你一人面臨這樣的問題!幾十年來,人們一直在努力解決面向對象程序代碼中普遍存在的問題。他們想出了無數的設計模式。

設計模式

面向對象程序編程提供了一套原則,理論上應該可支持開發人員逐步構建越來越大的系統:SOLID原則、依賴注入、設計模式等等。

不幸的是,設計模式只不過是創可貼。它們的存在僅僅是為了解決面向對象程序設計的缺點。關於這個主題,已經寫了無數的書。如果不是給代碼庫帶來了巨大的複雜性,面向對象編程也就不會那麼糟糕。

問題工廠

事實上,編寫好的、可維護的面向對象代碼是不可能的。

另一方面,一個不一致的面向對象編程庫,似乎不符合任何標準。另一方面,有一個過度設計的代碼塔,一堆錯誤的抽象一個接一個地建立起來。設計模式對於構建這樣的抽象塔非常有幫助。

很快,增加新的功能,甚至理解所有的複雜性,變得越來越難。代碼庫將充滿像SimpleBeanFactoryAwareAspectInstanceFactory, AbstractInterceptorDrivenBeanDefinitionDecorator, TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactory.

試圖理解開發人員自己創造的抽象之塔,必須浪費寶貴的腦力。沒有結構在很多情況下比結構不好要好。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

面向對象編程的災難:是時候考慮更新換代了


Band-aids

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Pixabay


當某些東西不起作用時,我們該怎麼辦?很簡單,只有兩個選擇——扔掉或者試著修理。面向對象則難以去除,數百萬開發人員接受了面向對象的培訓。全世界數百萬的組織都在使用面向對象程序。

你可能已經看到面向對象編程並不能真正起作用,它使我們的代碼變得複雜和不可靠。然並非只有你一人面臨這樣的問題!幾十年來,人們一直在努力解決面向對象程序代碼中普遍存在的問題。他們想出了無數的設計模式。

設計模式

面向對象程序編程提供了一套原則,理論上應該可支持開發人員逐步構建越來越大的系統:SOLID原則、依賴注入、設計模式等等。

不幸的是,設計模式只不過是創可貼。它們的存在僅僅是為了解決面向對象程序設計的缺點。關於這個主題,已經寫了無數的書。如果不是給代碼庫帶來了巨大的複雜性,面向對象編程也就不會那麼糟糕。

問題工廠

事實上,編寫好的、可維護的面向對象代碼是不可能的。

另一方面,一個不一致的面向對象編程庫,似乎不符合任何標準。另一方面,有一個過度設計的代碼塔,一堆錯誤的抽象一個接一個地建立起來。設計模式對於構建這樣的抽象塔非常有幫助。

很快,增加新的功能,甚至理解所有的複雜性,變得越來越難。代碼庫將充滿像SimpleBeanFactoryAwareAspectInstanceFactory, AbstractInterceptorDrivenBeanDefinitionDecorator, TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactory.

試圖理解開發人員自己創造的抽象之塔,必須浪費寶貴的腦力。沒有結構在很多情況下比結構不好要好。

面向對象編程的災難:是時候考慮更新換代了

圖片來源: https://www.reddit.com/r/ProgrammerHumor/com


"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

面向對象編程的災難:是時候考慮更新換代了


Band-aids

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Pixabay


當某些東西不起作用時,我們該怎麼辦?很簡單,只有兩個選擇——扔掉或者試著修理。面向對象則難以去除,數百萬開發人員接受了面向對象的培訓。全世界數百萬的組織都在使用面向對象程序。

你可能已經看到面向對象編程並不能真正起作用,它使我們的代碼變得複雜和不可靠。然並非只有你一人面臨這樣的問題!幾十年來,人們一直在努力解決面向對象程序代碼中普遍存在的問題。他們想出了無數的設計模式。

設計模式

面向對象程序編程提供了一套原則,理論上應該可支持開發人員逐步構建越來越大的系統:SOLID原則、依賴注入、設計模式等等。

不幸的是,設計模式只不過是創可貼。它們的存在僅僅是為了解決面向對象程序設計的缺點。關於這個主題,已經寫了無數的書。如果不是給代碼庫帶來了巨大的複雜性,面向對象編程也就不會那麼糟糕。

問題工廠

事實上,編寫好的、可維護的面向對象代碼是不可能的。

另一方面,一個不一致的面向對象編程庫,似乎不符合任何標準。另一方面,有一個過度設計的代碼塔,一堆錯誤的抽象一個接一個地建立起來。設計模式對於構建這樣的抽象塔非常有幫助。

很快,增加新的功能,甚至理解所有的複雜性,變得越來越難。代碼庫將充滿像SimpleBeanFactoryAwareAspectInstanceFactory, AbstractInterceptorDrivenBeanDefinitionDecorator, TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactory.

試圖理解開發人員自己創造的抽象之塔,必須浪費寶貴的腦力。沒有結構在很多情況下比結構不好要好。

面向對象編程的災難:是時候考慮更新換代了

圖片來源: https://www.reddit.com/r/ProgrammerHumor/com


面向對象編程的災難:是時候考慮更新換代了


四大面向對象編程支柱的衰落


面向對象編程的四大支柱是:抽象、繼承、封裝和多態。

讓我們逐一來看看四大支柱到底是怎樣的。

繼承

可重用性的缺乏來自面向對象編程的語言,而不是函數語言。因為面向對象編程語言的問題是,其擁有所有這些隱含的環境。你想要一根香蕉,但你得到的是一直拿著香蕉的大猩猩和整個叢林。Joe Armstrong, Erlang的創造者


面向對象編程繼承與現實世界無關。事實上,繼承是實現代碼可重用性的低劣方式。“四人幫”已經明確提出,比起繼承,更喜歡組合。一些現代編程語言完全避免繼承。

繼承有幾個問題:

1. 引入許多類不需要的代碼(香蕉和叢林問題)。

2. 將類的一部分定義在其他地方會使代碼很難推理,尤其是在有多個繼承級別的情況下。

3. 在多數編程語言中,多重繼承幾乎是不可能的。這主要使得繼承作為代碼共享機制毫無用處。

多態

多態很棒,它允許我們在運行時改變編程行為。然而,這是計算機編程中非常基本的概念。面向對象編程多態性完成了這項工作,但再次導致精神雜耍行為。這會使得代碼庫變得非常複雜,並且關於被調用的具體方法的推理變得非常困難。

另一方面,函數編程使得我們以更簡潔的方式實現相同的多態性……只需傳遞一個定義所需運行時行為的函數。還有什麼比這更簡單的呢?不需要在多個文件(和接口)中定義一堆重載的抽象虛擬方法。

封裝

正如前面討論的,封裝是面向對象編程設計的特洛伊木馬。它實際上是一個美化的全局可變狀態,並使不安全的代碼看起來是安全的。不安全的編碼實踐是面向對象編程人員在日常工作中依賴的支柱……

抽象

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

在過程/功能語言中,我們可以簡單地在相鄰文件中“隱藏”實現細節。沒有必要稱這一基本行為為“抽象”。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

面向對象編程的災難:是時候考慮更新換代了


Band-aids

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Pixabay


當某些東西不起作用時,我們該怎麼辦?很簡單,只有兩個選擇——扔掉或者試著修理。面向對象則難以去除,數百萬開發人員接受了面向對象的培訓。全世界數百萬的組織都在使用面向對象程序。

你可能已經看到面向對象編程並不能真正起作用,它使我們的代碼變得複雜和不可靠。然並非只有你一人面臨這樣的問題!幾十年來,人們一直在努力解決面向對象程序代碼中普遍存在的問題。他們想出了無數的設計模式。

設計模式

面向對象程序編程提供了一套原則,理論上應該可支持開發人員逐步構建越來越大的系統:SOLID原則、依賴注入、設計模式等等。

不幸的是,設計模式只不過是創可貼。它們的存在僅僅是為了解決面向對象程序設計的缺點。關於這個主題,已經寫了無數的書。如果不是給代碼庫帶來了巨大的複雜性,面向對象編程也就不會那麼糟糕。

問題工廠

事實上,編寫好的、可維護的面向對象代碼是不可能的。

另一方面,一個不一致的面向對象編程庫,似乎不符合任何標準。另一方面,有一個過度設計的代碼塔,一堆錯誤的抽象一個接一個地建立起來。設計模式對於構建這樣的抽象塔非常有幫助。

很快,增加新的功能,甚至理解所有的複雜性,變得越來越難。代碼庫將充滿像SimpleBeanFactoryAwareAspectInstanceFactory, AbstractInterceptorDrivenBeanDefinitionDecorator, TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactory.

試圖理解開發人員自己創造的抽象之塔,必須浪費寶貴的腦力。沒有結構在很多情況下比結構不好要好。

面向對象編程的災難:是時候考慮更新換代了

圖片來源: https://www.reddit.com/r/ProgrammerHumor/com


面向對象編程的災難:是時候考慮更新換代了


四大面向對象編程支柱的衰落


面向對象編程的四大支柱是:抽象、繼承、封裝和多態。

讓我們逐一來看看四大支柱到底是怎樣的。

繼承

可重用性的缺乏來自面向對象編程的語言,而不是函數語言。因為面向對象編程語言的問題是,其擁有所有這些隱含的環境。你想要一根香蕉,但你得到的是一直拿著香蕉的大猩猩和整個叢林。Joe Armstrong, Erlang的創造者


面向對象編程繼承與現實世界無關。事實上,繼承是實現代碼可重用性的低劣方式。“四人幫”已經明確提出,比起繼承,更喜歡組合。一些現代編程語言完全避免繼承。

繼承有幾個問題:

1. 引入許多類不需要的代碼(香蕉和叢林問題)。

2. 將類的一部分定義在其他地方會使代碼很難推理,尤其是在有多個繼承級別的情況下。

3. 在多數編程語言中,多重繼承幾乎是不可能的。這主要使得繼承作為代碼共享機制毫無用處。

多態

多態很棒,它允許我們在運行時改變編程行為。然而,這是計算機編程中非常基本的概念。面向對象編程多態性完成了這項工作,但再次導致精神雜耍行為。這會使得代碼庫變得非常複雜,並且關於被調用的具體方法的推理變得非常困難。

另一方面,函數編程使得我們以更簡潔的方式實現相同的多態性……只需傳遞一個定義所需運行時行為的函數。還有什麼比這更簡單的呢?不需要在多個文件(和接口)中定義一堆重載的抽象虛擬方法。

封裝

正如前面討論的,封裝是面向對象編程設計的特洛伊木馬。它實際上是一個美化的全局可變狀態,並使不安全的代碼看起來是安全的。不安全的編碼實踐是面向對象編程人員在日常工作中依賴的支柱……

抽象

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

在過程/功能語言中,我們可以簡單地在相鄰文件中“隱藏”實現細節。沒有必要稱這一基本行為為“抽象”。

面向對象編程的災難:是時候考慮更新換代了


為什麼面向對象編程控制了這個行業?

答案很簡單,爬蟲類的外星種族與國家安全局(和俄羅斯人)合謀將美國程序員折磨致死……

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

面向對象編程的災難:是時候考慮更新換代了


Band-aids

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Pixabay


當某些東西不起作用時,我們該怎麼辦?很簡單,只有兩個選擇——扔掉或者試著修理。面向對象則難以去除,數百萬開發人員接受了面向對象的培訓。全世界數百萬的組織都在使用面向對象程序。

你可能已經看到面向對象編程並不能真正起作用,它使我們的代碼變得複雜和不可靠。然並非只有你一人面臨這樣的問題!幾十年來,人們一直在努力解決面向對象程序代碼中普遍存在的問題。他們想出了無數的設計模式。

設計模式

面向對象程序編程提供了一套原則,理論上應該可支持開發人員逐步構建越來越大的系統:SOLID原則、依賴注入、設計模式等等。

不幸的是,設計模式只不過是創可貼。它們的存在僅僅是為了解決面向對象程序設計的缺點。關於這個主題,已經寫了無數的書。如果不是給代碼庫帶來了巨大的複雜性,面向對象編程也就不會那麼糟糕。

問題工廠

事實上,編寫好的、可維護的面向對象代碼是不可能的。

另一方面,一個不一致的面向對象編程庫,似乎不符合任何標準。另一方面,有一個過度設計的代碼塔,一堆錯誤的抽象一個接一個地建立起來。設計模式對於構建這樣的抽象塔非常有幫助。

很快,增加新的功能,甚至理解所有的複雜性,變得越來越難。代碼庫將充滿像SimpleBeanFactoryAwareAspectInstanceFactory, AbstractInterceptorDrivenBeanDefinitionDecorator, TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactory.

試圖理解開發人員自己創造的抽象之塔,必須浪費寶貴的腦力。沒有結構在很多情況下比結構不好要好。

面向對象編程的災難:是時候考慮更新換代了

圖片來源: https://www.reddit.com/r/ProgrammerHumor/com


面向對象編程的災難:是時候考慮更新換代了


四大面向對象編程支柱的衰落


面向對象編程的四大支柱是:抽象、繼承、封裝和多態。

讓我們逐一來看看四大支柱到底是怎樣的。

繼承

可重用性的缺乏來自面向對象編程的語言,而不是函數語言。因為面向對象編程語言的問題是,其擁有所有這些隱含的環境。你想要一根香蕉,但你得到的是一直拿著香蕉的大猩猩和整個叢林。Joe Armstrong, Erlang的創造者


面向對象編程繼承與現實世界無關。事實上,繼承是實現代碼可重用性的低劣方式。“四人幫”已經明確提出,比起繼承,更喜歡組合。一些現代編程語言完全避免繼承。

繼承有幾個問題:

1. 引入許多類不需要的代碼(香蕉和叢林問題)。

2. 將類的一部分定義在其他地方會使代碼很難推理,尤其是在有多個繼承級別的情況下。

3. 在多數編程語言中,多重繼承幾乎是不可能的。這主要使得繼承作為代碼共享機制毫無用處。

多態

多態很棒,它允許我們在運行時改變編程行為。然而,這是計算機編程中非常基本的概念。面向對象編程多態性完成了這項工作,但再次導致精神雜耍行為。這會使得代碼庫變得非常複雜,並且關於被調用的具體方法的推理變得非常困難。

另一方面,函數編程使得我們以更簡潔的方式實現相同的多態性……只需傳遞一個定義所需運行時行為的函數。還有什麼比這更簡單的呢?不需要在多個文件(和接口)中定義一堆重載的抽象虛擬方法。

封裝

正如前面討論的,封裝是面向對象編程設計的特洛伊木馬。它實際上是一個美化的全局可變狀態,並使不安全的代碼看起來是安全的。不安全的編碼實踐是面向對象編程人員在日常工作中依賴的支柱……

抽象

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

在過程/功能語言中,我們可以簡單地在相鄰文件中“隱藏”實現細節。沒有必要稱這一基本行為為“抽象”。

面向對象編程的災難:是時候考慮更新換代了


為什麼面向對象編程控制了這個行業?

答案很簡單,爬蟲類的外星種族與國家安全局(和俄羅斯人)合謀將美國程序員折磨致死……

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Gaetano Cessati


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

自從微軟拒絕服務以來,Java是計算領域發生的最令人苦惱的事情。Alan Kay,面向對象編程的發明者

Java很簡單

當JAVA在1995年首次被引入時,與其他語言相比,Java是一種非常簡單的編程語言。當時,編寫桌面應用程序的門檻很高。開發桌面應用程序需要用C語言編寫低級win32應用程序接口,開發人員還必須關心手動內存管理。另一種選擇是Visual Basic,但許多人可能不想把自己鎖在微軟的生態系統中。

當Java被引入時,對許多開發人員來說,它是一個簡單的工具,因為它是免費的,可以在所有平臺上使用。像內置垃圾收集、友好命名的APIs(與神祕的win32 APIs相比)、合適的名稱空間和熟悉的類C語法這樣的東西使Java更容易使用。

圖形用戶界面編程也變得越來越流行,似乎各種用戶界面組件都很好地映射到類上。IDEs中的方法自動完成也使人們聲稱面向對象編程接口更容易使用。

如果不是把面向對象編程強加給開發人員,也許Java不會那麼糟糕。Java的所有功能都很好。其中垃圾收集、可移植性、異常處理特性,是其他主流編程語言所缺乏的,但在1995年非常棒

然後C#出現了

最初,微軟一直嚴重依賴Java。當事情開始出錯時(在與太陽微系統公司就Java許可進行了長時間的法律鬥爭之後),微軟決定投資自己的Java版本。這就是C# 1.0誕生的時候。C#作為一種語言,一直被認為是“更好的Java”。然而,也存在個巨大的問題——隱藏在稍微改進的語法下,它同樣也屬於面嚮對象語言,有著同樣的缺陷。

微軟一直在大力投資NET生態系統,其中還包括良好的開發工具。多年來,視覺工作室可能是最好的集成開發環境之一。這反過來又導致了NET框架的廣泛使用,尤其是在企業中。

最近,微軟一直在大力投資瀏覽器生態系統,推出它的TypeScript。TypeScript很棒,因為它可以編譯成JavaScript並添加靜態類型檢查等內容。不足之處在於沒有對函數構造的適當支持——沒有內置的不可變數據結構,沒有函數組合,沒有適當的模式匹配。 TypeScript是面向對象編程的第一步,對於瀏覽器來說主要是C#。Anders Hejlsberg 甚至負責C#和打字稿的設計。

功能語言

另一方面,函數式語言從未得到像微軟這樣大公司的支持。鑑於投資不多,F#不算數。功能語言的發展主要是由社區驅動的。這可能解釋了面向對象程序設計語言和浮點語言之間流行程度的差異。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

面向對象編程的災難:是時候考慮更新換代了


Band-aids

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Pixabay


當某些東西不起作用時,我們該怎麼辦?很簡單,只有兩個選擇——扔掉或者試著修理。面向對象則難以去除,數百萬開發人員接受了面向對象的培訓。全世界數百萬的組織都在使用面向對象程序。

你可能已經看到面向對象編程並不能真正起作用,它使我們的代碼變得複雜和不可靠。然並非只有你一人面臨這樣的問題!幾十年來,人們一直在努力解決面向對象程序代碼中普遍存在的問題。他們想出了無數的設計模式。

設計模式

面向對象程序編程提供了一套原則,理論上應該可支持開發人員逐步構建越來越大的系統:SOLID原則、依賴注入、設計模式等等。

不幸的是,設計模式只不過是創可貼。它們的存在僅僅是為了解決面向對象程序設計的缺點。關於這個主題,已經寫了無數的書。如果不是給代碼庫帶來了巨大的複雜性,面向對象編程也就不會那麼糟糕。

問題工廠

事實上,編寫好的、可維護的面向對象代碼是不可能的。

另一方面,一個不一致的面向對象編程庫,似乎不符合任何標準。另一方面,有一個過度設計的代碼塔,一堆錯誤的抽象一個接一個地建立起來。設計模式對於構建這樣的抽象塔非常有幫助。

很快,增加新的功能,甚至理解所有的複雜性,變得越來越難。代碼庫將充滿像SimpleBeanFactoryAwareAspectInstanceFactory, AbstractInterceptorDrivenBeanDefinitionDecorator, TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactory.

試圖理解開發人員自己創造的抽象之塔,必須浪費寶貴的腦力。沒有結構在很多情況下比結構不好要好。

面向對象編程的災難:是時候考慮更新換代了

圖片來源: https://www.reddit.com/r/ProgrammerHumor/com


面向對象編程的災難:是時候考慮更新換代了


四大面向對象編程支柱的衰落


面向對象編程的四大支柱是:抽象、繼承、封裝和多態。

讓我們逐一來看看四大支柱到底是怎樣的。

繼承

可重用性的缺乏來自面向對象編程的語言,而不是函數語言。因為面向對象編程語言的問題是,其擁有所有這些隱含的環境。你想要一根香蕉,但你得到的是一直拿著香蕉的大猩猩和整個叢林。Joe Armstrong, Erlang的創造者


面向對象編程繼承與現實世界無關。事實上,繼承是實現代碼可重用性的低劣方式。“四人幫”已經明確提出,比起繼承,更喜歡組合。一些現代編程語言完全避免繼承。

繼承有幾個問題:

1. 引入許多類不需要的代碼(香蕉和叢林問題)。

2. 將類的一部分定義在其他地方會使代碼很難推理,尤其是在有多個繼承級別的情況下。

3. 在多數編程語言中,多重繼承幾乎是不可能的。這主要使得繼承作為代碼共享機制毫無用處。

多態

多態很棒,它允許我們在運行時改變編程行為。然而,這是計算機編程中非常基本的概念。面向對象編程多態性完成了這項工作,但再次導致精神雜耍行為。這會使得代碼庫變得非常複雜,並且關於被調用的具體方法的推理變得非常困難。

另一方面,函數編程使得我們以更簡潔的方式實現相同的多態性……只需傳遞一個定義所需運行時行為的函數。還有什麼比這更簡單的呢?不需要在多個文件(和接口)中定義一堆重載的抽象虛擬方法。

封裝

正如前面討論的,封裝是面向對象編程設計的特洛伊木馬。它實際上是一個美化的全局可變狀態,並使不安全的代碼看起來是安全的。不安全的編碼實踐是面向對象編程人員在日常工作中依賴的支柱……

抽象

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

在過程/功能語言中,我們可以簡單地在相鄰文件中“隱藏”實現細節。沒有必要稱這一基本行為為“抽象”。

面向對象編程的災難:是時候考慮更新換代了


為什麼面向對象編程控制了這個行業?

答案很簡單,爬蟲類的外星種族與國家安全局(和俄羅斯人)合謀將美國程序員折磨致死……

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Gaetano Cessati


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

自從微軟拒絕服務以來,Java是計算領域發生的最令人苦惱的事情。Alan Kay,面向對象編程的發明者

Java很簡單

當JAVA在1995年首次被引入時,與其他語言相比,Java是一種非常簡單的編程語言。當時,編寫桌面應用程序的門檻很高。開發桌面應用程序需要用C語言編寫低級win32應用程序接口,開發人員還必須關心手動內存管理。另一種選擇是Visual Basic,但許多人可能不想把自己鎖在微軟的生態系統中。

當Java被引入時,對許多開發人員來說,它是一個簡單的工具,因為它是免費的,可以在所有平臺上使用。像內置垃圾收集、友好命名的APIs(與神祕的win32 APIs相比)、合適的名稱空間和熟悉的類C語法這樣的東西使Java更容易使用。

圖形用戶界面編程也變得越來越流行,似乎各種用戶界面組件都很好地映射到類上。IDEs中的方法自動完成也使人們聲稱面向對象編程接口更容易使用。

如果不是把面向對象編程強加給開發人員,也許Java不會那麼糟糕。Java的所有功能都很好。其中垃圾收集、可移植性、異常處理特性,是其他主流編程語言所缺乏的,但在1995年非常棒

然後C#出現了

最初,微軟一直嚴重依賴Java。當事情開始出錯時(在與太陽微系統公司就Java許可進行了長時間的法律鬥爭之後),微軟決定投資自己的Java版本。這就是C# 1.0誕生的時候。C#作為一種語言,一直被認為是“更好的Java”。然而,也存在個巨大的問題——隱藏在稍微改進的語法下,它同樣也屬於面嚮對象語言,有著同樣的缺陷。

微軟一直在大力投資NET生態系統,其中還包括良好的開發工具。多年來,視覺工作室可能是最好的集成開發環境之一。這反過來又導致了NET框架的廣泛使用,尤其是在企業中。

最近,微軟一直在大力投資瀏覽器生態系統,推出它的TypeScript。TypeScript很棒,因為它可以編譯成JavaScript並添加靜態類型檢查等內容。不足之處在於沒有對函數構造的適當支持——沒有內置的不可變數據結構,沒有函數組合,沒有適當的模式匹配。 TypeScript是面向對象編程的第一步,對於瀏覽器來說主要是C#。Anders Hejlsberg 甚至負責C#和打字稿的設計。

功能語言

另一方面,函數式語言從未得到像微軟這樣大公司的支持。鑑於投資不多,F#不算數。功能語言的發展主要是由社區驅動的。這可能解釋了面向對象程序設計語言和浮點語言之間流行程度的差異。

面向對象編程的災難:是時候考慮更新換代了


是時候繼續前進了?


我們現在知道面向對象是一個失敗的實驗。是向前看的時候了。現在是我們作為一個社區承認這個想法讓我們失望的時候了,我們必須放棄它。Lawrence Krubner
"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

面向對象編程的災難:是時候考慮更新換代了


Band-aids

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Pixabay


當某些東西不起作用時,我們該怎麼辦?很簡單,只有兩個選擇——扔掉或者試著修理。面向對象則難以去除,數百萬開發人員接受了面向對象的培訓。全世界數百萬的組織都在使用面向對象程序。

你可能已經看到面向對象編程並不能真正起作用,它使我們的代碼變得複雜和不可靠。然並非只有你一人面臨這樣的問題!幾十年來,人們一直在努力解決面向對象程序代碼中普遍存在的問題。他們想出了無數的設計模式。

設計模式

面向對象程序編程提供了一套原則,理論上應該可支持開發人員逐步構建越來越大的系統:SOLID原則、依賴注入、設計模式等等。

不幸的是,設計模式只不過是創可貼。它們的存在僅僅是為了解決面向對象程序設計的缺點。關於這個主題,已經寫了無數的書。如果不是給代碼庫帶來了巨大的複雜性,面向對象編程也就不會那麼糟糕。

問題工廠

事實上,編寫好的、可維護的面向對象代碼是不可能的。

另一方面,一個不一致的面向對象編程庫,似乎不符合任何標準。另一方面,有一個過度設計的代碼塔,一堆錯誤的抽象一個接一個地建立起來。設計模式對於構建這樣的抽象塔非常有幫助。

很快,增加新的功能,甚至理解所有的複雜性,變得越來越難。代碼庫將充滿像SimpleBeanFactoryAwareAspectInstanceFactory, AbstractInterceptorDrivenBeanDefinitionDecorator, TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactory.

試圖理解開發人員自己創造的抽象之塔,必須浪費寶貴的腦力。沒有結構在很多情況下比結構不好要好。

面向對象編程的災難:是時候考慮更新換代了

圖片來源: https://www.reddit.com/r/ProgrammerHumor/com


面向對象編程的災難:是時候考慮更新換代了


四大面向對象編程支柱的衰落


面向對象編程的四大支柱是:抽象、繼承、封裝和多態。

讓我們逐一來看看四大支柱到底是怎樣的。

繼承

可重用性的缺乏來自面向對象編程的語言,而不是函數語言。因為面向對象編程語言的問題是,其擁有所有這些隱含的環境。你想要一根香蕉,但你得到的是一直拿著香蕉的大猩猩和整個叢林。Joe Armstrong, Erlang的創造者


面向對象編程繼承與現實世界無關。事實上,繼承是實現代碼可重用性的低劣方式。“四人幫”已經明確提出,比起繼承,更喜歡組合。一些現代編程語言完全避免繼承。

繼承有幾個問題:

1. 引入許多類不需要的代碼(香蕉和叢林問題)。

2. 將類的一部分定義在其他地方會使代碼很難推理,尤其是在有多個繼承級別的情況下。

3. 在多數編程語言中,多重繼承幾乎是不可能的。這主要使得繼承作為代碼共享機制毫無用處。

多態

多態很棒,它允許我們在運行時改變編程行為。然而,這是計算機編程中非常基本的概念。面向對象編程多態性完成了這項工作,但再次導致精神雜耍行為。這會使得代碼庫變得非常複雜,並且關於被調用的具體方法的推理變得非常困難。

另一方面,函數編程使得我們以更簡潔的方式實現相同的多態性……只需傳遞一個定義所需運行時行為的函數。還有什麼比這更簡單的呢?不需要在多個文件(和接口)中定義一堆重載的抽象虛擬方法。

封裝

正如前面討論的,封裝是面向對象編程設計的特洛伊木馬。它實際上是一個美化的全局可變狀態,並使不安全的代碼看起來是安全的。不安全的編碼實踐是面向對象編程人員在日常工作中依賴的支柱……

抽象

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

在過程/功能語言中,我們可以簡單地在相鄰文件中“隱藏”實現細節。沒有必要稱這一基本行為為“抽象”。

面向對象編程的災難:是時候考慮更新換代了


為什麼面向對象編程控制了這個行業?

答案很簡單,爬蟲類的外星種族與國家安全局(和俄羅斯人)合謀將美國程序員折磨致死……

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Gaetano Cessati


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

自從微軟拒絕服務以來,Java是計算領域發生的最令人苦惱的事情。Alan Kay,面向對象編程的發明者

Java很簡單

當JAVA在1995年首次被引入時,與其他語言相比,Java是一種非常簡單的編程語言。當時,編寫桌面應用程序的門檻很高。開發桌面應用程序需要用C語言編寫低級win32應用程序接口,開發人員還必須關心手動內存管理。另一種選擇是Visual Basic,但許多人可能不想把自己鎖在微軟的生態系統中。

當Java被引入時,對許多開發人員來說,它是一個簡單的工具,因為它是免費的,可以在所有平臺上使用。像內置垃圾收集、友好命名的APIs(與神祕的win32 APIs相比)、合適的名稱空間和熟悉的類C語法這樣的東西使Java更容易使用。

圖形用戶界面編程也變得越來越流行,似乎各種用戶界面組件都很好地映射到類上。IDEs中的方法自動完成也使人們聲稱面向對象編程接口更容易使用。

如果不是把面向對象編程強加給開發人員,也許Java不會那麼糟糕。Java的所有功能都很好。其中垃圾收集、可移植性、異常處理特性,是其他主流編程語言所缺乏的,但在1995年非常棒

然後C#出現了

最初,微軟一直嚴重依賴Java。當事情開始出錯時(在與太陽微系統公司就Java許可進行了長時間的法律鬥爭之後),微軟決定投資自己的Java版本。這就是C# 1.0誕生的時候。C#作為一種語言,一直被認為是“更好的Java”。然而,也存在個巨大的問題——隱藏在稍微改進的語法下,它同樣也屬於面嚮對象語言,有著同樣的缺陷。

微軟一直在大力投資NET生態系統,其中還包括良好的開發工具。多年來,視覺工作室可能是最好的集成開發環境之一。這反過來又導致了NET框架的廣泛使用,尤其是在企業中。

最近,微軟一直在大力投資瀏覽器生態系統,推出它的TypeScript。TypeScript很棒,因為它可以編譯成JavaScript並添加靜態類型檢查等內容。不足之處在於沒有對函數構造的適當支持——沒有內置的不可變數據結構,沒有函數組合,沒有適當的模式匹配。 TypeScript是面向對象編程的第一步,對於瀏覽器來說主要是C#。Anders Hejlsberg 甚至負責C#和打字稿的設計。

功能語言

另一方面,函數式語言從未得到像微軟這樣大公司的支持。鑑於投資不多,F#不算數。功能語言的發展主要是由社區驅動的。這可能解釋了面向對象程序設計語言和浮點語言之間流行程度的差異。

面向對象編程的災難:是時候考慮更新換代了


是時候繼續前進了?


我們現在知道面向對象是一個失敗的實驗。是向前看的時候了。現在是我們作為一個社區承認這個想法讓我們失望的時候了,我們必須放棄它。Lawrence Krubner
面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash SpaceX



為什麼我們堅持使用從根本上說是次優的程序組織方式?這是明顯的無知嗎?從事軟件工程的人並不愚蠢。通過使用像“設計模式”這樣花哨的面向對象的術語如抽象”、“封裝”、“多態性”和“接口隔離”,我們是否更擔心在我們的同行面前“看起來聰明”?可能不會。

繼續使用已經使用了幾十年的東西真的很容易。大多數人從未真正嘗試過函數式編程。那些使用過的人再也不會回去寫面向對象程序代碼了。

亨利·福特曾經說過一句名言——“如果我問人們想要什麼,他們會說跑得快的駿馬”。在軟件世界裡,大多數人可能想要一種“更好的面嚮對象語言”。人們可以很容易地描述他們遇到的一個問題(使代碼庫有條理,不那麼複雜),但不是最好的解決方案。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

面向對象編程的災難:是時候考慮更新換代了


Band-aids

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Pixabay


當某些東西不起作用時,我們該怎麼辦?很簡單,只有兩個選擇——扔掉或者試著修理。面向對象則難以去除,數百萬開發人員接受了面向對象的培訓。全世界數百萬的組織都在使用面向對象程序。

你可能已經看到面向對象編程並不能真正起作用,它使我們的代碼變得複雜和不可靠。然並非只有你一人面臨這樣的問題!幾十年來,人們一直在努力解決面向對象程序代碼中普遍存在的問題。他們想出了無數的設計模式。

設計模式

面向對象程序編程提供了一套原則,理論上應該可支持開發人員逐步構建越來越大的系統:SOLID原則、依賴注入、設計模式等等。

不幸的是,設計模式只不過是創可貼。它們的存在僅僅是為了解決面向對象程序設計的缺點。關於這個主題,已經寫了無數的書。如果不是給代碼庫帶來了巨大的複雜性,面向對象編程也就不會那麼糟糕。

問題工廠

事實上,編寫好的、可維護的面向對象代碼是不可能的。

另一方面,一個不一致的面向對象編程庫,似乎不符合任何標準。另一方面,有一個過度設計的代碼塔,一堆錯誤的抽象一個接一個地建立起來。設計模式對於構建這樣的抽象塔非常有幫助。

很快,增加新的功能,甚至理解所有的複雜性,變得越來越難。代碼庫將充滿像SimpleBeanFactoryAwareAspectInstanceFactory, AbstractInterceptorDrivenBeanDefinitionDecorator, TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactory.

試圖理解開發人員自己創造的抽象之塔,必須浪費寶貴的腦力。沒有結構在很多情況下比結構不好要好。

面向對象編程的災難:是時候考慮更新換代了

圖片來源: https://www.reddit.com/r/ProgrammerHumor/com


面向對象編程的災難:是時候考慮更新換代了


四大面向對象編程支柱的衰落


面向對象編程的四大支柱是:抽象、繼承、封裝和多態。

讓我們逐一來看看四大支柱到底是怎樣的。

繼承

可重用性的缺乏來自面向對象編程的語言,而不是函數語言。因為面向對象編程語言的問題是,其擁有所有這些隱含的環境。你想要一根香蕉,但你得到的是一直拿著香蕉的大猩猩和整個叢林。Joe Armstrong, Erlang的創造者


面向對象編程繼承與現實世界無關。事實上,繼承是實現代碼可重用性的低劣方式。“四人幫”已經明確提出,比起繼承,更喜歡組合。一些現代編程語言完全避免繼承。

繼承有幾個問題:

1. 引入許多類不需要的代碼(香蕉和叢林問題)。

2. 將類的一部分定義在其他地方會使代碼很難推理,尤其是在有多個繼承級別的情況下。

3. 在多數編程語言中,多重繼承幾乎是不可能的。這主要使得繼承作為代碼共享機制毫無用處。

多態

多態很棒,它允許我們在運行時改變編程行為。然而,這是計算機編程中非常基本的概念。面向對象編程多態性完成了這項工作,但再次導致精神雜耍行為。這會使得代碼庫變得非常複雜,並且關於被調用的具體方法的推理變得非常困難。

另一方面,函數編程使得我們以更簡潔的方式實現相同的多態性……只需傳遞一個定義所需運行時行為的函數。還有什麼比這更簡單的呢?不需要在多個文件(和接口)中定義一堆重載的抽象虛擬方法。

封裝

正如前面討論的,封裝是面向對象編程設計的特洛伊木馬。它實際上是一個美化的全局可變狀態,並使不安全的代碼看起來是安全的。不安全的編碼實踐是面向對象編程人員在日常工作中依賴的支柱……

抽象

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

在過程/功能語言中,我們可以簡單地在相鄰文件中“隱藏”實現細節。沒有必要稱這一基本行為為“抽象”。

面向對象編程的災難:是時候考慮更新換代了


為什麼面向對象編程控制了這個行業?

答案很簡單,爬蟲類的外星種族與國家安全局(和俄羅斯人)合謀將美國程序員折磨致死……

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Gaetano Cessati


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

自從微軟拒絕服務以來,Java是計算領域發生的最令人苦惱的事情。Alan Kay,面向對象編程的發明者

Java很簡單

當JAVA在1995年首次被引入時,與其他語言相比,Java是一種非常簡單的編程語言。當時,編寫桌面應用程序的門檻很高。開發桌面應用程序需要用C語言編寫低級win32應用程序接口,開發人員還必須關心手動內存管理。另一種選擇是Visual Basic,但許多人可能不想把自己鎖在微軟的生態系統中。

當Java被引入時,對許多開發人員來說,它是一個簡單的工具,因為它是免費的,可以在所有平臺上使用。像內置垃圾收集、友好命名的APIs(與神祕的win32 APIs相比)、合適的名稱空間和熟悉的類C語法這樣的東西使Java更容易使用。

圖形用戶界面編程也變得越來越流行,似乎各種用戶界面組件都很好地映射到類上。IDEs中的方法自動完成也使人們聲稱面向對象編程接口更容易使用。

如果不是把面向對象編程強加給開發人員,也許Java不會那麼糟糕。Java的所有功能都很好。其中垃圾收集、可移植性、異常處理特性,是其他主流編程語言所缺乏的,但在1995年非常棒

然後C#出現了

最初,微軟一直嚴重依賴Java。當事情開始出錯時(在與太陽微系統公司就Java許可進行了長時間的法律鬥爭之後),微軟決定投資自己的Java版本。這就是C# 1.0誕生的時候。C#作為一種語言,一直被認為是“更好的Java”。然而,也存在個巨大的問題——隱藏在稍微改進的語法下,它同樣也屬於面嚮對象語言,有著同樣的缺陷。

微軟一直在大力投資NET生態系統,其中還包括良好的開發工具。多年來,視覺工作室可能是最好的集成開發環境之一。這反過來又導致了NET框架的廣泛使用,尤其是在企業中。

最近,微軟一直在大力投資瀏覽器生態系統,推出它的TypeScript。TypeScript很棒,因為它可以編譯成JavaScript並添加靜態類型檢查等內容。不足之處在於沒有對函數構造的適當支持——沒有內置的不可變數據結構,沒有函數組合,沒有適當的模式匹配。 TypeScript是面向對象編程的第一步,對於瀏覽器來說主要是C#。Anders Hejlsberg 甚至負責C#和打字稿的設計。

功能語言

另一方面,函數式語言從未得到像微軟這樣大公司的支持。鑑於投資不多,F#不算數。功能語言的發展主要是由社區驅動的。這可能解釋了面向對象程序設計語言和浮點語言之間流行程度的差異。

面向對象編程的災難:是時候考慮更新換代了


是時候繼續前進了?


我們現在知道面向對象是一個失敗的實驗。是向前看的時候了。現在是我們作為一個社區承認這個想法讓我們失望的時候了,我們必須放棄它。Lawrence Krubner
面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash SpaceX



為什麼我們堅持使用從根本上說是次優的程序組織方式?這是明顯的無知嗎?從事軟件工程的人並不愚蠢。通過使用像“設計模式”這樣花哨的面向對象的術語如抽象”、“封裝”、“多態性”和“接口隔離”,我們是否更擔心在我們的同行面前“看起來聰明”?可能不會。

繼續使用已經使用了幾十年的東西真的很容易。大多數人從未真正嘗試過函數式編程。那些使用過的人再也不會回去寫面向對象程序代碼了。

亨利·福特曾經說過一句名言——“如果我問人們想要什麼,他們會說跑得快的駿馬”。在軟件世界裡,大多數人可能想要一種“更好的面嚮對象語言”。人們可以很容易地描述他們遇到的一個問題(使代碼庫有條理,不那麼複雜),但不是最好的解決方案。

面向對象編程的災難:是時候考慮更新換代了


還有哪些替代方案?


如果像functors和monads這樣的術語讓你有點不安,那麼你並不孤單!如果函數式編程為某些概念提供更直觀的名稱,那麼它們就不會那麼可怕。函子? 這只是我們可以用函數轉換的東西,想想list.map? 簡單的計算可以鏈接!

嘗試函數式編程會讓你成為更好的開發者。你將最終有時間編寫解決現實世界問題的真實代碼,而不是花大部分時間思考抽象和設計模式。

你可能沒有意識到這一點,但是你已經是一個功能性的程序員了。你在日常工作中使用功能嗎?是的?那你已經是一個功能性程序員了!你只需要學會如何充分利用這些功能。

兩種學習曲線非常平緩的偉大函數語言是埃利希蘭德榆樹。它們讓開發人員專注於最重要的事情——編寫可靠的軟件,同時消除更多傳統功能語言的複雜性。

還有其他選擇嗎?試試F#吧——它是一種令人驚歎的功能語言,並提供了與現有語言的良好互操作性。NET代碼。使用Java?那麼使用Scala或Clojure都是很好的選擇。使用JavaScript?有了正確的指導和限制,JavaScript可以成為一種很好的功能性語言。

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

面向對象編程的災難:是時候考慮更新換代了


Band-aids

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Pixabay


當某些東西不起作用時,我們該怎麼辦?很簡單,只有兩個選擇——扔掉或者試著修理。面向對象則難以去除,數百萬開發人員接受了面向對象的培訓。全世界數百萬的組織都在使用面向對象程序。

你可能已經看到面向對象編程並不能真正起作用,它使我們的代碼變得複雜和不可靠。然並非只有你一人面臨這樣的問題!幾十年來,人們一直在努力解決面向對象程序代碼中普遍存在的問題。他們想出了無數的設計模式。

設計模式

面向對象程序編程提供了一套原則,理論上應該可支持開發人員逐步構建越來越大的系統:SOLID原則、依賴注入、設計模式等等。

不幸的是,設計模式只不過是創可貼。它們的存在僅僅是為了解決面向對象程序設計的缺點。關於這個主題,已經寫了無數的書。如果不是給代碼庫帶來了巨大的複雜性,面向對象編程也就不會那麼糟糕。

問題工廠

事實上,編寫好的、可維護的面向對象代碼是不可能的。

另一方面,一個不一致的面向對象編程庫,似乎不符合任何標準。另一方面,有一個過度設計的代碼塔,一堆錯誤的抽象一個接一個地建立起來。設計模式對於構建這樣的抽象塔非常有幫助。

很快,增加新的功能,甚至理解所有的複雜性,變得越來越難。代碼庫將充滿像SimpleBeanFactoryAwareAspectInstanceFactory, AbstractInterceptorDrivenBeanDefinitionDecorator, TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactory.

試圖理解開發人員自己創造的抽象之塔,必須浪費寶貴的腦力。沒有結構在很多情況下比結構不好要好。

面向對象編程的災難:是時候考慮更新換代了

圖片來源: https://www.reddit.com/r/ProgrammerHumor/com


面向對象編程的災難:是時候考慮更新換代了


四大面向對象編程支柱的衰落


面向對象編程的四大支柱是:抽象、繼承、封裝和多態。

讓我們逐一來看看四大支柱到底是怎樣的。

繼承

可重用性的缺乏來自面向對象編程的語言,而不是函數語言。因為面向對象編程語言的問題是,其擁有所有這些隱含的環境。你想要一根香蕉,但你得到的是一直拿著香蕉的大猩猩和整個叢林。Joe Armstrong, Erlang的創造者


面向對象編程繼承與現實世界無關。事實上,繼承是實現代碼可重用性的低劣方式。“四人幫”已經明確提出,比起繼承,更喜歡組合。一些現代編程語言完全避免繼承。

繼承有幾個問題:

1. 引入許多類不需要的代碼(香蕉和叢林問題)。

2. 將類的一部分定義在其他地方會使代碼很難推理,尤其是在有多個繼承級別的情況下。

3. 在多數編程語言中,多重繼承幾乎是不可能的。這主要使得繼承作為代碼共享機制毫無用處。

多態

多態很棒,它允許我們在運行時改變編程行為。然而,這是計算機編程中非常基本的概念。面向對象編程多態性完成了這項工作,但再次導致精神雜耍行為。這會使得代碼庫變得非常複雜,並且關於被調用的具體方法的推理變得非常困難。

另一方面,函數編程使得我們以更簡潔的方式實現相同的多態性……只需傳遞一個定義所需運行時行為的函數。還有什麼比這更簡單的呢?不需要在多個文件(和接口)中定義一堆重載的抽象虛擬方法。

封裝

正如前面討論的,封裝是面向對象編程設計的特洛伊木馬。它實際上是一個美化的全局可變狀態,並使不安全的代碼看起來是安全的。不安全的編碼實踐是面向對象編程人員在日常工作中依賴的支柱……

抽象

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

在過程/功能語言中,我們可以簡單地在相鄰文件中“隱藏”實現細節。沒有必要稱這一基本行為為“抽象”。

面向對象編程的災難:是時候考慮更新換代了


為什麼面向對象編程控制了這個行業?

答案很簡單,爬蟲類的外星種族與國家安全局(和俄羅斯人)合謀將美國程序員折磨致死……

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Gaetano Cessati


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

自從微軟拒絕服務以來,Java是計算領域發生的最令人苦惱的事情。Alan Kay,面向對象編程的發明者

Java很簡單

當JAVA在1995年首次被引入時,與其他語言相比,Java是一種非常簡單的編程語言。當時,編寫桌面應用程序的門檻很高。開發桌面應用程序需要用C語言編寫低級win32應用程序接口,開發人員還必須關心手動內存管理。另一種選擇是Visual Basic,但許多人可能不想把自己鎖在微軟的生態系統中。

當Java被引入時,對許多開發人員來說,它是一個簡單的工具,因為它是免費的,可以在所有平臺上使用。像內置垃圾收集、友好命名的APIs(與神祕的win32 APIs相比)、合適的名稱空間和熟悉的類C語法這樣的東西使Java更容易使用。

圖形用戶界面編程也變得越來越流行,似乎各種用戶界面組件都很好地映射到類上。IDEs中的方法自動完成也使人們聲稱面向對象編程接口更容易使用。

如果不是把面向對象編程強加給開發人員,也許Java不會那麼糟糕。Java的所有功能都很好。其中垃圾收集、可移植性、異常處理特性,是其他主流編程語言所缺乏的,但在1995年非常棒

然後C#出現了

最初,微軟一直嚴重依賴Java。當事情開始出錯時(在與太陽微系統公司就Java許可進行了長時間的法律鬥爭之後),微軟決定投資自己的Java版本。這就是C# 1.0誕生的時候。C#作為一種語言,一直被認為是“更好的Java”。然而,也存在個巨大的問題——隱藏在稍微改進的語法下,它同樣也屬於面嚮對象語言,有著同樣的缺陷。

微軟一直在大力投資NET生態系統,其中還包括良好的開發工具。多年來,視覺工作室可能是最好的集成開發環境之一。這反過來又導致了NET框架的廣泛使用,尤其是在企業中。

最近,微軟一直在大力投資瀏覽器生態系統,推出它的TypeScript。TypeScript很棒,因為它可以編譯成JavaScript並添加靜態類型檢查等內容。不足之處在於沒有對函數構造的適當支持——沒有內置的不可變數據結構,沒有函數組合,沒有適當的模式匹配。 TypeScript是面向對象編程的第一步,對於瀏覽器來說主要是C#。Anders Hejlsberg 甚至負責C#和打字稿的設計。

功能語言

另一方面,函數式語言從未得到像微軟這樣大公司的支持。鑑於投資不多,F#不算數。功能語言的發展主要是由社區驅動的。這可能解釋了面向對象程序設計語言和浮點語言之間流行程度的差異。

面向對象編程的災難:是時候考慮更新換代了


是時候繼續前進了?


我們現在知道面向對象是一個失敗的實驗。是向前看的時候了。現在是我們作為一個社區承認這個想法讓我們失望的時候了,我們必須放棄它。Lawrence Krubner
面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash SpaceX



為什麼我們堅持使用從根本上說是次優的程序組織方式?這是明顯的無知嗎?從事軟件工程的人並不愚蠢。通過使用像“設計模式”這樣花哨的面向對象的術語如抽象”、“封裝”、“多態性”和“接口隔離”,我們是否更擔心在我們的同行面前“看起來聰明”?可能不會。

繼續使用已經使用了幾十年的東西真的很容易。大多數人從未真正嘗試過函數式編程。那些使用過的人再也不會回去寫面向對象程序代碼了。

亨利·福特曾經說過一句名言——“如果我問人們想要什麼,他們會說跑得快的駿馬”。在軟件世界裡,大多數人可能想要一種“更好的面嚮對象語言”。人們可以很容易地描述他們遇到的一個問題(使代碼庫有條理,不那麼複雜),但不是最好的解決方案。

面向對象編程的災難:是時候考慮更新換代了


還有哪些替代方案?


如果像functors和monads這樣的術語讓你有點不安,那麼你並不孤單!如果函數式編程為某些概念提供更直觀的名稱,那麼它們就不會那麼可怕。函子? 這只是我們可以用函數轉換的東西,想想list.map? 簡單的計算可以鏈接!

嘗試函數式編程會讓你成為更好的開發者。你將最終有時間編寫解決現實世界問題的真實代碼,而不是花大部分時間思考抽象和設計模式。

你可能沒有意識到這一點,但是你已經是一個功能性的程序員了。你在日常工作中使用功能嗎?是的?那你已經是一個功能性程序員了!你只需要學會如何充分利用這些功能。

兩種學習曲線非常平緩的偉大函數語言是埃利希蘭德榆樹。它們讓開發人員專注於最重要的事情——編寫可靠的軟件,同時消除更多傳統功能語言的複雜性。

還有其他選擇嗎?試試F#吧——它是一種令人驚歎的功能語言,並提供了與現有語言的良好互操作性。NET代碼。使用Java?那麼使用Scala或Clojure都是很好的選擇。使用JavaScript?有了正確的指導和限制,JavaScript可以成為一種很好的功能性語言。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程的捍衛者

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

面向對象編程的災難:是時候考慮更新換代了


Band-aids

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Pixabay


當某些東西不起作用時,我們該怎麼辦?很簡單,只有兩個選擇——扔掉或者試著修理。面向對象則難以去除,數百萬開發人員接受了面向對象的培訓。全世界數百萬的組織都在使用面向對象程序。

你可能已經看到面向對象編程並不能真正起作用,它使我們的代碼變得複雜和不可靠。然並非只有你一人面臨這樣的問題!幾十年來,人們一直在努力解決面向對象程序代碼中普遍存在的問題。他們想出了無數的設計模式。

設計模式

面向對象程序編程提供了一套原則,理論上應該可支持開發人員逐步構建越來越大的系統:SOLID原則、依賴注入、設計模式等等。

不幸的是,設計模式只不過是創可貼。它們的存在僅僅是為了解決面向對象程序設計的缺點。關於這個主題,已經寫了無數的書。如果不是給代碼庫帶來了巨大的複雜性,面向對象編程也就不會那麼糟糕。

問題工廠

事實上,編寫好的、可維護的面向對象代碼是不可能的。

另一方面,一個不一致的面向對象編程庫,似乎不符合任何標準。另一方面,有一個過度設計的代碼塔,一堆錯誤的抽象一個接一個地建立起來。設計模式對於構建這樣的抽象塔非常有幫助。

很快,增加新的功能,甚至理解所有的複雜性,變得越來越難。代碼庫將充滿像SimpleBeanFactoryAwareAspectInstanceFactory, AbstractInterceptorDrivenBeanDefinitionDecorator, TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactory.

試圖理解開發人員自己創造的抽象之塔,必須浪費寶貴的腦力。沒有結構在很多情況下比結構不好要好。

面向對象編程的災難:是時候考慮更新換代了

圖片來源: https://www.reddit.com/r/ProgrammerHumor/com


面向對象編程的災難:是時候考慮更新換代了


四大面向對象編程支柱的衰落


面向對象編程的四大支柱是:抽象、繼承、封裝和多態。

讓我們逐一來看看四大支柱到底是怎樣的。

繼承

可重用性的缺乏來自面向對象編程的語言,而不是函數語言。因為面向對象編程語言的問題是,其擁有所有這些隱含的環境。你想要一根香蕉,但你得到的是一直拿著香蕉的大猩猩和整個叢林。Joe Armstrong, Erlang的創造者


面向對象編程繼承與現實世界無關。事實上,繼承是實現代碼可重用性的低劣方式。“四人幫”已經明確提出,比起繼承,更喜歡組合。一些現代編程語言完全避免繼承。

繼承有幾個問題:

1. 引入許多類不需要的代碼(香蕉和叢林問題)。

2. 將類的一部分定義在其他地方會使代碼很難推理,尤其是在有多個繼承級別的情況下。

3. 在多數編程語言中,多重繼承幾乎是不可能的。這主要使得繼承作為代碼共享機制毫無用處。

多態

多態很棒,它允許我們在運行時改變編程行為。然而,這是計算機編程中非常基本的概念。面向對象編程多態性完成了這項工作,但再次導致精神雜耍行為。這會使得代碼庫變得非常複雜,並且關於被調用的具體方法的推理變得非常困難。

另一方面,函數編程使得我們以更簡潔的方式實現相同的多態性……只需傳遞一個定義所需運行時行為的函數。還有什麼比這更簡單的呢?不需要在多個文件(和接口)中定義一堆重載的抽象虛擬方法。

封裝

正如前面討論的,封裝是面向對象編程設計的特洛伊木馬。它實際上是一個美化的全局可變狀態,並使不安全的代碼看起來是安全的。不安全的編碼實踐是面向對象編程人員在日常工作中依賴的支柱……

抽象

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

在過程/功能語言中,我們可以簡單地在相鄰文件中“隱藏”實現細節。沒有必要稱這一基本行為為“抽象”。

面向對象編程的災難:是時候考慮更新換代了


為什麼面向對象編程控制了這個行業?

答案很簡單,爬蟲類的外星種族與國家安全局(和俄羅斯人)合謀將美國程序員折磨致死……

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Gaetano Cessati


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

自從微軟拒絕服務以來,Java是計算領域發生的最令人苦惱的事情。Alan Kay,面向對象編程的發明者

Java很簡單

當JAVA在1995年首次被引入時,與其他語言相比,Java是一種非常簡單的編程語言。當時,編寫桌面應用程序的門檻很高。開發桌面應用程序需要用C語言編寫低級win32應用程序接口,開發人員還必須關心手動內存管理。另一種選擇是Visual Basic,但許多人可能不想把自己鎖在微軟的生態系統中。

當Java被引入時,對許多開發人員來說,它是一個簡單的工具,因為它是免費的,可以在所有平臺上使用。像內置垃圾收集、友好命名的APIs(與神祕的win32 APIs相比)、合適的名稱空間和熟悉的類C語法這樣的東西使Java更容易使用。

圖形用戶界面編程也變得越來越流行,似乎各種用戶界面組件都很好地映射到類上。IDEs中的方法自動完成也使人們聲稱面向對象編程接口更容易使用。

如果不是把面向對象編程強加給開發人員,也許Java不會那麼糟糕。Java的所有功能都很好。其中垃圾收集、可移植性、異常處理特性,是其他主流編程語言所缺乏的,但在1995年非常棒

然後C#出現了

最初,微軟一直嚴重依賴Java。當事情開始出錯時(在與太陽微系統公司就Java許可進行了長時間的法律鬥爭之後),微軟決定投資自己的Java版本。這就是C# 1.0誕生的時候。C#作為一種語言,一直被認為是“更好的Java”。然而,也存在個巨大的問題——隱藏在稍微改進的語法下,它同樣也屬於面嚮對象語言,有著同樣的缺陷。

微軟一直在大力投資NET生態系統,其中還包括良好的開發工具。多年來,視覺工作室可能是最好的集成開發環境之一。這反過來又導致了NET框架的廣泛使用,尤其是在企業中。

最近,微軟一直在大力投資瀏覽器生態系統,推出它的TypeScript。TypeScript很棒,因為它可以編譯成JavaScript並添加靜態類型檢查等內容。不足之處在於沒有對函數構造的適當支持——沒有內置的不可變數據結構,沒有函數組合,沒有適當的模式匹配。 TypeScript是面向對象編程的第一步,對於瀏覽器來說主要是C#。Anders Hejlsberg 甚至負責C#和打字稿的設計。

功能語言

另一方面,函數式語言從未得到像微軟這樣大公司的支持。鑑於投資不多,F#不算數。功能語言的發展主要是由社區驅動的。這可能解釋了面向對象程序設計語言和浮點語言之間流行程度的差異。

面向對象編程的災難:是時候考慮更新換代了


是時候繼續前進了?


我們現在知道面向對象是一個失敗的實驗。是向前看的時候了。現在是我們作為一個社區承認這個想法讓我們失望的時候了,我們必須放棄它。Lawrence Krubner
面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash SpaceX



為什麼我們堅持使用從根本上說是次優的程序組織方式?這是明顯的無知嗎?從事軟件工程的人並不愚蠢。通過使用像“設計模式”這樣花哨的面向對象的術語如抽象”、“封裝”、“多態性”和“接口隔離”,我們是否更擔心在我們的同行面前“看起來聰明”?可能不會。

繼續使用已經使用了幾十年的東西真的很容易。大多數人從未真正嘗試過函數式編程。那些使用過的人再也不會回去寫面向對象程序代碼了。

亨利·福特曾經說過一句名言——“如果我問人們想要什麼,他們會說跑得快的駿馬”。在軟件世界裡,大多數人可能想要一種“更好的面嚮對象語言”。人們可以很容易地描述他們遇到的一個問題(使代碼庫有條理,不那麼複雜),但不是最好的解決方案。

面向對象編程的災難:是時候考慮更新換代了


還有哪些替代方案?


如果像functors和monads這樣的術語讓你有點不安,那麼你並不孤單!如果函數式編程為某些概念提供更直觀的名稱,那麼它們就不會那麼可怕。函子? 這只是我們可以用函數轉換的東西,想想list.map? 簡單的計算可以鏈接!

嘗試函數式編程會讓你成為更好的開發者。你將最終有時間編寫解決現實世界問題的真實代碼,而不是花大部分時間思考抽象和設計模式。

你可能沒有意識到這一點,但是你已經是一個功能性的程序員了。你在日常工作中使用功能嗎?是的?那你已經是一個功能性程序員了!你只需要學會如何充分利用這些功能。

兩種學習曲線非常平緩的偉大函數語言是埃利希蘭德榆樹。它們讓開發人員專注於最重要的事情——編寫可靠的軟件,同時消除更多傳統功能語言的複雜性。

還有其他選擇嗎?試試F#吧——它是一種令人驚歎的功能語言,並提供了與現有語言的良好互操作性。NET代碼。使用Java?那麼使用Scala或Clojure都是很好的選擇。使用JavaScript?有了正確的指導和限制,JavaScript可以成為一種很好的功能性語言。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程的捍衛者

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ott Maidre


期待面向對象編程的捍衛者們做出某種反應。他們會說這篇文章不準確。有些人甚至可能開始罵人。

他們有權發表自己的意見。然而,他們為面向對象編程設計辯護的論點通常相當薄弱。諷刺的是,他們中的大多數人可能從未真正用真正的函數語言編程。如果從未真正嘗試過兩件事,又怎會在兩件事之間進行比較呢?這樣的比較是無用的。

德米特里定律不是很有用——它無助於解決非決定論的問題,不管你如何訪問或改變那個狀態,共享的可變狀態仍然是共享的可變狀態。a.total()並不比a.getB().getC().total()好多少。只是掩蓋了問題。

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

只是工具箱裡的一個工具……

人們經常說面向對象只是工具箱中的另一個工具。是的,它是工具箱中的一個工具,就像馬和汽車都是交通工具一樣……畢竟,它們都服務於相同的目的,對嗎?當我們可以繼續騎好馬的時候,為什麼還要用汽車呢?

歷史會重演

1900年,紐約路上形式的車輛寥寥無幾,人們一直用馬來運輸。1917年,公路上不再有馬了。圍繞馬匹運輸為中心產生了一個巨大的產業。整個企業都是圍繞著糞肥清理這樣的事情創建的。

人們抵制變革。他們稱汽車是最終會過去的另一種“時尚”。畢竟,馬已經存在幾個世紀了!有些人甚至要求政府幹預。

這有什麼關係?軟件業以面向對象編程為中心。數百萬人接受了面向對象編程的培訓,數百萬公司在其代碼中使用了面向對象編程。當然,他們會試圖詆譭任何威脅他們生計的東西!這是常識。

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

"
全文共13316字,預計學習時長26分鐘


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Jungwoo Hong


許多人認為面向對象編程是計算機科學的珍寶,代碼組織的最終解決方案,所有問題的終極回答,編寫程序的唯一真正方法,編程之神賜予我們的財富……

但事實證明並非如此,人們常常屈服於抽象和混雜共享可變對象的複雜圖形的重壓。耗費寶貴的時間和腦力來思考“抽象”和“設計模式”,而不關注解決現實問題。

許多人批評面向對象編程,其中包括非常傑出的軟件工程師。有意思的是,就連面向對象變成的發明者自己也是現代面向對象編程的著名批評者!

每個軟件開發人員都應該以編寫可靠的代碼為終極目標。如果代碼有問題且不可靠,那其他方面的優點都不足為提。編寫可靠代碼的最佳方式是什麼?簡潔。簡潔與複雜相反。因此,作為軟件開發人員,我們首要的責任應該是降低代碼複雜性。

面向對象編程的災難:是時候考慮更新換代了


包絡高級技術人員在內的很多人認為面向對象編程是代碼組織的標準,這是不正確的。同樣不可接受的是,除面對對象編程之外,許多主流語言都沒有提供任何代碼組織的替代方案。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程可以作為正確程序的替代品。Edsger W. Dijkstra,計算機科學的先驅


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Sebastian Herrmann



面向對象編程的目的只有一個——管理過程代碼庫的複雜性。換句話說,它應該改進代碼組織。沒有客觀和公開的證據表明面向對象編程優於普通的過程性編程。

殘酷的事實是,面向對象編程在它唯一能處理的任務上失敗了。它在紙上看起來很好——有清晰的動物、狗、人類等層級。然而,一旦應用程序的複雜性開始增加,它就會停滯不前。不但沒有降低複雜性,反而鼓勵混雜地共享可變狀態,並通過其眾多的設計模式引入了額外的複雜性。面向對象程序增加了常見的開發實踐(如重構和測試)的難度。

有些人可能不同意這一觀點,但事實是現代Java/ c# OOP從來沒有正確地設計過,從未出自一個合適的研究機構(與Haskell/FP不同)。λ微積分為函數式編程提供了完整的理論基礎,而面向對象編程做則沒有與之匹配的理論。

短期來看使用面向對象編程似乎是無害的,尤其是在綠地投資上。但是長期使用面向對象編程又會導致哪些後果呢?面向對象編程是一個定時炸彈,當代碼基足夠大時,將在將來某個時候爆炸。

項目被推遲、錯過最後期限、開發人員精力被耗盡,添加新功能幾乎是不可能的。組織將代碼庫標記為“遺留代碼庫”,開發團隊計劃重寫代碼庫。

面向對象編程與人腦思維相悖,人類的思維過程集中在“做”事情上——散步,和朋友聊天,吃比薩餅。我們的大腦進化成了做事的方式,而不是把世界組織成抽象對象的複雜層次。

面向對象編程代碼充滿了不確定性——與函數式編程不同,我們不能保證在給定相同輸入的情況下得到相同的輸出。這使得編程推理非常困難。舉一個極其簡單的例子——輸出2+2或執行calculator.Add(2, 2) 的結果大部分等於4,但有時可能也等於3 、5,甚至是1004。Calculator對象的依賴關係可能以微妙但顯著的方式改變計算結果。

面向對象編程的災難:是時候考慮更新換代了


對彈性框架的需求


不管編程範式是怎麼樣的,優秀的程序員會寫出好的代碼,糟糕的程序員會寫出壞的代碼,然而,編程範式應該限制糟糕的程序員造成太大的破壞。糟糕的程序員從來沒有時間學習,他們只是瘋狂地在鍵盤上亂按。不管是否願意,你都會遇到糟糕的程序員,他們中的一些人會非常非常糟糕。不幸的是,面對對象編程沒有足夠的約束力來防止糟糕的程序員造成太多的破壞。

如果沒有一個強有力的框架來支撐工作的話,程序員很難寫出好的代碼。有些框架關注一些非常特殊的問題(例如Angular或ASP.Net)。

框架的抽象定義是:“一個基本的支持結構”——框架關注更抽象的東西,比如代碼組織和處理代碼複雜性。儘管面向對象編程和函數編程都是編程範例,但它們也是高級框架。

限制我們的選擇

c++是一種可怕的[面向對象]語言……將你的項目限制為C意味著人們不會用任何愚蠢的“對象模型”來把事情搞砸。Linus Torvalds, Linux創始人

Linus Torvalds以他對c++和麵向對象編程的公開批評而聞名。有一件事他是百分之百正確的,那就是限制程序員的選擇。事實上,程序員的選擇越少,他們的代碼就越有彈性。如上面引用的句子所說,Linus Torvalds強烈建議使用一個好的框架來構建代碼。


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/specphotops


許多人不喜歡道路限速,但限速對於防止車禍致死是必不可少的。同樣地,好的編程框架應該提供防止人們做傻事的機制。

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

1. 模塊化和可重用性

2. 適當的隔離狀態

3. 高信噪比

不幸的是,面向對象編程給開發人員提供了太多的工具和選擇,卻沒有施加合適的限制。儘管面向對象編程承諾會解決模塊化和提高可重用性,但卻未能兌現其承諾(稍後將對此進行更多闡述)。面向對象編程代碼鼓勵使用共享的可變狀態,這一點多次證明是不安全的。面向對象編程通常需要大量樣板代碼(低信噪比)。

函數編程

什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於“現實世界”。這與事實相去甚遠!

函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流編程語言的弱點而提出的。函數是函數編程的核心抽象。如果使用得當,函數提供了在面向對象程序設計中前所未有的代碼模塊化和可以重複使用性。它甚至以解決可空性問題的設計模式為特色,並提供了一種出色的錯誤處理方式。

函數式編程做得很好的一點是幫助我們編寫可靠的軟件。對調試器的需求幾乎完全消失了——是的,不需要遍歷代碼並查看變量。

面向對象編程的災難:是時候考慮更新換代了


我們所做的面對對象編程都錯了


我對之前為這個主題創造了“對象”這個術語感到抱歉,因為它使許多人把注意力集中在較次要的概念上。事實上,最重要的是傳遞信息。Alan Kay, 面向對象編程發明者


通常,人們認為Erlang不是一種面向對象的語言。但是Erlang可能是唯一主流的面嚮對象語言。是的, Smalltalk當然是合適的面嚮對象語言——但是,並沒有得到廣泛的應用。Smalltalk和Erlang都按照面向對象編程的發明者Alan Kay最初設計的方式使用面向對象編程。

信息

AlanKay在20世紀60年代創造了“面向對象編程”這個詞,他有生物學背景,並試圖使計算機程序像活細胞一樣進行通信。

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash/Muukii

Alan Kay的想法是讓獨立的程序(細胞)通過相互發送消息進行通信。獨立程序的狀態永遠不會與外部世界共享(封裝)。

面向對象編程從來沒有打算擁有諸如繼承、多態性、“new”關鍵字和無數設計模式之類的東西。

最純粹的面向對象編程設計

Erlang是面向對象編程最純粹的形式。與更主流的語言不同,它關注面向對象的核心思想——消息傳遞。在Erlang中,對象通過在對象之間傳遞不可變的消息進行通信。

有證據證明不可變消息比方法調用更好嗎?

沒有!Erlang可能是世界上最可靠的語言。它為世界上大多數電信(以及互聯網)基礎設施提供動力。用Erlang編寫的一些系統的可靠性為99.9999999%。

面向對象編程的災難:是時候考慮更新換代了


代碼複雜性


隨著面向對象編程語言的改變,計算機軟件變得更冗長、可讀性更差、描述性更差、更難以修改和維護。Richard Mansfield

軟件開發最重要的方面是降低代碼的複雜性。如果代碼庫無法維護,那麼這些花哨的特性都無關緊要;如果代碼庫變得過於複雜和難以維持,那麼即使是100%的測試覆蓋率也沒有任何價值。

是什麼使代碼基變得複雜?回答這個問題需要考慮的因素有很多,但最主要的問題是——共享的可變狀態、錯誤的抽象和低信噪比(通常由樣板代碼引起)。它們在面向對象編程中都很常見。

面向對象編程的災難:是時候考慮更新換代了


狀態問題

面向對象編程的災難:是時候考慮更新換代了

圖片來源:UnsplashMika Baumeister



什麼是狀態?簡單地說,狀態是存儲在內存中的所有臨時數據。想想面向對象編程中的變量或字段/屬性。命令式編程(包括面向對象編程)根據程序狀態和對該狀態的改變來描述計算。聲明式(函數式)編程描述的是期望的結果,而不是明確指定對狀態的改變。

可變狀態——精神雜耍行為

大型面向對象編程在構建這個可變對象的大型對象圖時,會變得越來越複雜。嘗試理解記住——當你調用一個方法時會發生什麼以及副作用是什麼。Rich Hickey, Clojur創造者


面向對象編程的災難:是時候考慮更新換代了

圖片來源:https://www.flickr.com/photos/48137825@N05/87



狀態本身是無害的。然而可變狀態卻不是,尤其當它是共享的時候。可變狀態到底是什麼——任何會發生改變的狀態。例如面向對象編程中的變量或字段。

舉個現實世界的例子:

假設你有一張空白的紙,在上面寫了一個便條,最後得到了一張不同狀態的紙(文本)。實際上,你改變了那張紙的狀態。

這在現實世界中完全沒問題,因為沒人會在意那張紙。除非這張紙是蒙娜麗莎的原畫。

· 人腦的侷限性

為什麼可變狀態是一個大問題?人腦是宇宙中已知的最強大的機器。然而,我們的大腦在處理狀態方面確實很糟糕,因為我們在工作記憶中一次只能保存大約5個項目。如果只考慮代碼的功能,而不考慮它在代碼庫周圍所改變的變量,那麼對一段代碼進行推理就容易得多。

可變狀態的編程是一種精神雜耍。我不知道你會怎麼樣做,但我可能會玩兩個球。給我三個或三個以上的球,我一定都接不住。為什麼我們要在工作中每天都做這種精神上的雜耍?

不幸的是,可變狀態的精神雜耍是面向對象編程的核心。對象上存在方法的唯一目的是改變同一對象。

分散狀態

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


面向對象編程通過將狀態分散到整個程序中,使得代碼組織問題更加嚴重。然後分散狀態在不同的對象之間雜亂地共享。

舉個現實世界的例子:

暫時忘記我們都是成年人,假裝我們在組裝一輛很酷的樂高卡車。

然而,有一個陷阱——所有的卡車零件都隨機與其他樂高玩具的零件混合。他們被隨機地放進50個不同的盒子裡。而且你不能把卡車零件組合在一起——你必須記住不同的卡車零件在哪裡,並且只能一個一個地拿出來。

是的,最終也能完成組裝。但要花多長時間?

這與編程有什麼關係?

在函數式編程中,狀態通常是孤立的。你總是知道某種狀態的來源。狀態永遠不會分散在不同的函數中。在面向對象編程中,每個對象都有自己的狀態,在構建程序時,必須記住當前正在使用的所有對象的狀態。

為了使工作更容易,最好只在代碼庫中處理狀態的一小部分。讓應用程序的核心部分是無狀態和純的。這實際上是前端流量模式(又稱Redux)取得巨大成功的主要原因。

雜亂共享的狀態

因為有分散的可變狀態,我們的工作並不那麼困難。面向對象編程更進了一步!

舉個現實世界的例子:

在現實世界中,可變狀態幾乎從來都不是問題,因為事物都是私有的,從不共享。這就是工作中的“適當封裝”。想象一下,一位畫家正在創作下一幅《蒙娜麗莎》。他獨自完成了這幅畫,完成後以數百萬美元的價格賣出了他的傑作。

現在,他厭倦了金錢,決定做一些不一樣的事情。他認為舉辦一個繪畫聚會是個好主意。他邀請他的朋友小精靈、甘道夫、警察和殭屍來幫助他。這是一個團隊合作!他們同時開始在同一塊畫布上作畫。當然,結果並不好,這幅畫完全是一場災難!

共享可變狀態在現實世界中沒有任何意義。然而,這正是面向對象編程中所發生的事情——狀態在不同的對象之間隨意共享,並且以它們認為合適的方式對其進行修改。反過來,隨著代碼庫的不斷增長,對程序的推理也變得越來越困難。

併發問題

面向對象編程代碼中可變狀態的混雜共享使得這種代碼幾乎不可能並行化。為了解決這個問題,人們發明了複雜的機制。線程鎖定、互斥和許多其他機制已經被髮明出來。當然,這種複雜的方法也有其自身的缺點——死鎖、缺乏可組合性、調試多線程代碼既困難又耗時。更別提使用這種併發機制會增加複雜性。

並非所有狀態都是邪惡的

所有的狀態都是邪惡的嗎?並非如此。Alan Kay狀態並不邪惡!如果狀態變化是真正孤立的(而不是“面向對象”的孤立),則可能是好的。

擁有不可變的數據傳輸對象也是完全可以的。關鍵是“不變”。這些對象會被用在函數之間傳遞數據。

然而,這樣的對象也會使面向對象的方法和屬性完全冗餘。如果一個對象不能被改變,那麼它的方法和屬性又有什麼用呢?

面向對象編程的可變性是固有的

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

當然也有例外——即Scala和OCaml,但它們不是主流面嚮對象語言,本文將不會討論它們。

面向對象編程的災難:是時候考慮更新換代了


封裝的木馬病毒

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Jamie McInall



封裝被譽為是面向對象編程的最大優點之一。它應該保護對象的內部狀態不受外部訪問的影響。不過這有個小問題。它並不起作用。

封裝是面向對象編程的木馬病毒。它通過使共享可變狀態看起來安全來推銷這種想法。封裝允許(甚至鼓勵)不安全的代碼潛入我們的代碼庫,使代碼庫從內部腐爛。

全球狀態問題

眾所周知,全球狀態是萬惡之源。無論如何都應該避免。然而,封裝實際上是美化的全局狀態這一事實卻鮮有人知。

為了使代碼更有效,傳遞對象不是通過對象的值,而是通過對象的引用。這就是“依賴注入”失敗的地方。

試著解釋一下:無論何時在面向對象編程中創建對象,都會將對其依賴項的引用傳遞給構造函數。這些依賴項也有己的內部狀態。新創建的對象將對這些依賴項的引用存儲在其內部狀態中,然後以自己喜歡的方式修改它們。它還將這些引用傳遞給它可能最終使用的其他東西。

這就創建了一個雜亂共享對象的複雜圖像,這些對象最終都改變了彼此的狀態。由於幾乎不可能看到是什麼導致了程序狀態的更改,而導致了巨大的問題。調試這樣的狀態更改可能會浪費幾天的時間。

方法/屬性

提供對特定字段的訪問的方法或屬性並不比直接更改字段的值更好。不管是否使用一個特別的屬性或方法來修改對象的狀,結果都是一樣的——修改後的狀態。

面向對象編程的災難:是時候考慮更新換代了


現實世界建模的問題


面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Markus Spiske


有人說面向對象編程試圖模擬現實世界。這根本不是真的——面向對象編程與現實世界沒有任何關聯。試圖將程序建模為對象可能是面向對象編程設計中最大的錯誤之一。

現實世界不是層級森嚴的

面向對象編程設計試圖把一切都建模為對象的層次結構。不幸的是,現實世界並非如此運轉。現實世界中的對象使用消息進行交互,但絕大多是相互獨立的。

現實世界中的繼承

面向對象編程的繼承不是模仿現實世界。現實世界中的父對象在運行時無法更改子對象的行為。即使你從父母那裡繼承了你的基因,他們也不能隨心所欲地改變你的基因。你不是從父母那裡繼承“行為”,而是發展自己的行為。進一步而言,你也不能“無視”你父母的行為。

現實世界沒有辦法

你正在用的那張紙上有“寫”的這一方法嗎?並沒有。你拿著一張空紙,拿起一支筆,寫一些文字。作為一個人,你也沒有“寫”的方法——你根據外部事件或你的內心想法來決定寫一些文字。

面向對象編程的災難:是時候考慮更新換代了


名詞王國


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



面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Cederic X



對象(或名詞)是面向對象編程的核心。面向對象編程的一個基本限制是它將所有東西都強制轉換成名詞。不是所有的東西都應該被建模為名詞。操作(功能)不應被建模為對象。只需要一個將兩個數字相乘的函數,為何卻要被迫創建一個乘法類?簡單的使用一個乘法函數,讓數據成為數據,讓函數成為函數!

在非面向對象編程中,類似將數據保存到文件這樣的小事很簡單——就像用簡單的英語描述一個動作。

舉一個真實的例子:

讓我們回到畫家的例子,該畫家擁有一個繪畫工廠。他僱傭了一名專職的刷牆經理、色彩經理、一名畫布經理和一名MonaLisa供應商。他的好朋友殭屍利用了大腦消耗策略。這些對象依次定義了以下方法:創建繪畫、查找畫筆、拾取顏色、調用MonaLisa和消費鏈接。

當然這相當愚蠢,在現實世界中是不可能發生的。畫一幅畫這個簡單的動作產生了多少不必要的複雜性?

當它們能和對象分開存在時,就沒有必要發明奇怪的概念來容納你的函數。

面向對象編程的災難:是時候考慮更新換代了


單元測試

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ani Kolleshi


自動化測試是開發過程中的一個重要部分,它極大地有助於防止迴歸(即缺陷被引入到現有代碼中)。單元測試在自動化測試過程中起著巨大的作用。

有些人可能不同意,但眾所周知的是,面向對象程序代碼很難進行單元測試。單元測試假設獨立地測試事物,並使得方法成為單元可測試的:

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

2. 為新創建的類創建一個接口。

3. 聲明字段以此來保存新創建類的實例。

4. 利用模擬框架來模擬依賴關係。

5. 利用依賴注入框架來注入依賴項。

為了使一段代碼可測試,還需要創建多少複雜性? 浪費了多少時間才能使一些代碼可測試?

備註:為了測試一個方法,我們還必須實例化整個類。這也將從它的所有父類中引入代碼。

有了面向對象編程,為遺留代碼編寫測試更加困難——幾乎不可能。圍繞測試遺留面向對象程序代碼的問題,已經創建了整個公司((TypeMock)。

樣板代碼

談到信噪比,樣板代碼可能是最大的違規者。樣板代碼是程序編譯所需的“噪音”。樣板代碼需要時間編寫,並且由於增加了噪聲,使得代碼庫可讀性降低。

雖然“編程到接口,而不是執行”是面向對象編程中推薦的方法,但並不是所有的東西都應該成為接口。為了測試性的唯一目的,不得不訴諸於在整個代碼庫中使用接口。還可能不得不利用依賴注入,這進一步引入了不必要的複雜性。

測試私有方法

有些人說私有方法不應該被測試……不置可否,單元測試被稱為“單元”是有原因的——單獨測試小的代碼單元。然而,在面向對象編程中測試私有方法幾乎是不可能的。我們不應該僅僅為了測試性而將私有方法內部化。

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

面向對象編程的災難:是時候考慮更新換代了


重構


重構是開發人員日常工作的重要組成部分。諷刺的是,眾所周知,面向對象程序代碼很難重構。重構應該使代碼不那麼複雜,且更容易維護。相反,重構的面向對象編程代碼變得非常複雜——為了使代碼可測試,我們必須利用依賴注入,併為重構的類創建一個接口。即便如此,如果沒有像Resharper這樣的專用工具,重構面向對象程序代碼真的很難。

// 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();
}


以上簡單示例,僅僅為了提取一個方法,行數就增加了一倍多。當代碼被重構以降低複雜性的時候,為什麼會產生更多的複雜性呢?

將它與JavaScript中類似的非面向對象編程代碼重構對比:

// 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;
}
}


代碼實際上保持不變——不過是將isValidInput函數移到不同的文件中,並添加一行來導入該函數。為了測試性,我們還在函數簽名中添加了_isValidInput。

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

這還不是全部。重構面向對象程序代碼風險極大。複雜的依賴圖和狀態分散在面向對象程序代碼庫中,使得人腦不可能考慮所有潛在的問題。

面向對象編程的災難:是時候考慮更新換代了


Band-aids

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Pexels Pixabay


當某些東西不起作用時,我們該怎麼辦?很簡單,只有兩個選擇——扔掉或者試著修理。面向對象則難以去除,數百萬開發人員接受了面向對象的培訓。全世界數百萬的組織都在使用面向對象程序。

你可能已經看到面向對象編程並不能真正起作用,它使我們的代碼變得複雜和不可靠。然並非只有你一人面臨這樣的問題!幾十年來,人們一直在努力解決面向對象程序代碼中普遍存在的問題。他們想出了無數的設計模式。

設計模式

面向對象程序編程提供了一套原則,理論上應該可支持開發人員逐步構建越來越大的系統:SOLID原則、依賴注入、設計模式等等。

不幸的是,設計模式只不過是創可貼。它們的存在僅僅是為了解決面向對象程序設計的缺點。關於這個主題,已經寫了無數的書。如果不是給代碼庫帶來了巨大的複雜性,面向對象編程也就不會那麼糟糕。

問題工廠

事實上,編寫好的、可維護的面向對象代碼是不可能的。

另一方面,一個不一致的面向對象編程庫,似乎不符合任何標準。另一方面,有一個過度設計的代碼塔,一堆錯誤的抽象一個接一個地建立起來。設計模式對於構建這樣的抽象塔非常有幫助。

很快,增加新的功能,甚至理解所有的複雜性,變得越來越難。代碼庫將充滿像SimpleBeanFactoryAwareAspectInstanceFactory, AbstractInterceptorDrivenBeanDefinitionDecorator, TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactory.

試圖理解開發人員自己創造的抽象之塔,必須浪費寶貴的腦力。沒有結構在很多情況下比結構不好要好。

面向對象編程的災難:是時候考慮更新換代了

圖片來源: https://www.reddit.com/r/ProgrammerHumor/com


面向對象編程的災難:是時候考慮更新換代了


四大面向對象編程支柱的衰落


面向對象編程的四大支柱是:抽象、繼承、封裝和多態。

讓我們逐一來看看四大支柱到底是怎樣的。

繼承

可重用性的缺乏來自面向對象編程的語言,而不是函數語言。因為面向對象編程語言的問題是,其擁有所有這些隱含的環境。你想要一根香蕉,但你得到的是一直拿著香蕉的大猩猩和整個叢林。Joe Armstrong, Erlang的創造者


面向對象編程繼承與現實世界無關。事實上,繼承是實現代碼可重用性的低劣方式。“四人幫”已經明確提出,比起繼承,更喜歡組合。一些現代編程語言完全避免繼承。

繼承有幾個問題:

1. 引入許多類不需要的代碼(香蕉和叢林問題)。

2. 將類的一部分定義在其他地方會使代碼很難推理,尤其是在有多個繼承級別的情況下。

3. 在多數編程語言中,多重繼承幾乎是不可能的。這主要使得繼承作為代碼共享機制毫無用處。

多態

多態很棒,它允許我們在運行時改變編程行為。然而,這是計算機編程中非常基本的概念。面向對象編程多態性完成了這項工作,但再次導致精神雜耍行為。這會使得代碼庫變得非常複雜,並且關於被調用的具體方法的推理變得非常困難。

另一方面,函數編程使得我們以更簡潔的方式實現相同的多態性……只需傳遞一個定義所需運行時行為的函數。還有什麼比這更簡單的呢?不需要在多個文件(和接口)中定義一堆重載的抽象虛擬方法。

封裝

正如前面討論的,封裝是面向對象編程設計的特洛伊木馬。它實際上是一個美化的全局可變狀態,並使不安全的代碼看起來是安全的。不安全的編碼實踐是面向對象編程人員在日常工作中依賴的支柱……

抽象

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

在過程/功能語言中,我們可以簡單地在相鄰文件中“隱藏”實現細節。沒有必要稱這一基本行為為“抽象”。

面向對象編程的災難:是時候考慮更新換代了


為什麼面向對象編程控制了這個行業?

答案很簡單,爬蟲類的外星種族與國家安全局(和俄羅斯人)合謀將美國程序員折磨致死……

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Gaetano Cessati


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

自從微軟拒絕服務以來,Java是計算領域發生的最令人苦惱的事情。Alan Kay,面向對象編程的發明者

Java很簡單

當JAVA在1995年首次被引入時,與其他語言相比,Java是一種非常簡單的編程語言。當時,編寫桌面應用程序的門檻很高。開發桌面應用程序需要用C語言編寫低級win32應用程序接口,開發人員還必須關心手動內存管理。另一種選擇是Visual Basic,但許多人可能不想把自己鎖在微軟的生態系統中。

當Java被引入時,對許多開發人員來說,它是一個簡單的工具,因為它是免費的,可以在所有平臺上使用。像內置垃圾收集、友好命名的APIs(與神祕的win32 APIs相比)、合適的名稱空間和熟悉的類C語法這樣的東西使Java更容易使用。

圖形用戶界面編程也變得越來越流行,似乎各種用戶界面組件都很好地映射到類上。IDEs中的方法自動完成也使人們聲稱面向對象編程接口更容易使用。

如果不是把面向對象編程強加給開發人員,也許Java不會那麼糟糕。Java的所有功能都很好。其中垃圾收集、可移植性、異常處理特性,是其他主流編程語言所缺乏的,但在1995年非常棒

然後C#出現了

最初,微軟一直嚴重依賴Java。當事情開始出錯時(在與太陽微系統公司就Java許可進行了長時間的法律鬥爭之後),微軟決定投資自己的Java版本。這就是C# 1.0誕生的時候。C#作為一種語言,一直被認為是“更好的Java”。然而,也存在個巨大的問題——隱藏在稍微改進的語法下,它同樣也屬於面嚮對象語言,有著同樣的缺陷。

微軟一直在大力投資NET生態系統,其中還包括良好的開發工具。多年來,視覺工作室可能是最好的集成開發環境之一。這反過來又導致了NET框架的廣泛使用,尤其是在企業中。

最近,微軟一直在大力投資瀏覽器生態系統,推出它的TypeScript。TypeScript很棒,因為它可以編譯成JavaScript並添加靜態類型檢查等內容。不足之處在於沒有對函數構造的適當支持——沒有內置的不可變數據結構,沒有函數組合,沒有適當的模式匹配。 TypeScript是面向對象編程的第一步,對於瀏覽器來說主要是C#。Anders Hejlsberg 甚至負責C#和打字稿的設計。

功能語言

另一方面,函數式語言從未得到像微軟這樣大公司的支持。鑑於投資不多,F#不算數。功能語言的發展主要是由社區驅動的。這可能解釋了面向對象程序設計語言和浮點語言之間流行程度的差異。

面向對象編程的災難:是時候考慮更新換代了


是時候繼續前進了?


我們現在知道面向對象是一個失敗的實驗。是向前看的時候了。現在是我們作為一個社區承認這個想法讓我們失望的時候了,我們必須放棄它。Lawrence Krubner
面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash SpaceX



為什麼我們堅持使用從根本上說是次優的程序組織方式?這是明顯的無知嗎?從事軟件工程的人並不愚蠢。通過使用像“設計模式”這樣花哨的面向對象的術語如抽象”、“封裝”、“多態性”和“接口隔離”,我們是否更擔心在我們的同行面前“看起來聰明”?可能不會。

繼續使用已經使用了幾十年的東西真的很容易。大多數人從未真正嘗試過函數式編程。那些使用過的人再也不會回去寫面向對象程序代碼了。

亨利·福特曾經說過一句名言——“如果我問人們想要什麼,他們會說跑得快的駿馬”。在軟件世界裡,大多數人可能想要一種“更好的面嚮對象語言”。人們可以很容易地描述他們遇到的一個問題(使代碼庫有條理,不那麼複雜),但不是最好的解決方案。

面向對象編程的災難:是時候考慮更新換代了


還有哪些替代方案?


如果像functors和monads這樣的術語讓你有點不安,那麼你並不孤單!如果函數式編程為某些概念提供更直觀的名稱,那麼它們就不會那麼可怕。函子? 這只是我們可以用函數轉換的東西,想想list.map? 簡單的計算可以鏈接!

嘗試函數式編程會讓你成為更好的開發者。你將最終有時間編寫解決現實世界問題的真實代碼,而不是花大部分時間思考抽象和設計模式。

你可能沒有意識到這一點,但是你已經是一個功能性的程序員了。你在日常工作中使用功能嗎?是的?那你已經是一個功能性程序員了!你只需要學會如何充分利用這些功能。

兩種學習曲線非常平緩的偉大函數語言是埃利希蘭德榆樹。它們讓開發人員專注於最重要的事情——編寫可靠的軟件,同時消除更多傳統功能語言的複雜性。

還有其他選擇嗎?試試F#吧——它是一種令人驚歎的功能語言,並提供了與現有語言的良好互操作性。NET代碼。使用Java?那麼使用Scala或Clojure都是很好的選擇。使用JavaScript?有了正確的指導和限制,JavaScript可以成為一種很好的功能性語言。

面向對象編程的災難:是時候考慮更新換代了


面向對象編程的捍衛者

面向對象編程的災難:是時候考慮更新換代了

圖片來源:Unsplash Ott Maidre


期待面向對象編程的捍衛者們做出某種反應。他們會說這篇文章不準確。有些人甚至可能開始罵人。

他們有權發表自己的意見。然而,他們為面向對象編程設計辯護的論點通常相當薄弱。諷刺的是,他們中的大多數人可能從未真正用真正的函數語言編程。如果從未真正嘗試過兩件事,又怎會在兩件事之間進行比較呢?這樣的比較是無用的。

德米特里定律不是很有用——它無助於解決非決定論的問題,不管你如何訪問或改變那個狀態,共享的可變狀態仍然是共享的可變狀態。a.total()並不比a.getB().getC().total()好多少。只是掩蓋了問題。

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

只是工具箱裡的一個工具……

人們經常說面向對象只是工具箱中的另一個工具。是的,它是工具箱中的一個工具,就像馬和汽車都是交通工具一樣……畢竟,它們都服務於相同的目的,對嗎?當我們可以繼續騎好馬的時候,為什麼還要用汽車呢?

歷史會重演

1900年,紐約路上形式的車輛寥寥無幾,人們一直用馬來運輸。1917年,公路上不再有馬了。圍繞馬匹運輸為中心產生了一個巨大的產業。整個企業都是圍繞著糞肥清理這樣的事情創建的。

人們抵制變革。他們稱汽車是最終會過去的另一種“時尚”。畢竟,馬已經存在幾個世紀了!有些人甚至要求政府幹預。

這有什麼關係?軟件業以面向對象編程為中心。數百萬人接受了面向對象編程的培訓,數百萬公司在其代碼中使用了面向對象編程。當然,他們會試圖詆譭任何威脅他們生計的東西!這是常識。

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

面向對象編程的災難:是時候考慮更新換代了


留言 點贊 關注

我們一起分享AI學習與發展的乾貨

編譯組:廖馨婷、宋蘭欣

相關鏈接:

https://medium.com/better-programming/object-oriented-programming-the-trillion-dollar-disaster-%EF%B8%8F-92a4b666c7c7

如需轉載,請後臺留言,遵守轉載規範

"

相關推薦

推薦中...