面向對象編程是否已淘汰?函數式編程的槍口瞄錯了對象
編程在上世紀60年代遇到了一個大問題:計算機那時還沒有那么強大,需要以某種方式在數據結構和進程之間分配容量。這意味著如果擁有大量數據,那么在不將計算機推向極限的情況下,很多事情將無法完成。另一方面,如果需要做很多事情,那么就不能使用過多的數據,否則計算機將永遠占據空間。
接著艾倫·凱(AlanKay)大約于1966年或1967年得出理論認為可以使用封裝的微型計算機,這些微型計算機不共享數據,而是通過消息傳遞進行通信。這樣可以更加經濟地使用計算資源。
盡管這個想法很巧妙,但直到1981年,面向對象編程才成為主流。但是從那以后,它并沒有停止吸引軟件開發新手和老手。面向對象編程的程序員一如既往的繁忙。
但近年來,這一已有十年歷史的范式受到越來越多的批評。難道是在面向對象程序設計大行其道40年之后,技術已經超越了這種范式?
帶數據的耦合函數是否可笑?
面向對象編程的主要思想非常簡單:嘗試將一個程序分解為功能強大的整體。隨之而來的是,將數據片段和僅在相關數據上使用的那些函數耦合在一起。
請注意,這僅涵蓋封裝的概念。也就是說,位于對象內部的數據和函數對于外部是不可見的,一個人只能通過消息(通常稱為getter和setter函數)與對象的內容進行交互。
繼承和多態并沒有包含在最初的想法中,但是對于當今的面向對象編程而言,這是必需的。繼承基本上意味著開發人員可以定義具有其父類具有的所有屬性的子類,直到1976年——面向對象的程序設計概念問世十年后,才將其引入。
十年后,多態進入了面向對象的程序設計。從根本上講,這意味著方法或對象可以用作其他方法的模板。從某種意義上說,這是繼承的概括,因為并非原始方法或對象的所有屬性都需要傳輸給新實體;相反,可以選擇覆蓋屬性。
多態的特殊之處在于,即使兩個實體在源代碼中相互依賴,被調用實體的工作方式也更像插件。這使開發人員的工作更加輕松,他們不必再擔心運行時的依賴關系。
值得一提的是,繼承和多態性并不是面向對象編程所獨有的。真正的區別在于封裝數據及其所屬的方法。在那個計算資源比今天稀缺得多的時代,這是一個天才般的想法。面向對象的編程并不可笑,它使編碼變得容易得多。
面向對象編程中的五大問題
面向對象編程一問世便改變了開發人員查看代碼的方式。在1980年代以前,面向過程編程通常以機器為中心,開發人員需要非常了解計算機如何工作才能編寫好的代碼。
通過封裝數據和方法,面向對象的編程使軟件開發更加以人為中心。與人類的直覺相符,方法drive()屬于數據組 car,但不屬于teddybear組。當繼承產生時,這也很直觀。Hyundai是car的一個子類,并且具有相同的屬性,但PooTheBear卻不是,這是完全合理的。
這聽起來像是一臺強大的機器。但問題在于,只懂面向對象代碼的程序員將會用這種思維方式思考他們所做的一切。就像人們到處看到釘子一樣,因為他們只有錘子。正如我們將在下面看到的那樣,當你的工具箱只有錘子時,可能會導致致命的問題。
大猩猩叢林香蕉問題
如果你正在設置一個新程序,并且正在考慮設計一個新類。你可能會回想起為另一個項目創建的簡潔的小類,并且意識到這對當前正在嘗試的工作非常適合。沒問題!可以將舊項目中的類重用于新項目。
除了該類實際上可能是另一個類的子類之外,因此現在還需要把父類包括在內。然后你意識到父類也依賴于其他類,并且最終包含了代碼堆。
Erlang的創建者Joe Armstrong的這句話非常著名:“面向對象編程語言的問題在于,它們具有隨身攜帶的所有隱式環境。你想要香蕉,但是得到的是一只拿著香蕉的大猩猩和整個叢林。”
這對此方法進行了很好的說明。可以重用類,實際上,這可能是面向對象編程的主要優點。但不要走極端,有時最好編寫一個新類,而不是為了寫重復代碼而添加大量依賴項。要靈活變通,不要死板地遵從某個范式。
脆弱的基類問題
如果已經成功地將另一個項目中的類重用于新代碼,那么基類會發生怎樣的變化?
它可能會破壞整個代碼,而你甚至可能都沒有碰過它。也許有一天你手上的項目熠熠生輝,而第二天卻被打回原形,因為有人更改了基類中的一個細微細節,而該細節最終對項目至關重要。
使用繼承的次數越多,潛在的維護工作就越多。因此,即使在短期內重用代碼似乎非常有效,但從長遠來看,它可能會帶來很大的代價。
鉆石問題
繼承是一件可愛的小事,可以在其中繼承一類的屬性并將其轉移給其他類。但該如何組合兩個不同類的屬性?
這也許做不到,至少沒辦法以簡潔的方式做到,例如Copier類。(筆者從Charles Scalfani的熱門文章《再見,面向對象的編程》中借用了這個示例以及有關此處出現的問題的一些信息。)復印機掃描文檔的內容并將其打印在空白紙上,它應該是Scanner還是Printer的子類?
根本沒有好的答案。即使這個問題不會破壞代碼,但它經常出現足以令人沮喪。
層次問題
在鉆石問題中,問的是Copier是哪個類的子類。但其實我話沒說完,有一個簡單的解決方案。假設Copier 是父類,而Scanner 和Printer是僅繼承屬性子集的子類。
這就變得很簡單。但如果Copier只是黑白復印,而Printer還可以彩色打印怎么辦?從這個意義上說,打印機不是包括復印機的嗎?如果打印機連接到WiFi但復印機沒有連接怎么辦?
在類上堆積的屬性越多,建立適當的層次結構就越困難。確實,在處理屬性集群時,其中Copier共享了Printer的部分但不是全部屬性,反之亦然。而且,如果嘗試將其置于層次結構中,并且是一個大型復雜項目,則可能會導致混亂。不要混淆層次結構,否則可能會陷入混亂。
參考問題
有人也許會說那么我們將進行沒有層次結構的面向對象編程。其實相反,我們可以使用屬性集群,并根據需要繼承、擴展或覆蓋屬性。這會有些混亂,但這將是對當前問題的準確表現。
還有一個問題。封裝的全部目的是使數據片段彼此之間保持安全,從而使計算效率更高。沒有嚴格的層次結構,這是行不通的。
如果一個對象A通過與另一個對象B交互來覆蓋層次結構,會發生什么?A與B的關系并不重要,除了B不是直接的父類。然后,A必須包含對B的私有引用,否則,將無法交互。但是,如果A包含B的子代也具有的信息,則可以在多個位置修改該信息。因此,有關B的信息已不再安全,并且封裝被破壞。
盡管許多面向對象的程序員都使用這種架構來構建程序,但這并不是面向對象的編程,只是一團糟。
單一范式的危險
這五個問題的共同點是它們在不是最佳解決方案的地方實現了繼承。由于繼承甚至沒有包含在面向對象編程的原始形式中,因此筆者不會將這些問題稱為面向對象固有的問題,它們只是太過教條式的例子。
但是,不僅面向對象的編程可能會被夸大。在純函數式編程中,處理用戶輸入或在屏幕上打印消息極為困難。出于這些目的,面向對象或過程編程要好得多。
仍然有一些開發人員嘗試將這些東西實現為純函數,并將其代碼分解為數十行,沒人能理解。使用另一種范式,他們可以輕松地將代碼簡化為幾行可讀的代碼。
范式有點像宗教,它們都具有一定的合理性,耶穌、穆罕默德和佛陀說了一些很酷的話。但是,如果一直遵循教條,可能最終會使自己和周圍人的生活痛苦不堪。編程范式也是如此。毫無疑問,函數式編程正逐漸受到人們的歡迎,而在過去的幾年中,面向對象的編程遭到了一些嚴厲的批評。
了解新的編程范式并在適當的時候使用它們是有意義的。如果面向對象編程是使開發人員無論走到哪里都能看到釘子的錘子,那是把錘子扔出窗戶的原因嗎?不是。你在工具箱中添加了一把螺絲刀,也許是一把刀或一把剪刀,你不過是根據當前問題選擇工具。
函數式編程和面向對象編程的程序員都不要像對待宗教那樣對待編程范式。它們是工具,都可以在某處使用,所使用的內容僅取決于待解決的問題。
一個大問題:我們正處于一場新革命的風口浪尖上嗎?
歸根結底,關于函數式編程和面向對象編程的爭論(相當激烈)可以歸結為這一點:是否可以邁入面向對象編程時代的盡頭?
函數式編程通常是更有效的選擇,越來越多的問題出現。如數據分析、機器學習和并行編程,對這些領域的投入越多,就會越喜歡函數式編程。但看看現狀,有十多種面向對象編程的程序員提供的產品,還有一種針對函數式編碼器的產品。這并不意味著你不會喜歡這份工作,如今,函數式編程開發人員仍然非常稀缺。
最有可能的情況是,面向對象的編程將繼續存在十年左右。函數式編程當然會越來越受歡迎,但這并不意味著應該放棄面向對象編程。把面向對象編程作為保留技能仍然非常有優勢。
因此,在接下來的幾年中,不要將面向對象的編程丟到工具箱外,但是請確保它不是你唯一的工具。
本文轉載自微信公眾號「 讀芯術」,可以通過以下二維碼關注。轉載本文請聯系 讀芯術公眾號。