面向對像編程的弊端是什麼? | 知乎問答精選

 

A-A+

面向對像編程的弊端是什麼?

2018年07月18日 知乎問答精選 暫無評論 閱讀 13 ℃ 次

【馮東的回答(72票)】:

面向對象的弊端在於作為一種建模技術沒有很好的定義自己的適用範圍。面向對像脫胎的環境有兩個重要因素,一是基於 WIMP (Window, Icon, Menu, Pointer) 的圖形化界面,二是早期提供圖形界面接口的機器缺乏代碼級別之外的組件管理方式 (比如 Unix 的進程和 IPC)。

面向對像在 WIMP 的環境中是很必要也是很成功的。原因是 WIMP 環境需要重量的實現繼承提供的重用,WIMP 的對象種類能很好的被單繼承模擬,WIMP 的屬性和類別容易區分。而面向對像擴展到 WIMP 之外的環境中就失敗了:

  1. 實際世界是多緯度的,屬性和類別不好區分。紅蘋果是 color 屬性為 red 的蘋果,還是 Apple 的子類?
  2. 實際世界的工具是用來完成任務的。而不是象 WIMP 那樣構建一個虛擬的空間化界面。
  3. 《人月神話》指出,編寫 reusable code 比編寫普通 code 至少要多花三倍的工作量。而面向對象的模糊了代碼的重用和使用。使被重用的代碼的依賴複雜化。導致很多不適合被重用的代碼被重用。編寫代碼時要過分考慮重用的可能性。
  4. 其它管理複雜度的機制越來越流行。

【知乎用戶的回答(70票)】:

弊端是,沒有人還記得面向對像原本要解決的問題是什麼。

1、面向對像原本要解決什麼(或者說有什麼優良特性)

似乎很簡單,但實際又很不簡單:面向對像三要素封裝、繼承、多態

警告:事實上,從業界如此總結出這面向對像三要素的一剎那開始,就已經開始犯錯了!)。

封裝:封裝的意義,在於明確標識出會訪問某個數據結構(用面向對象的術語來說就是 類成員變量)的所有接口

有了封裝,就可以明確區分內外,使得類實現者可以修改封裝的東西而不影響部調用者;而外部調用者也可以知道自己不可以碰哪裡。這就提供一個良好的合作基礎——或者說,只要接口這個基礎約定不變,則代碼改變不足為慮。

繼承+多態:繼承和多態必須一起說。一旦割裂,就說明理解上已經誤入歧途了。

先說繼承:繼承同時具有兩種含義:其一是繼承基類的方法,並做出自己的擴展——號稱解決了代碼重用問題;其二是聲明某個子類兼容於某基類(或者說,接口上完全兼容於基類),外部調用者可無需關注其差別。

再說多態:基於對像所屬類的不同,外部對同一個方法的調用,實際執行的邏輯不同。

很顯然,多態實際上是依附於繼承的第二種含義的。讓它與封裝、繼承這兩個概念並列,是不符合邏輯的。不假思索的就把它們當作可並列概念使用的人,顯然是從一開始就被誤導了。

實踐中,繼承的第一種含義(實現繼承)意義並不很大,甚至常常是有害的。因為它使得子類與基類出現強耦合。

繼承的第二種含義非常重要。它又叫「接口繼承」。

接口繼承實質上是要求「做出一個良好的抽像,這個抽像規定了一個兼容接口,使得外部調用者無需關心具體細節,可一視同仁的處理繼承自同一個基類的對象」——這在程序設計上,叫做歸一化

歸一化使得外部使用者可以簡單的用一個for循環,不加區分的處理所有接口兼容的對象集合——就好像linux的泛文件概念一樣,所有東西都可以當文件處理,不必關心它是內存、磁盤、網絡還是屏幕(當然,如果你需要,當然也可以區分出「字符設備」和「塊設備」,然後做出針對性的設計:細緻到什麼程度,視需求而定)。

歸一化的實例:

a、一切對象都可以序列化/toString

b、一切UI對象都是個window,都可以響應窗口事件。

——必須注意,是一切(符合xx條件的)對象皆可以做什麼,而不是「一切皆對像」。後者毫無意義。

總結:面向對象的好處實際就這麼兩點。

一是通過封裝明確定義了何謂接口、何謂接口內部實現、何謂接口的外部調用者,使得大家各司其職,不得越界;

二是通過繼承+多態這種內置機制,在語言的層面支持歸一化的設計,並使得內行可以從代碼本身看到這個設計——但,注意僅僅只是支持歸一化的設計。不懂如何做出這種設計的外行仍然不可能從瞎胡鬧的設計中得到任何好處。

顯然,不用面向對像語言、不用class,一樣可以做歸一化的設計(如老掉牙的泛文件概念、遊戲行業的一切皆精靈),一樣可以封裝(通過定義模塊和接口),只是用面向對像語言可以直接用語言元素顯式聲明這些而已;

而用了面向對像語言,滿篇都是class,並不等於就有了歸一化的設計。甚至,因為被這些花哨的東西迷惑,反而更加不知道什麼才是設計。

2、人們以為面向對象是什麼、以及因此製造出的悲劇以及鬧劇

誤解一、面向對像語言支持用語言元素直接聲明封裝性和接口兼容性,所以用面向對像語言寫出來的東西一定更清晰、易懂

事實上,既然class意味著聲明了封裝、繼承意味著聲明了接口兼容,那麼錯誤的類設計顯然就是錯誤的聲明、盲目定義的類就是無意義的喋喋不休。而錯誤的聲明比沒有聲明更糟;通篇毫無意義的喋喋不休還不如錯誤的聲明

除非你真正做出了漂亮的設計,然後用面向對象的語法把這個設計聲明出來——僅僅聲明真正有設計、真正需要人們注意的地方,而不是到處瞎叫喚——否則不可能得到任何好處。

一切皆對像實質上是在鼓勵堆砌毫無意義的喋喋不休。大部分人——注意,不是個別人——甚至被這種無意義的喋喋不休搞出了神經質,以至於非要在喋喋不休中找出意義:沒錯,我說的就是設計模式驅動編程,以及如此理解面向對像編程。

誤解二、面向對像三要素是封裝、繼承、多態,所以只要是面向對像語言寫的程序,就一定「繼承」了語言的這三個優良特性

事實上,如前所述,封裝、繼承、多態只是語言層面對良好設計的支持,並不能導向良好的設計。

如果你的設計做不出真正的封裝性、不懂得何謂歸一化,那它用什麼寫出來都是垃圾。

誤解三、把軟件寫成面向對象的至少是無害的

要瞭解事實上是什麼,需要先科普幾個概念。

什麼是真正的封裝

——回答我,封裝是不是等於「把不想讓別人看到、以後可能修改的東西用private隱藏起來」?

顯然不是

如果功能得不到滿足、或者未曾預料到真正發生的需求變更,那麼你怎麼把一個成員變量/函數放到private裡面的,將來就必須怎麼把它挪出來。

你越瞎搞,越去搞某些華而不實的「靈活性」——比如某種設計模式——真正的需求來臨時,你要動的地方就越多。

真正的封裝是,經過深入的思考,做出良好的抽像,給出「完整且最小」的接口,並使得內部細節可以對外透明(注意:對外透明的意思是外部調用者完全意識不到細節的存在

一個設計,只有達到了這個高度,才能真正做到所謂的「封裝性」,才能真正杜絕對內部細節的訪問。

否則,生硬放進private裡面的東西,最後還得生硬的被拖出來——當然,這種東西經常會被美化成「訪問函數」之類渣渣(不是說訪問函數是渣渣,而是說因為設計不良、不得不以訪問函數之類玩意兒在封裝上到處挖洞洞這種行為是渣渣)。

接口繼承真正的好處是什麼?是用了繼承就顯得比較高大上嗎?

顯然不是。

接口繼承沒有任何好處。它只是聲明某些對像在某些場景下,可以用歸一化的方式處理而已。

換句話說,如果不存在「需要不加區分的處理類似的一系列對像」的場合,那麼繼承不過是在裝X罷了。

封裝可應付需求變更、歸一化可簡化(類的使用者的)設計:以上,就是面向對像最最基本的好處。

——其它一切,都不過是在這兩個基礎上的衍生而已。

換言之,如果得不到這兩個基本好處,那麼也就沒有任何衍生好處——應付需求變更/簡化設計並不是打打嘴炮就能做到的。

瞭解了如上兩點,那麼,很顯然:

1、如果你沒有做出好的抽像、甚至完全不知道需要做好的抽像就忙著去「封裝」,那麼你只是在「封」和「裝」而已。

這種「封」和「裝」的行為只會製造累贅和虛假的承諾;這些累贅以及必然會變卦的承諾,必然會為未來的維護帶來更多的麻煩,甚至拖垮整個項目。

正是這種累贅和虛假的承諾的拖累,而不是所謂的為了應付「需求改變」所必需的「靈活性」,才是大多數面向對像項目代碼量暴增的元兇。

2、沒有真正的抓到一類事物(在當前應用場景下)的根本,就去設計繼承結構,是必不會有所得的。

不僅如此,請注意我強調了在當前應用場景下

這是因為,分類是一個極其主觀的東西,不存在普適的分類法

舉例來說,我要研究種族歧視,那麼必然以膚色分類;換到法醫學,那就按死因分類;生物學呢,則搞門科目屬種……

想像下,需求是「時尚女裝」,你卻按「窒息死亡/溺水死亡/中毒死亡之體征」來了個分類……你說後面這軟件還能寫嗎?

類似的,我遇到過寫遊戲的卻去糾結「武器裝備該不該從遊戲角色繼承」的神人。你覺得呢?

事實上,遊戲界真正的抽像方法之一是:一切都是個有位置能感受時間流逝的精靈;而某個「感受到時間流逝顯示不同圖片的對象」,其實就是遊戲主角;而「當收到碰撞事件時,改變主角下一輪顯示的圖片組的」,就是遊戲邏輯。

看看它和「武器裝備該不該從遊戲角色繼承」能差多遠。想想到得後來,以遊戲角色為基類的方案會變成什麼樣子?為什麼會這樣?

——你還敢說面向對像無害嗎?

——在真正明白何謂封裝、何謂歸一化之前,每一次寫下class,就在錯誤的道路上又多走了一步。

——設計真正需要關注的核心其實很簡單,就是封裝和歸一化。一個項目開始的時候,「class」寫的越早,就離這個核心越遠

——過去鼓吹的各種面向對像方法論、甚至某些語言本身,恰恰正是在慫恿甚至逼迫開發者盡可能早、盡可能多的寫class。

誤解四、只有面向對像語言寫的程序才是面向對象的。

事實上,unix系統提出泛文件概念時,面向對像語言根本就不存在;遊戲界的精靈這個基礎抽像,最初是用C甚至彙編寫的;……。

面向對像其實是汲取以上各種成功設計的經驗才提出來的。

所以,面向對象的設計,不必非要c++/java之類支持面向對象的語言才能實現;它們不過是在你做出了面向對象的設計之後,能讓你寫得更愜意一些罷了——但,如果一個項目無需或無法做出面向對象的設計,某些面向對像語言反而會讓你很難受。

用面向對像語言寫程序,和一個程序的設計是面向對象的,兩者是八桿子打不著的兩碼事。純C寫的linux kernel事實上比c++/java之類語言搞出來的大多數項目更加面向對像——只是絕大部分人都自以為自己到處瞎寫class的麵條代碼才是面向對象的正統、而死腦筋的linus搞的泛文件抽像不過是過程式思維搞出來的老古董。

——這個誤解之深,甚至達到連wiki詞條裡面,都把OOP定義為「用支持面向對象的語言寫程序」的程度。

——恐怕這也是沒有人說泛文件設計思想是個騙局、而面向對像卻被業界大牛們嚴厲抨擊的根本原因了:真正的封裝、歸一化精髓被拋棄,浮於表面的、喋喋不休的class/設計模式卻成了」正統「!

借用樓下PeytonCai朋友的鏈接:

名家吐槽:面向對像編程從骨子裡就有問題

————————————————————————————

總結: 面向對像其實是對過去成功的設計經驗的總結。但那些成功的設計,不是因為用了封裝/歸一化而成功,而是切合自己面對的問題,給出了恰到好處的設計

讓一個初學者知道自己應該向封裝/歸一化這個方向前進,是好的;用一個面向對象的條條框框把他們框在裡面、甚至使得他們以為寫下class是完全無需思索的、真正應該追求的是設計模式,則是罪惡的。

事實上,class寫的越隨意,才越需要設計模式;就著錯誤的實現寫得越多、特性用得越多,它就越發的死板,以至於必須更加多得多的特性、模式、甚至語法hack,才能勉強完成需求。

只有經過真正的深思熟慮,才有可能做到KISS。

到處鼓噪的面向對像編程的最大弊端,是把軟件設計工作偷換概念,變成了「就著class及相關教條瞎胡鬧,不管有沒有好處先插一槓子」,甚至使得人們忘記去關注「抽像是否真正簡化了面對的問題」。

【知乎用戶的回答(13票)】:

我不知道為啥有人邀請我回答這個問題。不管怎麼樣我就來答一下

面向對像最大的問題是,這個詞早就變成buzzword了。各種語言都大談面向對象的優點,說自己支持面向對象,(所以就自動有這些優點),而沒人願意去瞭解,到底什麼才是面向對象。

面向對象的核心是消息。對象就好比是計算機,消息就好比是IP包。封裝就是說,一個對像只有通過發消息才能改變另一個對象的狀態。多態就是說,一個對像不關心另一個對象是怎麼工作的,只要遵守相同的協議,就能協作。一種語言要是要求程序員把函數調用腦補成消息的那一定不是面向對象的。但是很不幸,很多自以為是面向對象的語言,把函數調用稱為發消息, 把一組約定好參數格式的函數稱為協議,把名詞的含義都搞亂了,實際上,這種做法更接近於Abstract Data Type而不是面向對象。這就是現代的指鹿為馬。

所以,你會看到很多人會抱怨Erlang不支持面向對象,而且,Erlang最初的作者們也很討厭面向對象,實際上呢,在常見的語言裡,還真的只有Erlang是面向對象的。

你看看Erlang就會知道,對於面向對像來說,類並沒有那麼重要,當然了,繼承也沒有什麼大問題。這些開銷都是因為冒牌的面向對像語言不原生支持消息導致的,而不是類,或者面向對像導致的。

你能看見Erlang就因為兩點,一是,愛立信當年一個項目用C++,延期了很久也沒開發出來,沒辦法就拿Erlang試試,結果就有了那個AXD301。非要說的話,那也是面向對像提高了開發效率。二是,愛立信當時腦殘了,儘管內部研究已經得出結論說Erlang開發效率就是高,但還是因為Erlang是私有語言,禁止在新項目中使用Erlang。Erlang的開發者們不接受這種說法,就去推動Erlang開源。

現在根本就是不是談面向對像弊端的時候,等Erlang流行起來再來談還差不多。

就是這樣

【kubisoft的回答(12票)】:

大部分問題是面向對像無法解決的。能夠採用面向對像簡化模型得到代碼重用性的時候,就用;不能的時候,就放棄,不要到處都用。

有的場合下,面向對像會增加代碼複雜度,增加維護的困難。

除了面向對象,我們還有很多辦法來實現我們要做的功能。不談更加高端大氣的函數式編程,其實用一串switch case或者else if就可以實現,代碼還簡單。

所有需要的東西都放在一起,同一個文件裡面同一段代碼,閱讀起來並不困難,很整齊。某個case下面太長的時候,也可以獨立出來包裝成一個函數調用;你愛放在同一個文件裡面還是另開一個文件都可以。

可是面向對象是一個很重量級的方法。你得設計類的繼承關係。得寫類的聲明,得實現不同的虛方法;實現時要注意是否要調用父類的方法。調試的時候經常搞不清楚到底執行哪個類的虛方法了。而用一大堆switch case,語句執行順序一目瞭然。

可能還是覺得代碼層級過多?有很多辦法可以整理得更乾淨。用了面向對象,省了switch case,卻要寫更多的virtual function的聲明和實現。

業務邏輯本身是複雜的,無法避免。沒有銀彈。

【知乎用戶的回答(14票)】:

很多時候為了OO而OO就是它最大的弊端。

【vczh的回答(17票)】:

你怎麼會覺得類關係的設計會成為一個開銷呢?其實正因為你有這種感覺,所以你才不能參加大型軟件的開發的。開發一個軟件比這個複雜的東西多了去了,連幾個類的關係你都會覺得很麻煩,那邏輯根本不要處理了。

當然,我並不是說OO就是萬能的,他當然在某些領域裡面有壞處,但是壞處也不是在你想像中的什麼

【類關係的開銷】上的。但是一個東西要不要用OO做,基本上得看你老闆喜歡什麼語言。就算他做個UI framework想用haskell,你也得上了。

【知乎用戶的回答(8票)】:

作為C原教旨教徒和C++一生黑以及從不用類的Python黨怒答。有函數有數據結構就可以了,面向對象就是個騙局………

【不知秋的回答(3票)】:

面向對像只是一種組織邏輯的方式,類關係複雜說明邏輯沒劃分清楚。

【知乎用戶的回答(5票)】:

不忘初心,方得始終。

前面幾個答案都說到了點子上:OOP最大的弊端,就是很多程序員已經忘記了OOP的初心,潛意識中把OOP教條主義化(如同對GOTO語句的禁忌一般),而不是著眼於OOP著力達到的、更本質的目標,如:

- 改善可讀性

- 提升重用性

但是OOP最重要的目標,其實是OCP,即「開閉原則」。這一點很多答案都沒有提到。

遵循開閉原則設計出的模塊具有兩個主要特徵:

(1)對於擴展是開放的(Open for extension)。這意味著模塊的行為是可以擴展的。當應用的需求改變時,我們可以對模塊進行擴展,使其具有滿足那些改變的新行為。也就是說,我們可以改變模塊的功能。

(2)對於修改是關閉的(Closed for modification)。對模塊行為進行擴展時,不必改動模塊的源代碼或者二進制代碼。模塊的二進制可執行版本,無論是可鏈接的庫、DLL或者.EXE文件,都無需改動。

這就是為什麼會有「用多態代替switch」的說法。在應該使用多態的地方使用switch,會導致:

1 - 違反「開放擴展」原則。假如別人的switch調用了你的代碼,你的代碼要擴展,就必須在別人的代碼裡,人工找出每一個調用你代碼的switch,然後把你新的case加進去。

2 - 違反「封閉修改」原則。這是說,被switch調用的邏輯可能會因為過於緊密的耦合,而無法在不碰switch的情況下進行修改。

但是OCP不是免費的。如果一個模塊根本沒有擴展的需求,沒有多人協作的需求,花時間達成OCP又有什麼意義呢?設計類關係的時候忘記了OOP的初心,可能就會寫出很多沒有幫助的類,白白浪費人力和運行效率。

所以,假如所有代碼都是你一個人維護,沒有什麼擴展的需求,那麼多用一些switch也未嘗不可;假如你的代碼是要被別人使用或者使用了別人的代碼,OOP很可能就是你需要用到的工具。

除了OOP,Type class和Duck Typing都是可以幫助你達成OCP原則的工具。當然,如果你使用的語言是Java,這兩種工具都不用想了。

【PeytonCai的回答(4票)】:

名家吐槽:面向對像編程從骨子裡就有問題

我覺得只有封裝和接口才是軟件設計的精髓,這也應該是為什麼GO語言了只實現了接口這個特性。

【薛非的回答(2票)】:

我覺得也許問題應該是面向對像編程的局限是什麼為好吧面向對像編程並不適用所有情況

在不適用的情況下硬要面向對像編程必然有問題

但把這歸結為弊端也許不太公平

【姜兆寧的回答(6票)】:

容易走神,背對對像會好很多

【童剛剛的回答(1票)】:

面向對像和面向過程只是代碼的組織方法不同,代碼本身還是那些代碼,並沒有減少。

但是面向對像和面向過程最關鍵的還是思想,你做題目的時候總是先從大局出發,而面向過程卻要求你先寫最小的單元。

要你算一下1+2等於幾,用面向過程肯定最簡單,要是寫個現實的程序就困難了。

【郭小燦的回答(0票)】:

沒有關係設計開銷其實很可能花更多時間在複雜邏輯的編寫上而且很有可能不容易維護

,目前來說面向對像還是構建易維護軟件的比較理想的形式

另外就編寫邏輯層來說確實寫起來相比於函數式編程會比較慢

【張鑫的回答(0票)】:

面向對象是一個非常基礎的概念,題主想質疑的可能是面向對像中的過度使用的抽像設計。還有一些語言的面向對象是無類型的,比如JavaScript,題主可以多瞭解一下。

【JaskeyLam的回答(0票)】:

增加了設計上的難度。

【殷奎生的回答(0票)】:

我認為面向對象是個過渡概念,有它的歷史價值。由面向對像到基於接口的組件編程,是個巨大進步。解決了大規模軟件組織的困難。不過因為面向對象是有狀態的,在cpu多核成為主流的今天,有狀態成為發揮多核效能的最大的 困難(同步、鎖,不好整)。所以函數式編程又鹹魚翻身了。

【qwetyed的回答(0票)】:

面向對像得目標是大規模重用代碼。

面向對象的手段是綁定結構和函數。

面向對象的哲學含義是給客體下定義來完成形式化抽像。 目的說白了就是為了盡可能重用代碼來減少程序員工作。但是矛盾的地方在於,真實世界中的客體的定義隨著主體的看法而改變,換句話,不存在固定的形式化抽像,一切都要符合環境(上下文)。最簡單的例子就是,中文詞的含義天天變,只有根據環境知識才能知道是什麼含義。

而代碼是為了具體問題而出現的,所以不存在通用抽像,也就不存在可以無限重用的定義和邏輯。

所以對象也就是用於計算的模型而已,技術手段是正確的(給數據綁定操作) 但是對於目標(大規模代碼重用)相去甚遠,能重用的應該只有為了解決問題的方法,而不是只有模型。

另外的難點,不同人為了解決相似問題,開發出來的模型可以十萬八千里,為了重用模型,修改之後又能適應新問題,於是這叫泛化,它估計你去製造全能模型,但是不僅難,還沒法向後兼容,有時候就硬是要把飛機做成魚……這就是面向對像思維的硬傷,創造出一個大家都懂,大家都認為對,大家都能拿去用的模型太難!

更加合理的設計理念應該還是 提供機制而不限制策略 所以從這一點上來看,提供一個常用的函數庫要比精心設計複雜的對象模型要好的多。

更進一步,理想狀態是全世界共享一種函數式語言,共享一份源代碼,並對問題分類,將所用所有函數按照問題進行整理,只有這樣感覺才能徹底避免重新發明輪子。

【劉十九的回答(0票)】:

前段時間讀到一篇文章《名詞王國的死刑》

這是一種信仰,拿著錘子,把世界當成釘子。

當一個新的想法出現在你腦海的時候,你不得不重塑它,包裝它,甚至弄碎它,直到它變成一個名詞,即使它一開始是一個動作,過程,或者任何不是"物"的概念。

我沒有黑面相對象的意思,相反我很喜歡這個編程範式,只是沒有什麼東西是完美的,如果用面相對像表達一些東西很彆扭的時候,我們不必總是去妥協我們的思維,也許我們還可以試試函數式,或許就柳暗花明了呢

【宋銳的回答(0票)】:

OO 最有用的地方:將可能改變的地方,變量化。

標籤:-編程 -計算機科學 -程序員 -編程語言 -面向對像編程


相關資源:





給我留言