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

找不到星期八


這是一個非常好的問題,作為一名IT從業者,同時也出版過編程書籍,所以我來說說我的看法。

首先,面向對象編程依然是當前程序開發的主要方式之一,不論是Java、C#還是Python都是比較典型的面向對象編程語言,而且從當前的軟件開發體系來看,未來很長一段時間內,面向對象編程都將依然是程序開發的主流開發方式之一。

說到面向對象編程的弊端,可以從以下三個方面來進行討論:

第一:過度抽象。“抽象”是面向對象編程的核心,通過“抽象”可以構建出非常複雜的軟件產品,所以面向對象編程語言非常適合一些大型軟件項目的開發,實際上面向對象編程主要就是為了適應大規模軟件開發場景。

在使用面向對象編程的過程中,抽象的程度是很難把握的,這也是導致面向對象編程複雜度較高的重要原因之一,抽象不足和過度抽象都是面向對象編程中非常常見的問題,抽象不足會導致軟件的擴展性大幅下降,而抽象過度又會導致軟件複雜度全面上升。

第二:模塊化。模塊化一直是面向對象編程所面臨的較大挑戰之一,面向對象編程由於自身的抽象程度比較高,所以在進行模塊化的過程中就會遇到各種障礙,比如Java的模塊化問題一直在困擾開發人員。為了解決面向對象的模塊化問題,各種開發框架被提出,比如OSGI就是比較典型的代表,實際上AOP編程方式的提出,一個重要的原因也是為了彌補面嚮對象語言的模塊化問題。

第三:安全。安全永遠是面向對象編程需要重點關注的核心問題之一,雖然不同的編程語言對於安全都有比較全面的考慮,但是由於面向對象編程語言自身的開放性,安全方案本身的複雜程度也非常高,比如Java的安全機制就非常複雜,而這個複雜的規則背後,往往會限制編程語言的應用邊界。

我從事互聯網行業多年,目前也在帶計算機專業的研究生,主要的研究方向集中在大數據和人工智能領域,我會陸續寫一些關於互聯網技術方面的文章,感興趣的朋友可以關注我,相信一定會有所收穫。

如果有互聯網、大數據、人工智能等方面的問題,或者是考研方面的問題,都可以在評論區留言,或者私信我!


IT人劉俊明


面向對象編程(以下簡稱OOP),內存存取成為現代計算機性能的重要瓶頸,OOP編程有可能緩存不友好(cache friendly),導致有時候並不能發揮硬件最佳性能。原因如下:

1. 過度封裝

使用OOP時,會把一些複雜的問題分拆抽象成較簡單的獨立對象,通過對象的互相調用去實現方案。但是,由於對象包含自己封裝的數據,一個問題的數據集會被分散在不同的內存區域。互相調用時很可能會出現數據的cache miss的情況。

2. 多態

在C++的一般的多態實現中,會使用到虛函數表。虛函數表是通過加入一次間接層來實現動態派送。但在調用的時候需要讀取虛函數表,增加cache miss的可能性。基本上要支持動態派送,無論用虛函數表、函數指針都會形成這個問題,但如果類的數目極多,把函數指針如果和數據放在一起有時候可放緩問題。

3. 數據佈局

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

struct Particle {

Vector3 position;

Vector4 velocity;

Vector4 color;

float age;

// ...

};

即使不使用多態,我們幾乎不加思索地會使用這種數據佈局方式。我們通常會以為,由於各個成員變量都緊湊地放置在一起,這種數據佈局通常對緩存友好。然而,實際上,我們需要考慮數據的存取模式(access pattern)。

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

for (Particle* p = begin; p != end; ++p)

p->position += p->velocity * dt;

// 或 p->SimulateMotion(dt);

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

如果採用傳統的OOP編程範式及實現方式,數據佈局的問題幾乎沒有解決方案。所以在[1]裡,作者提出,在某些情況下,應該放棄OOP方式,以數據的存取及佈局為編程的考慮重點,稱作面向數據編程(data-oriented programming, DOP)。



住建行業大數據


做了好多年面向對象編程,最近做嵌入式軟件開發,純C語言,突然發現了面向過程編程,在這些方面比面向對象更有優勢:

1. 讓您能夠自頂向下,完全掌握軟件的架構;

2. 軟件的每一個細節,您都清楚;

3. 生成的二進制文件非常緊湊,運行效率比JAVA和C++都要高。

但是面向對象有個非常大的好處,就是軟件開發速度快,而且符合人的邏輯思維方式。


編程實踐


面相對象,其實是一套方法論體系。在計算機編程領域,通過這一套方法論來模擬真實世界。可以說,面向對象吸取了人類至今所有的哲學成果,是以“上帝”視角來觀察、模擬世界的。

事實上面向對象主要在面向對象分析、設計這兩塊,而面向對象編程只是最後的實現。

如果說面向對象的優點,可以說目前所有技術產品都有面向對象的影子,這種方法論已經稱為IT包括互聯網領域最基礎的思維方式,其影響無時無處不在。

要說它有哪些缺點,這個還真不能說是缺點,只能和麵向過程方法進行比較。相較於面向過程,面向對象相對更為複雜一些,所需的代碼量更大,CPU的計算量也更大。但這些代價都是有明確好處的。


春衫不薄


繼承層次達到五層以上時,如果還有多繼承,對代碼的理解非常不利,帶來了很大困難。特別是又加了重載和虛擬方法的話,就更分散了。這一點在看spring源代碼的時候非常明顯,即使spring的封裝做得非常好,閱讀起來文件間跳轉次數還是非常多。這是第一個弊端,就是繼承層次的問題。第二個弊端就是,泛型帶來的難度,有一些通用算法和數據結構,泛型化是應該的,但是也帶來了使用和編寫上的難度。這一點以cxx為代表,有時候編譯時泛型報個錯,一屏幕都裝不下。第三個就是類太多,類和方法的結合緊密,以至於出現了aop這種方法,用於解耦。想要新增修改一個功能點,有時候需要修改很多類,因為是面向對象的,不是面向過程的,所以要根據流程圖,去抓出所有涉及的對象,然後修改成員和方法。go語言的鴨子類型一定程度上緩解了這種情況。


天一閣圖書管理員


面向對象編程是一種處理複雜問題的設計工具,本身沒有什麼好壞之分,只有用的好壞之分。但面向對象的問題在於長期以來的技術環境、編程語言、一些工具的推廣、培訓和教育都大大的過分樂觀的強調了面向對象編程本身可以帶來的好處。以至於很多學習編程的人都深深的相信“只要用了面向對象編程(以及基於其基礎之上的的一系列設計模式、規範、工具、框架),就能得到非常容易維護、可以複用、明晰可理解的代碼“。


但,這並不是真的。

如果你經歷過很多,就會發現“只要如何如何,就一定能如何如何”這個提法一旦出現,基本上就不靠譜,不管是編程還是別的什麼事情。

在大量的場景中,可以偏執的認為“萬物皆對象”(或者萬物皆別的什麼),但是哲學上的單純並不一定能讓現實中的工程變得更“好”。如果說非得有個“萬物皆XX”,那麼這個XX八成就是根據眾多需求綜合到一起的“折衷”。

簡單從工程講的話,如果程序(或者說工作)是一次性的,那麼怎麼寫得快,能work就怎麼來。這個相對好理解。但是,如果程序是要長期維護的,那麼如何管理其複雜性是核心的問題。而管理複雜性的要點在於

  • 讓事情本身變得簡單。這說白了就是砍需求,研發和PM之間要經常溝通去避免nice to have的需求變動帶來的程序複雜性的劇烈變化(比如一個1對1的實體關係,需求變動一點就變成了麻煩的多的“有時1對1,有時1對多”的混合關係)。
  • 運用隔離的手段將複雜性拆解為互相影響很小的單元。一個單元對外只暴露一個簡單的“接口”,隱藏內部複雜性。這就是“抽象”或者“封裝“的力量。但是問題在於,這個抽象本身是否做的合適是由於問題決定的,而不是代碼本身決定的。

即便是抽象,也有很多種做法。可以定義一組接口,這個接口是一組函數、一組服務的RPC還是一個class的public method都可以根據實際情況商討。面向對象只是這裡面其中一種做法而已。一個想要把程序編好的人,需要注重的是理解問題,然後嘗試做出幾種不同的抽象,評估各自優缺點後得到一個當時可行解的能力。而現有的大環境、教育體系,沒有那麼多真實的、複雜的案例,只能用一些簡單的sample code來教授。並且在說明問題本身時,簡化問題本身,而突出代碼設計的“模式”。這就好像是在用視頻教人游泳一樣。學習者自己需要認識到這些培訓只是個參考,玩真的還是要到項目裡去體會。

即便是用面向對象做抽象也會有問題。很多時候,面向對象編程並不是一種好的“抽象”。如果抽象做得好,透過抽象出來的“接口”就可以輕易的使用這個系統。這時“大量的複雜性”被隱藏到接口後的實現裡。這就像是你看電視從來都不需要拆開殼子看裡面液晶屏幕和視頻信號的轉換,只需要知道【電源】、【調臺】、【調音量】就能用。一個抽象做得好,往往要“deep”,隱藏足夠的複雜度。而面向對象的文化/教育往往會鼓勵程序員做很多無意義的,無性價比的抽象。看看有些代碼裡完全不知所云的adaptor,factory,builder等就是這種做法的產物。

此外,在大量使用繼承作為設計方法時,也沒有起到任何實質的隔離作用。如果你嘗試擴展一個繼承體系,往往需要了解整個繼承體系才能寫對代碼——這時,複雜性並沒有被隱藏起來。你也許只是代碼寫的少了而已。對於這種複雜度沒有降低,編寫代碼只是寫的少,但是要看懂還是得結合整個體系才能做到的方式,不是抽象,是“壓縮”。壓縮只能少寫代碼,卻會讓系統更難以理解了。

也許不太容易理解壓縮在這裡意思。比如在一段被壓縮的數據中有3個bytes是“A”,“1”, “8”。但是他們的意思可能是A連續出現18次,也許是A1連續出現8次。至於到底是哪個意思,必須從頭讀所有的數據才能弄明白。編碼也是這個道理。

再說說類型本身。一些面向對象編碼對類型的定義要求的比較嚴格。其本質假設是“如果一個Object的類型是XXXX”,則其行為模式必然是“YYYY”。但現實當中,一個Object的行為模式不光與他的類型有關,還與這個Object“如何被使用”有關。比方說,一個User的Object,如果是用戶自己看自己,就可以登陸、登出,修改暱稱;如果是其他普通用戶看,就只能看看看暱稱和頭像;如果是管理員來操作,可以reset密碼、註銷或者踢出登陸。這時就得界定一個Scope,來說明現在的User到底是哪個scope的User。DDD的一些理念就源自於此——找到某個上下文的某個實體概念,不能有歧義。但是即便不用DDD,也必須用各種變通的手段,把“如何用”的信息與類型信息結合到一起來實現邏輯。很鬱悶的是,這個“如何用”完全沒有章法,可能是“iOS App登陸“,也可能是“第一次下單時”,或者是“系統處於降級狀態”時。你永遠也猜不到下一次可能會有個什麼條件是要納入到上下文的。大家都知道大量用if不好,容易讓代碼變成麻花,無法維護。但面向對象編程本身沒解決這個問題。很多文章提出面向對象某個模式可以少寫if,讓代碼容易維護。但是這其實是建立在那個問題的上下文已經明確的基礎之上。上下文易變的問題沒有解決,換一個上下文,招數便不靈了,到時還得處理一坨“模式代碼”,非常噁心。

最後,面向對象會傾向於將不同的代碼抽象為不同相互作用的Object,但是有一些現實因素會讓這麼面向對象得到非常不理想的效果:

  • 安全 - 如果你的代碼要求非常安全,那麼所有的Object都要耦合安全控制的代碼;要不就是在一層對外的接口之前攔截一道處理安全問題,內部Object都無視安全問題。這也就相當於放棄了一部分的安全性。
  • 性能 - 如果強調性能的話,是要儘量減少隔離的層次的。無論抽象如何做,只要隔離發生,就要經歷一次轉換以及相應的性能損耗。比如早期的Hibernate不支持“bulk insert”和“bulk update”,只能逼著程序員做for loop IO;而native的sql卻可以輕易辦到。在每多一次IO都很傷的場景下,這種隔離只能把事情做的更糟。
  • 數據為中心 - 很多業務場景都是以數據為中心。也就是說DB裡的那坨數據是唯一的truth。在代碼層面做的只是為處理數據更加方便。這時做的很多抽象意義不大。比如你可以在ORM層強制聲明讀取出來的一個數據少了某個字段是invalid的。但是你沒法阻止你的第三方數據提供商源給你invalid的數據。對Invalid數據的處理遠不是一個Annotation就能搞定的,必須引入複雜的業務流程。
  • 靈活性和成本 - 每次做某種抽象都意味著對一個系統“要做某種變化的能力做出優化”,但是同時,也就意味著或多或少對其他種變化適應性做“劣化“。如果系統變化的方向和預期的不一致,那麼浪費掉的工作不說,為了再次調整設計方向的代價也會相當的大。這種情況比比皆是。

總結下,我希望所有的程序員都要理解自己的工作的最終目的是幹什麼的,並且活用自己所能用到的一切工具來達成自己的目標。不要在各種編程範式裡迷了路。如果是初學編程的人,我衷心的希望你的編程課程講授的是解決一些實際的問題,多瞭解業務,多嘗試對業務的變動作出合理和準確的預。不要過早的接觸高層的思想和哲學層面的問題——一個小孩看《紅樓夢》又能真的看懂多少呢。


C語言基礎


比較容易分心[看]


喬維001


您好!非常高興回答您的問題!

面向對象編程是一種處理複雜問題的設計工具,本身沒有什麼好壞之分,只有用的好壞之分。但面向對象的問題在於長期以來的技術環境、編程語言、一些工具的推廣、培訓和教育都大大的過分樂觀的強調了面向對象編程本身可以帶來的好處。以至於很多學習編程的人都深深的相信“只要用了面向對象編程(以及基於其基礎之上的的一系列設計模式、規範、工具、框架),就能得到非常容易維護、可以複用、明晰可理解的代碼“。

但,這並不是真的。

如果你經歷過很多,就會發現“只要如何如何,就一定能如何如何”這個提法一旦出現,基本上就不靠譜,不管是編程還是別的什麼事情。

在大量的場景中,可以偏執的認為“萬物皆對象”(或者萬物皆別的什麼),但是哲學上的單純並不一定能讓現實中的工程變得更“好”。如果說非得有個“萬物皆XX”,那麼這個XX八成就是根據眾多需求綜合到一起的“折衷”。

簡單從工程講的話,如果程序(或者說工作)是一次性的,那麼怎麼寫得快,能work就怎麼來。這個相對好理解。但是,如果程序是要長期維護的,那麼如何管理其複雜性是核心的問題。而管理複雜性的要點在於

讓事情本身變得簡單。這說白了就是砍需求,研發和PM之間要經常溝通去避免nice to have的需求變動帶來的程序複雜性的劇烈變化(比如一個1對1的實體關係,需求變動一點就變成了麻煩的多的“有時1對1,有時1對多”的混合關係)。

運用隔離的手段將複雜性拆解為互相影響很小的單元。一個單元對外只暴露一個簡單的“接口”,隱藏內部複雜性。這就是“抽象”或者“封裝“的力量。但是問題在於,這個抽象本身是否做的合適是由於問題決定的,而不是代碼本身決定的。

即便是抽象,也有很多種做法。可以定義一組接口,這個接口是一組函數、一組服務的RPC還是一個class的public method都可以根據實際情況商討。面向對象只是這裡面其中一種做法而已。一個想要把程序編好的人,需要注重的是理解問題,然後嘗試做出幾種不同的抽象,評估各自優缺點後得到一個當時可行解的能力。而現有的大環境、教育體系,沒有那麼多真實的、複雜的案例,只能用一些簡單的sample code來教授。並且在說明問題本身時,簡化問題本身,而突出代碼設計的“模式”。這就好像是在用視頻教人游泳一樣。學習者自己需要認識到這些培訓只是個參考,玩真的還是要到項目裡去體會。

即便是用面向對象做抽象也會有問題。很多時候,面向對象編程並不是一種好的“抽象”。如果抽象做得好,透過抽象出來的“接口”就可以輕易的使用這個系統。這時“大量的複雜性”被隱藏到接口後的實現裡。這就像是你看電視從來都不需要拆開殼子看裡面液晶屏幕和視頻信號的轉換,只需要知道【電源】、【調臺】、【調音量】就能用。一個抽象做得好,往往要“deep”,隱藏足夠的複雜度。而面向對象的文化/教育往往會鼓勵程序員做很多無意義的,無性價比的抽象。看看有些代碼裡完全不知所云的adaptor,factory,builder等就是這種做法的產物。

此外,在大量使用繼承作為設計方法時,也沒有起到任何實質的隔離作用。如果你嘗試擴展一個繼承體系,往往需要了解整個繼承體系才能寫對代碼——這時,複雜性並沒有被隱藏起來。你也許只是代碼寫的少了而已。對於這種複雜度沒有降低,編寫代碼只是寫的少,但是要看懂還是得結合整個體系才能做到的方式,不是抽象,是“壓縮”。壓縮只能少寫代碼,卻會讓系統更難以理解了。

也許不太容易理解壓縮在這裡意思。比如在一段被壓縮的數據中有3個bytes是“A”,“1”, “8”。但是他們的意思可能是A連續出現18次,也許是A1連續出現8次。至於到底是哪個意思,必須從頭讀所有的數據才能弄明白。編碼也是這個道理。

再說說類型本身。一些面向對象編碼對類型的定義要求的比較嚴格。其本質假設是“如果一個Object的類型是XXXX”,則其行為模式必然是“YYYY”。但現實當中,一個Object的行為模式不光與他的類型有關,還與這個Object“如何被使用”有關。比方說,一個User的Object,如果是用戶自己看自己,就可以登陸、登出,修改暱稱;如果是其他普通用戶看,就只能看看看暱稱和頭像;如果是管理員來操作,可以reset密碼、註銷或者踢出登陸。這時就得界定一個Scope,來說明現在的User到底是哪個scope的User。DDD的一些理念就源自於此——找到某個上下文的某個實體概念,不能有歧義。但是即便不用DDD,也必須用各種變通的手段,把“如何用”的信息與類型信息結合到一起來實現邏輯。很鬱悶的是,這個“如何用”完全沒有章法,可能是“iOS App登陸“,也可能是“第一次下單時”,或者是“系統處於降級狀態”時。你永遠也猜不到下一次可能會有個什麼條件是要納入到上下文的。大家都知道大量用if不好,容易讓代碼變成麻花,無法維護。但面向對象編程本身沒解決這個問題。很多文章提出面向對象某個模式可以少寫if,讓代碼容易維護。但是這其實是建立在那個問題的上下文已經明確的基礎之上。上下文易變的問題沒有解決,換一個上下文,招數便不靈了,到時還得處理一坨“模式代碼”,非常噁心。

最後,面向對象會傾向於將不同的代碼抽象為不同相互作用的Object,但是有一些現實因素會讓這麼面向對象得到非常不理想的效果:

安全 - 如果你的代碼要求非常安全,那麼所有的Object都要耦合安全控制的代碼;要不就是在一層對外的接口之前攔截一道處理安全問題,內部Object都無視安全問題。這也就相當於放棄了一部分的安全性。

性能 - 如果強調性能的話,是要儘量減少隔離的層次的。無論抽象如何做,只要隔離發生,就要經歷一次轉換以及相應的性能損耗。比如早期的Hibernate不支持“bulk insert”和“bulk update”,只能逼著程序員做for loop IO;而native的sql卻可以輕易辦到。在每多一次IO都很傷的場景下,這種隔離只能把事情做的更糟。

數據為中心 - 很多業務場景都是以數據為中心。也就是說DB裡的那坨數據是唯一的truth。在代碼層面做的只是為處理數據更加方便。這時做的很多抽象意義不大。比如你可以在ORM層強制聲明讀取出來的一個數據少了某個字段是invalid的。但是你沒法阻止你的第三方數據提供商源給你invalid的數據。對Invalid數據的處理遠不是一個Annotation就能搞定的,必須引入複雜的業務流程。

靈活性和成本 - 每次做某種抽象都意味著對一個系統“要做某種變化的能力做出優化”,但是同時,也就意味著或多或少對其他種變化適應性做“劣化“。如果系統變化的方向和預期的不一致,那麼浪費掉的工作不說,為了再次調整設計方向的代價也會相當的大。這種情況比比皆是。

總結下,我希望所有的程序員都要理解自己的工作的最終目的是幹什麼的,並且活用自己所能用到的一切工具來達成自己的目標。不要在各種編程範式裡迷了路。


國際科技資訊



分享到:


相關文章: