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

現時C++能夠說是支持OOP範式中最爲經常使用及高性能的語言。雖然如此,在C++使用OOP的編程方式在一些場合未能提供最高性能。 [1]詳細描述了這個觀點,我在此嘗試簡單說明。注意:其餘支持OOP的語言一般都會有本答案中說起的問題,C++只是一個合適的說明例子。

歷史上,OOP大概是60年代出現,而C++誕生於70年代末。如今的硬件和當時的有很大差別,其中最大的問題是內存牆_百度百科

圖1: 處理器和內存的性能提高比較,處理器的提高速度大幅高於內存[2]。

跟據Numbers Every Programmer Should Know By Year
圖2:2014年計算機幾種操做的潛伏期(latency)。

從這些數據,咱們能夠看出,內存存取成爲現代計算機性能的重要瓶頸。然而,這個問題在C++設計OOP編程範式的實現方式之初應該並未能考慮獲得。現時的OOP編程有可能不緩存友好(cache friendly),致使有時候並不能發揮硬件最佳性能。如下描述一些箇中緣由。

1. 過分封裝

使用OOP時,會把一些複雜的問題分拆抽象成較簡單的獨立對象,經過對象的互相調用去實現方案。可是,因爲對象包含本身封裝的數據,一個問題的數據集會被分散在不一樣的內存區域。互相調用時極可能會出現數據的cache miss的狀況。

2. 多態

在C++的通常的多態實現中,會使用到虛函數表。虛函數表是經過加入一次間接層來實現動態派送。但在調用的時候須要讀取虛函數表,增長cache miss的可能性。基本上要支持動態派送,不管用虛函數表、函數指針都會造成這個問題,但若是類的數目極多,把函數指針若是和數據放在一塊兒有時候可放緩問題。

3. 數據佈局

雖然OOP自己並沒有限制數據的佈局方式,但基本上絕大部分OOP語言都是把成員變量連續包裹在一段內存中。甚至使用C去編程的時候,也一般會使用到OOP或Object-based的思考方式,把一些相關的數據放置於一個struct以內:html

struct Particle {
    Vector3 position;
    Vector4 velocity;
    Vector4 color;
    float age;
    // ...
};


即便不使用多態,咱們幾乎不加思索地會使用這種數據佈局方式。咱們一般會覺得,因爲各個成員變量都緊湊地放置在一塊兒,這種數據佈局一般對緩存友好。然而,實際上,咱們須要考慮數據的存取模式(access pattern)。

在OOP中,經過封裝,一個類的各類功能會被實現爲多個成員函數,而每一個成員函數實際上可能只會存取少許的成員變量。這可能形式很是嚴重的問題,例如:java

for (Particle* p = begin; p != end; ++p)
    p->position += p->velocity * dt; // 或 p->SimulateMotion(dt);

在這種模式下,實階上只存取了兩個成員變量,但其餘成員變量也會載入緩存形成浪費。固然,若是在迭代的時候能存取儘可能多的成員變量,這個問題可能並不存在,但其實是很困難的。

若是採用傳統的OOP編程範式及實現方式,數據佈局的問題幾乎沒有解決方案。因此在[1]裏,做者提出,在某些狀況下,應該放棄OOP方式,以數據的存取及佈局爲編程的考慮重中,稱做面向數據編程(data-oriented programming, DOP)。

有關DOP的內容就不在此展開了,讀者可參考[1],還有[3]做爲實際應用例子。

[1] ALBRECHT, 「Pitfalls of Object Oriented Programming」, GCAP Australia, 2009. 
[2] Hennessy, John L., and David A. Patterson. Computer architecture: a quantitative approach. Elsevier, 2012.
[3] COLLIN, 「Culling the Battlefield」, GDC 2011. linux

 

**************************************************************************************************************************************************************c++

 

不忘初心,方得始終。

前面幾個答案都說到了點子上: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,這兩種工具都不用想了。shell

 

******************************************************************************************************************************************************************編程

 

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

一、面向對象本來要解決什麼(或者說有什麼優良特性)
彷佛很簡單,但實際又很不簡單:面向對象三要素封裝、繼承、多態

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

封裝:封裝的意義,在於明確標識出容許外部使用的全部成員函數和數據項,或者叫接口

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



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

先說繼承:繼承同時具備兩種含義:其一是繼承基類的方法,並作出本身的擴展——號稱解決了代碼重用問題;其二是聲明某個子類兼容於某基類(或者說,接口上徹底兼容於基類),外部調用者可無需關注其差異(內部機制會自動把請求派發[dispatch]到合適的邏輯)。

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

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


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

繼承的第二種含義很是重要。它又叫「接口繼承」。
接口繼承實質上是要求「作出一個良好的抽象,這個抽象規定了一個兼容接口,使得外部調用者無需關心具體細節,可一視同仁的處理實現了特定接口的全部對象」——這在程序設計上,叫作歸一化


歸一化使得外部使用者能夠不加區分的處理全部接口兼容的對象集合——就好象linux的泛文件概念同樣,全部東西均可以當文件處理,沒必要關心它是內存、磁盤、網絡仍是屏幕(固然,若是你須要,固然也能夠區分出「字符設備」和「塊設備」,而後作出針對性的設計:細緻到什麼程度,視需求而定)。

歸一化的實例:
a、一切對象均可以序列化/toString
b、一切UI對象都是個window,均可以響應窗口事件。

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


顯然,歸一化能夠大大簡化使用者的處理邏輯:這和帶兵打仗是相似的,班長鬚要知道每一個戰士的姓名/性格/特長,不然就不知道該派誰去對付對面山坡上的狙擊手;而連長呢,只需知道本身手下哪一個班/排擅長什麼就好了,而後安排他們各自去守一段戰線;到了師長/軍長那裏,他更關注戰場形勢的轉變及預期……沒有這種層層簡化、而是必須直接指揮到每一個人的話,累死軍長都無法指揮哪怕只是一場形勢明朗的衝突——光一個個打完電話就能把他累成啞吧。


軟件設計一樣。好比說,消息循環在派發消息時,只需知道全部UI對象都是個window,均可以響應窗口消息就足夠了;它不必知道每一個UI對象到底是什麼——該對象本身知道收到消息該怎麼作。

合理劃分功能層級、適時砍掉沒必要要的繁雜信息,一層層向上提供簡潔卻又完備的信息/接口,高層模塊纔不會被累死——KISS是最難也是最優的軟件設計方法,沒有之一。



總結:面向對象的好處實際就這麼兩點。
一是經過封裝明肯定義了何謂接口、何謂接口內部實現、何謂接口的外部調用者,使得你們各司其職,不得越界;
二是經過繼承+多態這種內置機制,在語言的層面支持歸一化的設計,並使得內行能夠從代碼自己看到這個設計——但,注意僅僅只是支持歸一化的設計。不懂如何作出這種設計的外行仍然不可能從瞎胡鬧的設計中獲得任何好處。


顯然,不用面嚮對象語言、不用class,同樣能夠作歸一化的設計(如老掉牙的泛文件概念、遊戲行業的一切皆精靈),同樣能夠封裝(經過定義模塊和接口),只是用面嚮對象語言能夠直接用語言元素顯式聲明這些而已;
而用了面嚮對象語言,滿篇都是class,並不等於就有了歸一化的設計。甚至,由於被這些花哨的東西迷惑,反而更加不知道什麼纔是設計。


二、人們覺得面向對象是什麼、以及所以製造出的悲劇以及鬧劇

誤解1、面嚮對象語言支持用語言元素直接聲明封裝性和接口兼容性,因此用面嚮對象語言寫出來的東西必定更清晰、易懂

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

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

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



誤解2、面向對象三要素是封裝、繼承、多態,因此只要是面嚮對象語言寫的程序,就必定「繼承」了語言的這三個優良特性

事實上,如前所述,封裝、繼承、多態只是語言層面對良好設計的支持,並不能導向良好的設計。
若是你的設計作不出真正的封裝性、不懂得何謂歸一化,那它用什麼寫出來都是垃圾。



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

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

什麼是真正的封裝

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

顯然不是
若是功能得不到知足、或者不曾預料到真正發生的需求變動,那麼你怎麼把一個成員變量/函數放到private裏面的,未來就必須怎麼把它挪出來。

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

真正的封裝是,通過深刻的思考,作出良好的抽象,給出「完整且最小」的接口,並使得內部細節能夠對外透明(注意:對外透明的意思是外部調用者能夠順利的獲得本身想要的任何功能,徹底意識不到內部細節的存在;而不是外部調用者爲了完成某個功能、卻被礙手礙腳的private聲明弄得火冒三丈;最終只能經過怪異、複雜甚至奇葩的機制,才能更改他必須關注的細節——並且這種訪問每每被實現的如此複雜,以致於稍不注意就會釀成大禍)。

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

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



一個典型的例子,就是C++的new和過於靈活的內存使用方式之間的耦合。
這個耦合就致使了new[]/delete[]、placement new/placement delete之類怪異的東西:這些東西必須成對使用,怎麼分配就必須怎麼釋放,任何錯誤搭配均可能致使程序崩潰——這是爲了兼容C、以及獲得更高執行效率的無奈之舉;但,它更是「抽象層次過於複雜,以致於沒法作出真正透明的設計」的典型案例:只能說,c++設計者是真正的大師,如此複雜的東西在他手裏,才僅僅付出瞭如此之小的代價。

(更準確點說,是new/delete和c++的其它語言元素之間是非正交的;因而當同時使用這些語言元素時,就不可避免的出現了彼此扯淡的現象。即new/delete這個操做對其它語言元素非透明:在c++的設計裏,是經過把new/delete分紅兩層,一是內存分配、二是在分配的內存上初始化,而後暴露這個分層細節,從而在最大程度上實現了封裝——但比之其它真正能彼此透明的語言元素間的關係,new/delete顯然過於複雜了)

這個案例,能夠很是直觀的說明「設計出真正對外透明的封裝」究竟會有多難。





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

顯然不是。

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

換句話說,若是不存在「須要不加區分的處理相似的一系列對象」的場合,那麼繼承不過是在裝X罷了。



封裝可應付需求變動、歸一化可簡化(類的使用者的)設計:以上,就是面向對象最最基本的好處。
——其它一切,都不過是在這兩個基礎上的衍生而已。


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


瞭解瞭如上兩點,那麼,很顯然:
一、若是你沒有作出好的抽象、甚至徹底不知道須要作好的抽象就忙着去「封裝」,那麼你只是在「封」和「裝」而已。
這種「封」和「裝」的行爲只會製造累贅和虛假的承諾;這些累贅以及必然會變卦的承諾,必然會爲將來的維護帶來更多的麻煩,甚至拖垮整個項目。

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

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

不只如此,請注意我強調了在當前應用場景下
這是由於,分類是一個極其主觀的東西,不存在普適的分類法

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

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



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

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


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





最具重量級的炸彈則是:正方形是否是一個矩形?它該不應從矩形繼承?若是能夠從矩形繼承,那麼什麼是正方形的長和寬?在這個設計裏,若是我修改了正方形的長,那麼這個正方形類還能不能叫正方形?它不該該天然轉換成長方形嗎?什麼語言能提供這種機制?

形成這顆炸彈的根本緣由是,面向對象中的「類」,和咱們平常語言乃至數學語言中的「類」根本就不是一碼事。

面向對象中的「類」,意思是「接口上兼容的一系列對象」,關注的只不過是接口的兼容性而已(可搜索 里氏代換);關鍵放在「可一視同仁的處理」上(學術上叫is-a)。

顯然,這個定義徹底是且只是爲了應付歸一化的須要。

這個定義常常和咱們平常對話中提到的類概念上重合;但,如前所述,根本上卻不折不扣是八杆子打不着的兩碼事。

就着生活經驗濫用「類」這個術語,甚至依靠這種粗淺認識去作設計,必然會致使出現各類各樣的誤差。這種設計實質上就是在胡說八道。
就着這種胡說八道來寫程序——有人以爲這種人能有好結果嗎?

——但,幾乎全部的面嚮對象語言、差很少全部的面向對象方法論,卻就是在鼓勵你們都這麼作,徹底沒有意識到它們的理論基礎有多麼的不牢靠。
——如此做死,焉能不死?!


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

——在真正明白何謂封裝、何謂歸一化以前,每一次寫下class,就在錯誤的道路上又多走了一步。
——設計真正須要關注的核心其實很簡單,就是封裝和歸一化。一個項目開始的時候,「class」寫的越早,就離這個核心越遠
——過去鼓吹的各類面向對象方法論、甚至某些語言自己,偏偏正是在慫恿甚至逼迫開發者儘量早、儘量多的寫class。



誤解4、只有面嚮對象語言寫的程序纔是面向對象的。

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

面向對象實際上是汲取以上各類成功設計的經驗才提出來的。

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

用面嚮對象語言寫程序,和一個程序的設計是面向對象的,二者是八杆子打不着的兩碼事。純C寫的linux kernel事實上比c++/java之類語言搞出來的大多數項目更加面向對象——只是絕大部分人都自覺得本身處處瞎寫class的麪條代碼纔是面向對象的正統、而死腦筋的linus搞的泛文件抽象不過是過程式思惟搞出來的老古董。

——這個誤解之深,甚至達到連wiki詞條裏面,都把OOP定義爲「用支持面向對象的語言寫程序」的程度。
——恐怕這也是沒有人說泛文件設計思想是個騙局、而面向對象卻被業界大牛們嚴厲抨擊的根本緣由了:真正的封裝、歸一化精髓被拋棄,浮於表面的、喋喋不休的class/設計模式卻成了」正統「!

借用樓下PeytonCai朋友的連接:
名家吐槽:面向對象編程從骨子裏就有問題

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

總結: 面向對象實際上是對過去成功的設計經驗的總結。但那些成功的設計,不是由於用了封裝/歸一化而成功,而是切合本身面對的問題,給出了恰到好處的設計

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

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

只有通過真正的深思熟慮,纔有可能作到KISS。


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

一言以蔽之:沒有銀彈。任何寄但願於靠着某種「高大上」的技術——不管是面向對象、數據驅動、消息驅動仍是lambda、協程等等等等——就能一勞永逸的使得任何現實問題「迎刃而解」的企圖都是註定要失敗的,都不過是外行的意淫而已;靠意淫來作設計,不掉溝裏纔怪。

想要作出KISS的方案,就必須對面對的問題有透徹的瞭解,有足夠的經驗和能力,並通過深思熟慮,這才能作出簡潔的抽象:至於最終的抽象是面向對象的、面向過程的仍是數據驅動/消息驅動的,甚至是大雜燴的,都是可能的。只要這個設計能作到最重要、也是最難的KISS,那它就是個好設計。

的確有成功的經驗、正確/合理的方向:技術無罪,但,沒有銀彈。設計模式

 

 

references:緩存

http://www.zhihu.com/question/20275578網絡

相關文章
相關標籤/搜索