面向對象編程是否是從根本上反模塊化且反並行的?為什麼?

更新:Quaro的原問題已改為:Is object-oriented programming both anti-modular and anti-parallel by its very nature? Why or why not?
https://www.quora.com/Object-Oriented-Programming/Is-object-oriented-programming-both-anti-modular-and-anti-parallel-by-its-very-nature-Why-or-why-not

問題起源:卡內基梅隆大學計算機系刪除基礎課程中的面向對象編程課程。鏈接:Teaching FP to Freshmen


這個問題的根本在於 OOP 是基於狀態的。每個對象都維護著自己的狀態,暴露給外界的是一些可以改變對象狀態的方法。一個對象的狀態里可以有對其他對象的引用,一個對象的方法也可以調用其他對象的方法來改變其他對象的狀態,所以這些狀態還是關聯的。很多人提到的線程安全與效率的取捨之類其實都是細枝末節,即使是有辦法把所有方法都能高效地實現並且全都是線程安全的,只要狀態存在,狀態帶來的問題就存在。在一個複雜的並發系統中,你調用 foo.bar(42),幾個指令之後再調用 foo.bar(42),兩次調用的結果很可能是不一樣的,因為在這中間 foo 的狀態可能已經改變了,或者 foo 引用的某個對象的狀態可能改變了,不去看 bar() 的實現根本不知道結果依賴於什麼。同樣一段程序多次運行因為時序的不確定性可能結果也不一樣。不管 OOP 也好,過去說的過程式編程也好,理論基礎都是圖靈機模型,而圖靈機就是依靠對狀態的記錄和改變來進行運算的。圖靈機里的紙帶和狀態寄存器用來記錄狀態,而讀寫頭用來訪問和改變狀態。想像一下一個並行的圖靈機(多個有獨立狀態寄存器和不同速度的讀寫頭加上一條共享的紙帶)就不難理解在這個模型下並髮帶來的複雜度。

而目前很多人因為並發的需求所崇尚的函數式編程是基於 Lambda Calculus 的計算模型。計算由層層嵌套的函數調用完成;每個函數調用的結果只依賴於函數和它的參數。如果 f(4, 5) = 10,那麼無論你在什麼時候調用 f(4, 5),它的結果都是 10。相對而言,這是一個比較乾淨,比較容易推理和確保正確性的模型。OOP 的程序通常有很多隱藏的數據依賴,函數式編程把這些數據依賴都明確化了。

但函數式編程最大的一個問題是,函數是一個數學抽象,在現實世界中不存在,它必須被模擬出來。目前為止被廣泛使用的計算機還是基於圖靈機模型,計算機的寄存器、緩存、內存就是用來記錄狀態的。要真正懂得程序設計,必須知道沒有狀態的函數是如何在充滿狀態的計算機上實現的,所以還是繞不開非函數式的編程。另外絕大部分的函數式程序設計語言都不是純函數式的,出於實用性考慮都夾雜著其他語言的一些特點,並沒有完全排斥狀態。Haskell 號稱純函數式語言,用 Monad 來抽象狀態,理論上可以自圓其說,但在實際使用中其實還是帶來了很多不便(於是又發明了 Monad Transformer...)。

從某種程度上說,狀態是繞不過去的,畢竟人感知到的宏觀世界就是由各種各樣有各自狀態的對象構成。函數式編程可以幫我們避免很多用其他方式容易犯的錯誤,在很多情況下寫出更高質量的程序,但並髮帶來的複雜度並不會從根本上消失。各種編程風格一定是互相影響推動程序設計語言的進化,沒有絕對的好壞,從 C++ 和 Java 最新標準里引入的函數式方面的功能就很容易看出這一點。比較有意思的是,OOP 最早是在 LISP 里實現的,而 LISP 也被很多人看做函數式編程的起始。同樣,好的程序員也會根據具體情況使用合適的編程風格。

OOP 不失為一種比較容易理解的在計算機程序里對現實世界的抽象,在很多場合的應用是非常成功的,至少我沒發現以圖形用戶界面為中心的程序里有比 OOP 更行之有效的抽象方式。把 OOP 從程序員的教育中去掉過於片面和激進了。如果是從基礎課程調整為選修課程則是可以理解的,我上本科時記得也是那樣設置的。


SML(1990)/OCaml(1996):OO既反模塊化又反並行,你們快快受死,FP才是正道!

Emerald(1983, OOP):我被弄出來就是搞分散式OO的,我能一邊跑一邊把Object從一台機器移到另一台(不是同種架構也可以,移個process過去再移回來還不會加interpretation overhead),同時還可以接著發消息不會丟,話說OCaml你啥時候去掉Global Interpreter Lock支持多線程啊[0]

Simula(1965, OOP):啥?我Simulation就是基於Process的啊[1]

Smalltalk(1972, OOP):Module。。。我記得我比你早得多吧[2]

CLU(1975, OOP):他說的不是你這種Module了,他們那種更像我的Abstract Data Type,儘管比我的更強大[-1],而且ML也比我早點,是1973的[-1]

Scala(2004, OOP):沒事,你像我這樣,Module跟Object,Open跟Inheritance,Signature Matching跟Subtyping混在一起,也能風生水起了,能用JVM生態圈,還有一定的Dependent Type,多歡樂~[3]

Haskell(1990, FP):我們中出了一個叛徒!我早就知道Scala你不是我們的人!OCaml你也好不到那去,名字里就帶個Object,為啥大家都不能學我一樣,Haskell守國門,Miranda死社稷,不和親,不賠償,保留純潔性呢= =

Algol 60(1960, Structure Programming):你是說像我這樣區分純跟不純的部分?[4]

Object Calculus(1994, OO):又或者學學我,乾脆全部是純的[5]

C(1972, SP):好歹我又純又停機啊,你這跑著跑著就不停機,很危險啊[6]

GCL(1975, SP):哼,不純又怎麼樣了,non determinsm本來就跟assignment可以一起弄,我一直都是這麼搞的[7],反觀你HS把東西塞到IO或啥cloud haskell里,equational reasoning就得失效,這次fast and loose reasoning也救不了你了[8]

Lisp(1958-???, ???):不好意思,我1961就開始搞證明了[9],比你1967早得多[9],Conditional Expression,Recursion都是我發明的,Lambda Calculus(LC)是我引進的,我輩分比你大得多。不是我針對你,而是說。。。

Fortran(1957, ???),Cobol(1959, ???):好像有人在排資論輩呢,快去吃瓜

Plankalkül(1948, SP):什麼回事,還有Conditional是我早就有的[-1]

Lisp1.5(1958, Lisp):你這算什麼啊,連開放實現都沒有的語言

Algol 60:你好意思,你58的時候連文檔都沒有,要一直到1960才拋頭露面呢

ISWIM(1964, FP):LC你當初就會個λ,看著漂亮就拿起來[10],引入LC是我的功勞[11]

Algol 58(1958, SP):怪不得當初他是dynamic scope,沒搞對,害得我給他修[-1]

Actor(1973, OO):沒事,後來學了點我以後出了Scheme,終於好了[-1]

Smalltalk:想不到當年我抄完Logo跟Lisp1.5以後,竟然被Lisp抄回去了,正所謂英雄吸英雄啊,不過要什麼勞子的macro何用,我全是Object,Object是fexpr,自然比macro強[2](Actor借鑒了Smalltalk跟Simula, Smalltalk借鑒了Simula跟Lisp,Simula借鑒了Algol, Algol借鑒了Lisp, Lisp又借鑒了Algol跟Actor)

Prolog(1972, ???):萬人抄完以後抄萬人,Lisp這關係真是亂倫啊。。。不過看來比誰先誰後有點亂,有想法,有實現,有公開的標準/文檔/實現,不能混為一談啊[12]

Algol 60:不過話說回來,Lambda Calculus我們也資瓷啊,Landin搞出我跟LC的一一對應來了[13],最早denotational semantic也是給SP的[14],你看,還有範疇呢,這還有[15]

Scala:裡面還有subtyping呢,我們的確不一定需要用Object Calculus來建模,Lambda Calculus[16]很多時候就夠了,還可以用範疇論建模~Reynold他也搞過OO啊[17]

SML:不過真是好玩,Reynold他還給我們弄過Parametricity呢,不用說,也是推範疇[18]

Scala:我還沒說完呢,[16]裡面開頭就講了,"This constitutes a semantic basis for the unification of functional and object-oriented programming.",看來你們我們緣分挺深啊

SML:那篇文章的作者,Luca,還大力參與了ML系Module的設計呢[19],妙極,妙極

APL(1964, ???):什麼你們我們的,這些派別之爭,全是虛幻,反觀Lisp,不是想像中的鐵板一塊,啥CL, r6rs, r7rs, racket, logo, 還有各零星實現,分裂得何其嚴重。。。

Erlang(1986, ???):倒不如大家都忘了自己是啥幫派,一起去吃火鍋,姐姐我請客&> &<

[-1]: https://www.wikipedia.org/

[0]: The Development of the Emerald Programming Language

[1]: INTRODUCTION TO SIMULA

[2]: The Early History Of Smalltalk

[3]: Dependent Object Types

[4]: Revised Report on the Algorithmic Language ALGOL 60

[5]: A Theory of Objects

[6]: The C Language is purely functional

[7]: A Discipline of Programming

[8]: Fast and Loose Reasoning is Morally Correct

[9]: A BASIS FOR A MATHEMATICAL THEORY OF COMPUTATION

[10]: History of Lisp

[11]: The mechanical evaluation of expressions

[12]: Programming Languages: History and Future

[13]: A correspondence between ALGOL 60 and Church"s Lambda-notations

[14]: Towards a Mathematical Semantics for Computer Languages

[15]: The essence of ALGOL

[16]: A Semantics of Multiple Inheritance

[17]: Using category theory to design implicit conversions and generic operators

[18]: Types, Abstraction and Parametric Polymorphism

[19]: Luca Cardelli and the Early Evolution of ML

註:編程語言的人格純屬我因劇情需要捏造,而不是寫Erlang的就土豪喜歡吃火鍋這樣的。

然後,篇幅有限,很多東西只能一筆帶過,實際情況比這複雜得多,比如說Haskell的effect既在type上,也是first class的,其他幾個抬杠的語言全沒有。換言之,如果想真正明白過去/現在是怎麼樣的,還是得好好爬這些reference。不要見得風就是雨,說啥我瘋狂抹黑Lisp之類,我只是剛好要個機械降神(霧


這裡的應該理解為,面向對象編程不適合模塊化、並行化。或者說OOP必然在模塊化和並行化方面遇到一些障礙。

但我覺得這並不是說OOP有什麼不好,因為任何一種編程範式都有自己的局限。

這事兒不能去較真成OOP不能模塊化和並行化。就像你不能去較真C語言沒辦法OOP一樣,你願意的話可以自己搞出很多特性比如說私有(靜態局部變數)、多態(函數指針)之類。這裡的的意思就是不適合,不好用的意思。

說起來,現在的編程語言都是多範式的,OOP不適用於這,不適用於那其實也沒啥問題。適合的場景使用適合的範式。

OOP作為一門編程的基礎課程被刪除我還是比較贊成的,OO的優勢桌面應用主要在於GUI,企業開發主要在於領域建模。而這些都與編程的基礎課程沒有什麼太多的關係,換言之OOD是OO的骨,先學會設計和建模再運用OOP來實現這些理念才是最好的,而設計建模顯然又不是基礎課程的領域。所以不如在基礎課程中廢除OOP。
照貓畫虎不如先練好本領去畫真老虎,否則只能反類犬。

有些人用物理定律來打比方,是不合適的。
OO是為了解決特定問題而被發明的範式,所以不去了解那些特定的問題,把OOP當作基礎教學,是有問題的。就像繞開光速不變去談狹義相對論一樣,只能聽個雲山霧繞。事實上OO這個範式是非常年輕的,與大家所了解的不同的是,目前看起來是突然興起的函數式啥的才是真正的元老和基礎中的基礎。

其實我倒是覺得及早的讓學生接觸多範式語言,認識到編程不是一招鮮吃遍天的,會比較好,就這一點上,我覺得C#比C更適合用來教學。從這個反字裡面我倒是嗅到了萬能範式的味道,好像存在一種萬能的編程方式,學會了就能快速NX的開發各種程序,這樣說來的話,也許不是個好事。


順便說一下OOP怎麼去做高並發。

事實上OOP做高並發是很正常而且一點兒也不恐怖的事情,雖然說函數式在多線程的情況下會有先天的優勢,但是上面一群人把OOP的多線程未免也抹的太黑了。

首先拋棄對象的狀態是扯淡的,這就不是OOP了。對象因有狀態而成為對象,否則就是個函數集(Function Set),寫出來的代碼不叫做OOP,而是FP。並不是說,用OO的語言寫出來的代碼就都是OOP的

其次,由於狀態的存在,所以所有的實例方法都應當假定是線程不安全的。說白了不必看源代碼,如果一個方法不依賴於對象的狀態,直接靜態好了(否則就是糟糕的設計)。僅為了重寫而專門寫成實例方法的不依賴於對象狀態的做法也是dirty的。

所以,OOP的所有方法都是基於狀態的,這個問題似乎無解了?


怎麼可能!
那互聯網還開發個球啊。
所以OOP中多線程的方案就是不要跨線程共享非只讀對象,就這麼簡單,是不怎麼優美,但還能用。把狀態局限在一個線程內,就啥事兒也沒有了。
什麼?這樣你就只能開一個線程了,那你幹嗎非要OOP來的。


編程語言本質上來說是為了程序員服務而不是機器,首要是讓程序員正確描述出邏輯,次要的是讓機器能比較容易執行。一般程序員會覺得OO比FP更容易理解。
應用程序可以有不同的層次,底層封閉細節之後完全可以體現出另一種模式,比如一段面向對象的程序對外只暴露一個純函數介面,所有對象的構造和析構都在這個函數中完成,這也沒有什麼不可以的。並不存在所有邏輯都要用一種方法來寫否則就怎麼怎麼樣這種事,機器是死的人是活的,應該靠主觀能動性來保證代碼質量,而不是死框框。


說實話,用面向對象的手法來寫並發程序真的是難爆了。我最近在www.gaclib.net裡面添加一個「從EBNF自動生成智能提示」的演算法(寫得差不多了,代碼一直在http://gac.codeplex.com 上更新),就遇到了這個問題。

我們都知道,唯一可以給UI建模建的好用起來又爽的也就只有面向對象了,但是寫智能提示又怎能不並發呢,於是我只好把OO和Actor這兩種風格在C++裡面混合了起來。幸好OO是鼓吹封裝的,於是我把那些不需要給別人操作的Actor們也裝在了另一個大類裡面,用戶只需要單線程的操作這個類就可以了。多線程的部分就在這個類的內部。

於是當你操作不當的時候,不可避免的會遇到racing condition。什麼是racing condition呢?就是多個函數同時運行之後的結果,跟他們挨個運行之後的結果不一樣。你可以想像一下我同時對一個變數+1兩次,如果你加的方法不對,有時候就會發生racing condition——結果只加了1,不是2。

於是你就會慢慢明白OO跟並發到底抵觸在哪了。防止racing condition,寫出一個正確的並發程序,是需要你小心翼翼(對大部分流行語言都有是,除了Haskell)的處理一些狀態變數,而OO卻是希望你通過釋放介面來阻止你對狀態變數的認識(嗯,@王垠 說得對,面向對象他媽就是個禍害。而且禍害就算了,面向對象本來應該用interface oriented programming(認真學習COM就會得到這一點,但是這裡的interface並不一定需要是語法上的interface。所以說很多不學無術的人除了噴COM難用以外根本沒辦法從她身上學到任何東西,這些程序員都是a piece of shit)來做,結果那幫人竟然鼓吹什麼is-a啊,has-a這些傷天害理的知識,絕對是計算機科學教育的第一大禍害)。於是你現在開始寫代碼的時候就會遇到這個問題了:

這個類的這個方法到底能不能並發呢?

呵呵,如果沒有文檔,也沒有源代碼,你永遠也回答不出這個問題。

這是不是意味著我們必須把每一個method都做成線程安全的呢?這當然不是,線程安全是會損耗性能的,而且不能忽視。那是不是為了性能就全部都做成線程不安全的呢?當然也不是,正確性永遠都是第一位的。那到底要怎麼做呢?

當你用OO和並發一起做的時候,你除了付出痛苦來單獨思考每一個method是不是需要做成線程安全以外,沒有任何指導方法或者科學依據來減輕你的負擔。

當然對於工程師來講,只要公司多給點錢,那我們自然可以把程序寫得更好,於是這點小問題還是無所謂的。但是對於那幫做學術的人來講,那就不一樣了。他們眼中只有行跟不行,於是自然就說出這句話了。

不過我認為這句話說的也是有道理的,我並不是在說他們說的不對,我是在同意他們。OO和並行你想做的好,除了看Jeffrey Richter的書以外,沒有任何辦法。但是我們知道,很多懶惰的人根本連教程都不願意仔細看,怎麼會學到正確的並發程序的設計方法呢?


基於狀態做多線程一點問題都沒有。現在的 OOP 的問題在於,這些語言都嘗試把對象之間的消息傳遞重新嫁接在過程調用上,這是一切的原罪。正如 FP 如果直接用 C 的 calling convension 和 stack 的話一樣會死,必須發明另外的組織方法一樣。

解法很簡單:Actor。一個完美的體現 OO 精髓、完美表現對象之間「消息通信」,完美解決競爭狀態和死鎖可能,同時比函數式編程直觀。Actor 的模型更從設計行提供了更多優化和擴展的可能性:,如 M:N 線程調度("~2.5 million actors per GB of heap." with Java / Scala [3]),以及分散式調度 [3]。

所有真正的高並發庫都是基於 Actor 模型的,從古老的 Erlang 到 Ada 和 Go(mk chan),以及 Scala 和某個 Clojure 1.5 新加的 Actor 庫(找到了 [1]),以及 JVM 平台的 Kilim [2] ……真的不需要多說什麼了。

如果略微沒有概念,推薦每個人去看 [4]:Rob Pike 介紹自己在 Plan 9 發明的 Newsqueak 語言的演講。我自己看過之後對很多之前看 Golang 中雲里霧裡的疑惑都得到了清晰的來龍去脈的解答。

[1] Clojure/core
[2] kilim/kilim · GitHub
[3] Akka
[4] Advanced Topics in Programming Languages_在線視頻觀看


流行的面向對象編程其實是對這一概念的誤解,更像是市場推廣。我覺得首先要正確理解這一概念才能談是否是反模塊化和反並行的。

面向對象編程這一概念是由Smalltalk的發明人Alan Kay(不了解的人請Google)首先提出的。按Alan Kay的看法,面向對象最重要的一點是消息傳遞

OOP to me means only messaging, local retention and protection and

hiding of state-process, and extreme late-binding of all things. It

can be done in Smalltalk and in LISP. There are possibly other

systems in which this is possible, but I"m not aware of them.

見http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_kay_oop_en

面向對象就是為了解決大規模編程的複雜性問題提出的,用消息傳遞的方式降低模塊之間的耦合。

目前最流行的並發編程語言就數Erlang和Go了。根據Erlang之父Joe Armstrong,Erlang其實也是面向對象編程語言,在小的方面來講又是函數式編程語言,所以面向對象和函數編程並不矛盾。(見oop - Is Erlang Object-Oriented?)

Erlang建立在Actor Model之上,而Go建立在Communicating Sequential Process之上,這兩個計算模型都跟Alan Kay的消息傳遞模型有相通之處。所以說,消息傳遞模型也能很好地解決並發編程的問題。

雖說純粹的函數式編程語言沒有狀態,也就不存在競爭問題,適合併發,可那只是美好的願望罷了,更像是書獃子的意淫,在現實世界中怎麼可能完全消除狀態,就連Haskell也要發明複雜的Monad來解決狀態問題,所以說函數編程並不是解決並發問題的靈丹妙藥。

編程語言的發展過程可以看作不斷遠離圖靈機模型而接近人腦思維模型的一個過程,最開始是彙編語言,直接操作圖靈機的狀態;然後是過程化編程範式,用各種更有局限的控制語句替代過於靈活的go to,現在則是面向對象和函數式編程語言的天下。簡單地講,就是要解決如何擺脫賦值語句的問題。面向過程和面向對象都沒完全擺脫賦值語句,但是施加了更多的限制,防止賦值語句濫用。函數式編程企圖使用數學意義上的函數概念完全消除賦值語句,但是沒有成功,只得用更隱晦的方式加以保留。

PS. 另外吐槽一下,在知乎上編輯文字真麻煩,排版又難看,既然要抄襲stackoverflow,為啥不全抄過來!


這個問題我在stackexchange上問過,有很多很好的回答,可供參考。
http://programmers.stackexchange.com/questions/70831/is-objected-oriented-programming-paradigm-outdated-since-it-is-anti-modular-and-a


我們知道,命令式編程相當於在管理狀態機。而其中的面向對象編程則相當於同時管理多個狀態機,還要讓它們之間交互,顯然很難啊,也體現不出良好的劃分啊。這就可以解釋面向對象編程為什麼是反模塊化和反並行的。

但是經過長期的發展,在各種設計思想和模式的幫助下,我們通過在一些經過充分研究的場景採用特定的編程模式,能相當程度地hold住多個狀態機,從而在開發效率、運行效率、正確性、可靠性等方面取得一個較好的折中。

我對函數式編程了解不深,但是對Closure, CPS, Functional Data Structure這些東西看著挺喜歡的。如果函數式編程的生態環境能發展到接近面向對象編程的程度,那也挺好啊。

話說回來,我最喜歡的是類型系統,至於我擅長的OOP,還是我不擅長的FP,都不重要了。它們的大部分東西在我眼中沒有區別。


開發有三種類型,或者程序員有三種類別。寫框架的,寫庫的,和寫功能的。

面向對象之所以被很多人詬病的原因之一,是在框架和庫的層次(如果這兩個一起開發的話),他會形成巨型的繼承樹,一旦這顆樹的設計出現問題,做細節的微調還可以,比如功能函數的上推下拉,局部重構等等,但是如果做大的調整很容易造成重大兼容性問題。

面向對象之所以被很多人詬病的原因之二,是在寫功能的層次,很多程序員都試圖做出更大更深層次的抽象,不停的做抽象類和介面,在代碼中生成大量的粘合層,而這些內容應該放到框架和庫的層次去做,而不是在做功能開發時為了未來可能有的功能埋下大量的代碼介面而忽略了開發功能的本質工作。

面向對象之所以被很多人喜歡的原因之一,是適合描述世界。在進行編碼的時候,需要做問題空間到解空間的映射,這個時候面向對象方式最接近人類的思維方式,這就是面向對象的東西總是相對的容易理解和使用。在過程式編程的時代,人們認為程序=數據結構+演算法。而在面向對象時代則認為,對象=數據結構+演算法,程序=對象+對象+...+對象。多了一層「封裝"之後,對介面的理解更容易了,但是也付出了性能,維護,設計等等問題上的代價。並且對很多底層知識的理解,其實是不利的。

我也曾經認為面向對象可以解決一切問題,設計模式可以解決一切問題,但事實證明如果一個東西被認為是萬能神葯,基本上就是騙人的。當我在編碼中越來越痴迷於如何抽象和形成擴展可維護性,而耽誤了正常功能進度時,我總是懷疑是否自己面向對象的功力還不夠……現在我認為,面向對象也只是編程範式的一種,也只是一種解決問題的工具。適當的運用,還是很有必要的。對面向對象缺乏信心的人,可以看Qt的介面。Qt證明了面向對象也能做的非常出色,即使是在C++這種複雜的語言怪物上。

不客氣的講,中國程序員偏重歸納總結和細節上的奇技淫巧,不擅長抽象和設計,而這些恰恰是面向對象系統最需要的素質。國內項目的話,如果做設計,常見的情況是,把其他人驗證過的抽象,重做一次,這樣最保險,但如果需要自己做抽象和設計的時候,往往會卡殼。

面向對象系統的設計,如果沒有大把的時間金錢和精力,不要做過渡嘗試,因為只有不停的重構提煉,抽象才能越來越精確。對於中小公司的中小型項目,如果非要自己做框架和庫的話,繼承層次不要超過三到五層為好。也可以玩一玩組件式,那東西擴展性不錯。


如果我們抽象一種極端的面向對象編程模型如下:

  • 對象由內部狀態和消息傳遞約定組成,一個對象接收消息唯一的目的就是查詢、改變其內部狀態。
  • IO 抽象為「句柄」對象,與普通對象同等看待。
  • 對任何消息的處理都是非同步的,不存在同步消息處理函數(前端程序員可以理解為對象方法只能返回 promise)。
  • 對於可寫對象(內部狀態可能改變),同一對象的消息處理由一個讀寫鎖控制。

不難想像,在這種模型下,我們可以 0 成本地以對象為單位進行任意粒度的並發執行,絕對不會存在 race condition。問題是,這樣的模型並不現實,在這種模型下,對對象的合理劃分,會耗盡我們所有的心力,無處不在的非同步,會讓我們的代碼神秘莫測(考慮一下多個非同步步驟串列的錯誤處理、事務實現)。

所謂「面向對象反並發」,反就反在這個「並不現實」上了。

現實中面向對象的語言,是不可能按照這樣的模型設計的,一定是提供簡便的方法實現對象間消息傳遞,比如成員函數,也一定不可能規定一個對象內部必須有把鎖,任何成員函數執行,第一件事就是上鎖。就算真的這樣設計,你還得考慮使用者是不是劃分對象的方式不合理,導致對象粒度過大,一把鎖鎖了過多的狀態影響並發,等等。

所以我們看到了折中的設計,就是 Actor、消息隊列之類的模型,將面向對象模型實現為兩個層次,低層次、細粒度的對象按照傳統面向對象語言的方式實現,保證使用方便,高層次、粗粒度的對象按照上面那種方式實現,保證對象間狀態不共享,從而保證並發性。比如 erlang、java+Akka。

類似的東西目前已經比較成熟,被證明行之有效,那麼反過來說,面向對象模型支撐並發編程,也是行之有效的(這只是從形式角度的判斷,內存友好性之類的暫不論)。

但問題在於,大部分人學習面向對象編程,根本不是這麼學的~ 這個層面的認識,一般來說是一個程序員經過了一段時間(甚至幾年)實踐後才逐漸理解的。那一開始我們學面向對象,學了些啥?介面繼承、實現繼承、is-a、has-a,public/private、設計模式……簡而言之,對於大部分人,面向對象是以以下兩種面貌出現的:

  • 一種給代碼增加約束,將運行時錯誤轉化為構建時錯誤,從而減少出錯可能的方法論。
  • 一種代碼復用的技巧。

然而面向對象作為一種讓代碼從無到有的業務理解、程序實現方式,到底是如何指導我們思考的,以及到底會在運行期帶來哪些影響,是我們至少在初期很少會想到的,就算接觸到了,也一時很難理解。這種狀況,導致我們可能對於作為約束和復用手段的面向對象可能已經很熟悉了,但對作為思考方式和運行機制的面向對象,卻還不太理解。這對我們編寫可並發的面向對象代碼帶來了很大的障礙。我們可能過了很久才發現,我們編寫程序的方式,是不合理的。

但是函數式編程,就沒有這樣的鴻溝,它從一開始在你面前出現,就是高度形式化的,並且學到後來你會發現,你從一開始學的那個函數是編程,就是以它的本來面貌出現的。


哇噢,好幾年前的問題了。

The functional and imperative programming classes are independent of one another, and both are required for the new data structures class. Both courses share an emphasis on verification techniques (principally, state invariants for imperative programming, and structural induction for functional programming). The new data structures course emphasizes parallel algorithms as the general case, and places equal emphasis on persistent, as well as ephemeral, data structures. This is achieved by using an expressive functional language (Standard ML) that includes both evaluation- and mutation-based computations, and that supports modularity and data abstraction.

Object-oriented programming is eliminated from the introductory curriculum, in favor of a more fundamental view of data structures using imperative programming, and a new course on parallel data structures using functional programming. A new course on object-oriented design methodology will be offered at the sophomore level for those students who wish to study this topic.

教授原文中如上所說。大體意思就是,入門課程更新了,側重於基礎的編程教學,並且多了並行演算法相關的數據結構,為了更好實現教學目標,所以需要從入門課程中去除OOP。在大二課程中還會有OOP的內容。

雖然OOP存在其他答主提到的諸多問題,但我怎麼沒看出來教授原文中哪句話說「OOP反模塊化反並行」了?題主挺會搞事情的。

Quaro的原問題是:Is object-oriented programming both anti-modular and anti-parallel by its very nature? Why or why not?

本來是個問句,在題主這裡就成肯定句了。

因此,我重新編輯了問題。


我發現程序員最大的毛病就是不能好好說「人話」。很簡單的一個問題,非要搞得大家都看不懂。

對比兩件事情:

1+1 = 2

FP的理念就是永遠結果可以預測,比如相加兩個數字。

用戶名 + 密碼 = ?

OOP的理念我可以改狀態哦,結果不一定可以預測哦,比如我改了密碼,那原來的用戶名加密碼就不能登陸了。

再說什麼事都要看前言後語(context)。這個教授要教的是FP,當然要安利一下FP的優勢。並行方面FP確實有優勢,但這是因為特性符合。模式都是工具,用好用壞看匠人。不要再迷信,也不要再月經了。。。


人家只不過是把OO的東西放到二年級去選修了。作為大一新生,先掌握基本編程技術進而學習數據結構和演算法才是重要的,所以人家開了兩門基礎編程課程,一個函數式編程,一個指令式編程。沒仔細看是不是這兩門選一個修,還是都要修。我上大學的時候有一些兄弟是學scheme作為入門編程的,多數人都是java,到了下學期數據結構也是用java講的,scheme的兄弟們就比較痛苦。。。課程設置不合理害死人啊


反模塊,在我看來OOP的核心設計目的之一就是更好的模塊化,良好封裝的對象本身就是模塊,在Quora的答案中解釋說反模塊是因為對象被調用導致對象狀態變化,是為缺點。這難道不是對象本身固有的屬性嗎?(舉個荔枝,一個你喜歡的女孩子對你說了一句話(對象調用),然後你臉紅了(狀態變化))。

反並行,目前的各種OOP編程語言對並行化支持都不太好,但是這個不太好跟GPU編程或者集群運行關係不大,這些問題完全可以通過運行框架解決。OOP反並行最主要的問題是對象調用是同步的(再舉個荔枝,在OOP的世界裡,你(對象)和一個女孩子(對象)說話(消息調用),你說完以後,必須要等她的反應(消息返回)才能繼續下一步動作。而現實世界中,你說完話以後可以立即上知乎發個帖,跟大家討教下主意)。

題外話
我支持CMU從大一新生課程中取消OOP課程的決定,我覺得學習編程語言應該從彙編開始,經歷面向過程語言,然後學OOA,OOD,最後學OOP。
編程其實是一門哲學,必須了解歷史。


最近好像看到不少視頻、文章討伐面向對象;甚至我所在的公司里也有一位據說以前做過Java後端的前端程序員、FP狂熱信徒專門搞了一個講座來講FP如何之好,而OO如何之壞。作為一個Java猿,我想來試著聊幾句。

我是普通碼農一隻,對語言的認識也是從用戶角度,如果哪裡說得不對,歡迎拍磚。

------

其實我並不在意OOP還是FP,在我看來,那都是人們為了交流或教育的目的起的名字而已,就像設計模式一樣。

如果說我相信什麼,我只相信契約式編程是構建複雜系統所需要的。

而人們通常描述的OOP或者FP,在組織契約的時候各有優劣而已。

OOP之優,在於它支持複雜介面的描述。我說的複雜介面,是指單靠一個function和語法級別所支持的「源生數據類型」無法描述的介面。這是一種遞歸,——你只有有了描述複雜介面的類型,它才可以作為參數被傳遞,如此遞歸,你才可以得到任意大粒度(複雜度)介面的表達方式。在OOP里,類型就是介面,介面就是類型,這倆詞是一回事,而class和繼承都是多餘的。這是OOP之優,也是FP之劣。拿JavaScript來說,在我看來,它缺乏的就是表述複雜契約的語法,——即介面(類型),ES6加了個class的語法糖,其實並沒有什麼卵用,原本ES5的封裝方式就很好。ES5真正缺的,就是介面(類型),TypeScript的實現方式就很好,——至於介面檢查(類型檢查)在運行時做還是transpile的時候做,其實沒有那麼重要。描述複雜契約的介面是給人看的,是通過起名字的方式減輕人的記憶負擔所用,運行時並不需要。


OOP之劣,在於繼承的濫用。任何繼承都不是必須的,而大部分繼承都是濫用。這話您理解就理解,不理解我也沒法解釋,只能說等您讀多了各種笨蛋寫的糟爛代碼自然就懂了。

還有FP黨對OOP的常見詬病是:「狀態跟方法綁在一起就是不對的」。對此我只能說,您理解的角度有問題,只見表象,而且還未必是正確實踐的表象。狀態總是存在的,放在哪裡,暴露多少給世界,都是設計選擇問題。OOP的方式總是試圖隱藏不必要的信息,描述契約的時候只描述必須的、最少的信息,以期獲得契約更長久的生命力,讓契約對其背後實現可能出現的變更有更強的抗性,從而降低整個系統的維護難度。這其中就有包括對於某些狀態的隱藏。OOP的狀態不是跟方法綁在一起,而是每個功能模塊維護它自己的狀態,然後只通過介面暴露必要的(有用的)契約給外界。這裡我說的介面不是指單個interface,而是指一個模塊承諾給外界的所有介面。這種設計選擇也許不對所有的情況適用,但它確實對大部分情況適用。

你看,我從來只去尋找對抽象和契約最合適的表達方式,就讓那些搞學術的人去糾結那些概念和名字好了。

當我聽到人們說OOP,TDD,FP……每次都像發現新大陸一樣四處宣揚,而在我看來,都是對契約式編程不同角度、不同程度的描述而已。


其實我一直都好奇為什麼要面向對象。我覺得面向對象試圖從語言的角度解決軟體設計的問題,這個方向本來就是錯誤的,很蹩腳。


在手機上猶豫糾結半天,好想回答這個問題,但是不是很確定是否有能力答好。。先挖個坑,慢慢填,希望能填好。請多指正。
面向對象分為兩個層次的理解,首先最重要的是作為一種思考方式的存在,這種思考方式的核心是三個字:不確定。
當我們可以把一個邏輯或者流程或者關係描述得很精確的時侯,我們並不需要面向對象的方法。
我相信所有人都會承認我們所處的這個世界處於連續的動態變化和一種"測不準"的狀態。所以隨著我們需要用計算機描述、表達、解決更複雜的問題的時侯,我們並不能強制將所有的問題處理為獨立靜態的狀況。
很多程序員難以理解,這也許和許多人學習編程的順序有關,為了能夠解釋各種語法關係,我們必須使用各種靜態和固定的情景作為例子:例如賦值、運算、類型等。作為組成指令而存在,它們是必須並且是我們保持一致性的前提。
但是,當我們看到的世界不是如此的時侯,我們需要改變我們的編寫方法來符合我們看到的事實,因此有了對事物更抽象的理解,我們更追求在一種動態的平衡中描述它們。當我們去除那些靜態的要素並賦予它不確定的狀態的時侯,就是面向對象的思考的本質。
從這個角度而言,面向對象是編程思維的深入。
用通俗的語言講,面向對象的策略和方法是不明確事物的狀態或者關係,當能夠明確某個部分的時侯我們就拿來使用,如果不明確就繼續保持不明確。
從這個角度回到問題,模塊化將狀態固定下來,並行強制將關係平行(一致化)這在面向對象的方法看來是不對的。
今天先寫到這裡,手機上知乎不支持橫屏,躺著打字手快拿不住手機了。


各位不要把因為自己沒有找到好的方法,就埋怨語言的支持不好,語言層面的支持如果做到完美,那還要這麼多高級的軟體工程師幹嘛,OOP是一種思想而已,就看你怎麼用,面向事件驅動的設計模型當然可以很好的做到一個線程內支持大量並發的問題,關鍵就是看你如何去設計你的程序,如何去封裝和定義你的事件,沒有人讓你再設計程序的時候一個勁的什麼地方都使用OOP思想,才是遵循了OOP,一個複雜的軟體裡面,會同時存在這多種思想,只是在不同的角度和層次上看看,不同的思想會有一個主流的概念,比如在系統底層的通信機制和並發領域,可以使用基於事件驅動的設計,此處不是OOP的,但是對於系統服務層面的支撐模塊,完全可以使用OOP的思想,畢竟OOP帶給了軟體行業非常大的發展,是有他的很大價值的。

王根這哥們說的太偏激了,軟體編程不光是給高級工程師的,那些大量存在的中間或者底層的開發人員需要面向對象的思維方式來工作,高級工程師的需要做的就是把難題隱藏起來,對於底層難題,沒有人告訴你一定要用什麼思想去實現,沒有人要求你的代碼在這個層面也需要擁有非常好的可讀性,如果你非要跟OOP去較真兒,說他的什麼什麼方面做的不夠好,那誰也沒有辦法。


一個怪獸100點血,被砍一刀扣60點,問同時被兩人各砍一刀,血變成多少?
A. -20
B. 40
C. 0


推薦閱讀:

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

TAG:面向對象編程 | 計算機經典課程 | 卡內基梅隆大學 (Carnegie Mellon University) |