作者:二律背反
鏈接:https://www.zhihu.com/question/305042684/answer/557460817
OOP有且只有一個價值:應對需求的變化。
哪里的需求有變化,哪里就要OOP;哪里的需求不變,哪里就不需要OOP。一個hello world我可以一句話搞定、也可以寫四五個class用兩三種設計模式去實現(xiàn),我不是吃飽了撐的,而是因為我的客戶告訴我需求會發(fā)生變化:比如今天hello world在終端顯示、明天要換到GUI顯示、后天要用socket發(fā)到另一臺機器上顯示………
換句話說,如果需求永遠不變,OOP除了加了代碼的閱讀難度外,沒有任何價值。
從取舍的角度來說,OOP就是用“縱向增加代碼的復雜度”換取“對代碼進行增刪改時的最小工作量”。(元編程是橫向增加)
如果你面對一個一萬年也不會發(fā)生變化的需求,其實面向過程更好:不論從代碼效率還是代碼的可讀性上來說。
需求:開發(fā)一個計算器。
OOP的價值是:它可以讓你先開發(fā)出加減乘除,然后走測試甚至交付給用戶開始使用;后面你可以繼續(xù)開發(fā)乘方、求余、對數(shù)等功能。在你進行后續(xù)的這些開發(fā)時,你對之前已經(jīng)開發(fā)好的加減乘除的代碼的改動最?。◣缀醪挥酶模?,或者這樣說:加減乘除的代碼對你后續(xù)新增的代碼是無感知的。
如果用戶說,你就給我開發(fā)個實現(xiàn)加減乘除的計算器,我以后絕對不會改需求,如果我食言我就把合同吃了,那么,請你直接用最普通的面向過程開發(fā)吧,你甚至連函數(shù)都不需要寫、全部塞到main函數(shù)都行。
為了OOP而OOP的人,都是想不開的人。
我還見過一些“大神”寫那些一萬年也不會改或者就用兩天就廢棄的bash腳本,一堆function、模塊化設計,讀他們的腳本要切換來切換去,搞的人眼花繚亂……對此我只能說,你想自虐請自便但是別拉我一起好嗎?bash腳本,就簡簡單單從第一句寫到最后一句不好嗎?所以在這里順便對一些“大神”說一句:放過我們吧……
另外,把OOP理解為“以數(shù)據(jù)為中心、為對象”,沒有錯不過不夠徹底不夠本質(zhì),這只是初學者的理解,函數(shù)一樣可以OOP,lambda了解下?直接把OOP理解為“解耦”或者“為變化服務”更好一些。
如果你上面都看懂了,再回頭看下面這些非常官方的話,你應該就秒懂了:
面向?qū)ο蟮娜筇攸c是封裝、繼承和多態(tài)(為啥要封裝為啥要繼承為啥要多態(tài)?一句話:就是為了強化你代碼應對變化時的適應能力,比如有人覺得private public這些東西都沒用,你會覺得沒用是因為你沒開發(fā)過大型的軟件、沒解決過頻繁變化的需求)。
結(jié)構(gòu)化設計方法將審視問題的視角定位于不穩(wěn)定的操作之上,并將描述客體的屬性和行為分開,使得應用程序的日后維護和擴展相當困難,甚至一個微小的變動,都會波及到整個系統(tǒng)。面對問題規(guī)模的日趨擴大、環(huán)境的日趨復雜、需求變化的日趨加快,將利用計算機解決問題的基本方法統(tǒng)一到人類解決問題的習慣方法之上,徹底改變軟件設計方法與人類解決問題的常規(guī)方式扭曲的現(xiàn)象迫在眉睫,這是提出面向對象的首要原因……

作者:鐵原
鏈接:https://www.zhihu.com/question/305042684/answer/692966396
所有只是從OOP本身特性,語法等角度聊OOP的,都是不可能觸達問題實質(zhì)的。
因為OOP只是解決“某個問題”的手段的一種“相”而非所有相。見諸相非相,即見如來。
OOP產(chǎn)生的背景是計算機軟件行業(yè)大發(fā)展,軟件的種類和規(guī)模復雜度都急劇提升。原始的匯編和c語言等方式已經(jīng)難以滿足大規(guī)模工程化的要求了。軟件行業(yè)急需一種工程化的工具來滿足這種軟件開發(fā)的要求。它要求軟件行業(yè)能夠像搭積木一樣的組裝出任意復雜度和規(guī)模的軟件出來,這就是“組件化思想”(或者說模塊化思想)。
OOP是組件化思想的一種體現(xiàn),但并非唯一一種體現(xiàn),譬如還有MFC,SOA,微服務……等。不用管名詞是什么,粒度是什么,其實他們本質(zhì)都是一個東西——分而治之。這種思想得以實施的一個前提是復雜度內(nèi)部的耦合度和抽象。
組件化思想的目的是向上層屏蔽復雜性。所以這就意味著它的前提必須是基于耦合性和抽象的。如果不基于耦合性,則必須將內(nèi)部錯中復雜的關系暴露給上層——這是違反屏蔽復雜性目的。如果不基于抽象,而基于具體,則任何具體的變化會導致所有依賴這個組件的軟件產(chǎn)生聯(lián)動,這同樣違反向上層屏蔽復雜性。
這里基于內(nèi)聚性,大家都知道,都會努力做,只不過做的好不好而已。但是第二條,其實做到的并不多。面向?qū)ο缶幊?svg width="8px" height="8px" viewbox="0 0 15 15">
流毒甚廣的OO三原則:封裝,繼承,多態(tài),為什么說他們流毒甚廣呢?上面我們提到的東西,就是更基本的原則。其中只有封裝是對的,繼承和多態(tài)在很多情況下問題都頗多。有興趣的可以百度下。
只有理解了程序的復雜性,和解決復雜性的手段,以及這個手段和OOP之間的關系,才算是基本了解了OOP的本質(zhì)。
這里面有很多較細的情況
1. 程序都是復雜性的么?
2. 復雜性必須只能用OOP解決么?如過不是,什么情況下OOP合適?哪些情況其他 ?
3. OOP解決的真正有效的手段是什么?有哪些是謬誤的,或者問題比較多的,如何規(guī)避?

作者:大寬寬
鏈接:https://www.zhihu.com/question/305042684/answer/550196442
面向?qū)ο缶幊?svg width="8px" height="8px" viewbox="0 0 15 15">
I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages (so messaging came at the very beginning -- it took a while to see how to do messaging in a programming language efficiently enough to be useful).
...
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.
簡單解釋一下上面的這幾句話的大概意思:OOP應該體現(xiàn)一種網(wǎng)狀結(jié)構(gòu),這個結(jié)構(gòu)上的每個節(jié)點“Object”只能通過“消息”和其他節(jié)點通訊。每個節(jié)點會有內(nèi)部隱藏的狀態(tài),狀態(tài)不可以被直接修改,而應該通過消息傳遞的方式來間接的修改。
這個編程思想被設計能夠編寫龐大復雜的系統(tǒng)。
那么為什么OOP能夠支撐龐大復雜的系統(tǒng)呢?用開公司舉個例子。如果公司就只有幾個人,那么大家總是一起干活,工作可以通過“上帝視角“完全搞清楚每一個細節(jié),于是可以制定非常清晰的、明確的流程來完成這個任務。這個思想接近于傳統(tǒng)的面向過程編程。而如果公司人數(shù)變多,達到幾百上千,這種“上帝視角”是完全不可行的。在這樣復雜的公司里,沒有一個人能搞清楚一個工作的所有細節(jié)。為此,公司要分很多個部門,每個部門相對的獨立,有自己的章程,辦事方法和規(guī)則等。獨立性就意味著“隱藏內(nèi)部狀態(tài)”。比如你只能說申請讓某部門按照章程辦一件事,卻不能說命令部門里的誰誰誰,在什么時候之前一定要辦成。這些內(nèi)部的細節(jié)你管不著。類似的,更高一層,公司之間也存在大量的協(xié)作關系。一個汽車供應鏈可能包括幾千個企業(yè),組成了一個商業(yè)網(wǎng)絡。通過這種松散的協(xié)作關系維系的系統(tǒng)可以無限擴展下去,形成龐大的,復雜的系統(tǒng)。這就是OOP想表達的思想。
第一門OOP語言是Ole-Johan Dahland和Kristen Nygaard發(fā)明的Simula(比smalltalk還要早)。從名字就可以看出來,是用來支撐“模擬系統(tǒng)”的。模擬這個場景非常適合體現(xiàn)OOP的這個思想。這個語言引入了object、class、subclass、inheritance、動態(tài)綁定虛擬進程等概念,甚至還有GC。Java很大程度上受了Simula的影響。我們在現(xiàn)在教書上講解OOP類、實例和繼承關系時,總會給出比如動物-貓-狗,或者形狀-圓-矩形的例子,都源自于此。
還有一些帶有OO特征的語言或者研究成果在Simula之前就出現(xiàn),這里就不往前追溯了。
但隨后在施樂Palo Alto研究中心(Xerox PARC),Alan Kay、Dan Ingalls、Adele Goldberg在1970年開發(fā)了smalltalk,主要用于當時最前沿計算模型研究。在Simula的基礎之上,smalltak特別強調(diào)messaging的重要性,成為了當時最有影響力的OOP語言。與smalltalk同期進行的還有比如GUI、超文本等項目。smalltalk也最早的實現(xiàn)了在GUI使用MVC模型來編程。
但是,并不是說OOP程序一定要用OOP語言來寫。再強調(diào)一下,OOP首先是一種設計思想,非僅僅是編碼方式。從這個角度推演,其實OOP最成功的例子其實是互聯(lián)網(wǎng)。(Alan Kay也是互聯(lián)網(wǎng)前身ARPNET的設計者之一)。另外一個OOP典型的例子是Linux內(nèi)核,它充分體現(xiàn)了多個相對獨立的組件(進程調(diào)度器、內(nèi)存管理器、文件系統(tǒng)……)之間相互協(xié)作的思想。盡管Linux內(nèi)核是用C寫的,但是他比很多用所謂OOP語言寫的程序更加OOP。
現(xiàn)在很多初學者會把使用C++,Java等語言的“OOP”語法特性后的程序稱為OOP。比如封裝、繼承、多態(tài)等特性以及class、interface、private等管家你在會被大量提及和討論。OOP語言不能代替人類做軟件設計。既然做不了設計,就只能把一些輪子和語法糖造出來,供想編寫OOP程序的人使用。但是,特別強調(diào),是OOP設計思想在前,OOP編碼在后。簡單用OOP語言寫代碼,程序也不會自動變成OOP,也不一定能得到OOP的各種好處。
我們在以為我們在OOP時,其實很多時候都是在處理編碼的細節(jié)工作,而非OOP提倡的“獨立”,“通訊”。以“class”為例,實際上我們對它的用法有:
表達一個類型(和父子類關系),以對應真實世界的概念,一個類型可以起到一個“模版”的作用。這個類型形成的對象會嚴格維護內(nèi)部的狀態(tài)(或者叫不變量)
表達一個Object(即單例),比如XXXService這種“Bean”
表達一個名字空間,這樣就可以把一組相關的代碼寫到一起而不是散播的到處都是,其實這是一個“module”
表達一個數(shù)據(jù)結(jié)構(gòu),比如DTO這種
因為代碼復用,硬造出來的,無法與現(xiàn)實概念對應,但又不得不存在的類
提供便利,讓foo(a)這種代碼可以寫成a.foo()形式
其中前兩種和OOP的設計思想有關,而其他都是編寫具體代碼的工具,有的是為了代碼得到更好的組織,有的就是為了方便。
很多地方提及OOP=封裝+繼承+多態(tài)。我非常反對這個提法,因為這幾個術語把原本很容易理解的,直觀的做事方法變的圖騰化。初學者往往會覺得他們聽上去很牛逼,但是使用起來又經(jīng)常和現(xiàn)實相沖突以至于落不了地。
“封裝”,是想把一段邏輯/概念抽象出來做到“相對獨立”。這并不是OOP發(fā)明的,而是長久以來一直被廣泛采用的方法。比如電視機就是個“封裝”的好例子,幾個簡單的操作按鈕(接口)暴露出來供使用者操作,復雜的內(nèi)部電路和元器件在機器里面隱藏。再比如,Linux的文件系統(tǒng)接口也是非常好的“封裝”的例子,它提供了open,close,read,write和seek這幾個簡單的接口,卻封裝了大量的磁盤驅(qū)動,文件系統(tǒng),buffer和cache,進程的阻塞和喚醒等復雜的細節(jié)。然而它是用函數(shù)做的“封裝”。好的封裝設計意味著簡潔的接口和復雜的被隱藏的內(nèi)部細節(jié)。這并非是一個private關鍵字就可以表達的。一個典型的反面的例子是從數(shù)據(jù)庫里讀取出來的數(shù)據(jù),幾乎所有的字段都是要被處理和使用的,還有新的字段可能在處理過程中被添加進來。這時用ORM搞出一個個實體class,弄一堆private成員再加一堆getter和setter是非常愚蠢的做法。這里的數(shù)據(jù)并非是具有相對獨立性的,可以進行通訊的“Object“,而僅僅是“Data Structure”。因此我非常喜歡有些語言提供“data object”的支持。
當然,好的ORM會體現(xiàn)“Active Record”這種設計模式,非常有趣,本文不展開
再說說“繼承”,是希望通過類型的 is-a 關系來實現(xiàn)代碼的復用。絕大部分OOP語言會把is-a和代碼復用這兩件事情合作一件事。但是我們經(jīng)常會發(fā)現(xiàn)這二者之間并不一定總能對上。有時我們覺得A is a B,但是A并不想要B的任何代碼,僅僅想表達is-a關系而已;而有時,僅僅是想把A的一段代碼給B用,但是A和B之間并沒有什么語義關系。這個分歧會導致嚴重的設計問題。比如,做類的設計時往往會希望每個類能與現(xiàn)實當中的實體/概念對應上;但如果從代碼復用角度出發(fā)設計類,就可能會得到很多現(xiàn)實并不存在,但不得不存在的類。一般這種類都會有奇怪的名字和非常玄幻的意思。如果開發(fā)者換了個人,可能很難把握原來設計的微妙的思路,但又不得不改,再穩(wěn)妥保守一點就繞開重新設計,造成玄幻的類越來越多…… 繼承造成的問題相當多?,F(xiàn)在人們談論“繼承”,一般都會說“Composite Over Inheritance“。
多態(tài)和OOP也不是必然的關系。所謂多態(tài),是指讓一組Object表達同一概念,并展現(xiàn)不同的行為。入門級的OOP的書一般會這么舉例子,比如有一個基類Animal,定義了run方法。然后其子類Cat,Dog,Cow等都可以override掉run,實現(xiàn)自己的邏輯,因為Cat,Dog,Cow等都是Animal。例子說得挺有道理。但現(xiàn)實的復雜性往往會要求實現(xiàn)一個不是Animal的子類也能“run”,比如汽車可以run,一個程序也可以“run”等??傊灰莚un就可以,并不太在意其類型表達出的包含關系。這里想表達的意思是,如果想進行極致的“多態(tài)”,is-a與否就不那么重要了。在動態(tài)語言里,一般采用duck typing來實現(xiàn)這種“多態(tài)”——不關是什么東西,只要覺得他可以run,就給他寫個叫“run”的函數(shù)即可;而對于靜態(tài)語言,一般會設計一個“IRun”的接口,然后mixin到期望得到run能力的類上。簡單來說,要實現(xiàn)多態(tài)可以不用繼承、甚至不用class。
OOP一定好嗎?顯然是否定的?;氐絆OP的本心是要處理大型復雜系統(tǒng)的設計和實現(xiàn)。OOP的優(yōu)勢一定要到了根本就不可能有一個“上帝視角”的存在,不得不把系統(tǒng)拆成很多Object時才會體現(xiàn)出來。
舉個例子,smalltalk中,1 + 2 的理解方式是:向“1”這個Object發(fā)送一給消息“+”,消息的參數(shù)是“2”。的確是非常存粹的OOP思想。但是放在工程上,1 + 2理解為一般人常見的表達式可能更容易理解。對于1 + 2這樣簡單的邏輯,人很容易從上帝視角出發(fā)得到最直接的理解,也就有了最簡單直接的代碼而無用考慮“Object”。
如果是那種“第一步”、“第二步“……的程序,面向數(shù)據(jù)的程序,極致為性能做優(yōu)化的程序,是不應該用OOP去實現(xiàn)的。但很無奈如果某些“純OOP語言”,就不得不造一些本來就不需要的class,再繞回到這個領域適合的編碼模式上。比如普通的Web系統(tǒng)就是典型的“面向”數(shù)據(jù)庫這個中心進行數(shù)據(jù)處理(處理完了展示給用戶,或者響應用戶的操作)。這個用FP的思路去理解更加簡單,直觀。也有MVC,MVVM這樣的模式被廣泛應用。
還有一些領域盡管用OOP最為基礎很適合,但是根據(jù)場景,已經(jīng)誕生出了“領域化的OOP”,比如GUI是一個典型的例子。GUI里用OOP也是比較適合的,但是GUI里有很多細節(jié)OOP不管或者處理不好,因此好的GUI庫會在OOP基礎之上擴展很多。早期的MFC,.Net GUI Framework, React等都是這樣。另外一個領域是游戲,用OOP也很合適,但也是有些性能和領域細節(jié)需要特殊處理,因此ECS會得到廣泛的采用。
總結(jié)一下,OOP是眾多設計思想中的一種。很多OOP語言把這種思想的不重要的細節(jié)工具化,但直接無腦應用這些工具不會直接得到OOP的設計。即便是OOP思想本身也有其適合的場景和不適合的場景。即便是適合的場景,也可能針對這個場景在OOP之上做更針對這個場景需求的定制的架構(gòu)/框架。如果簡單把OOP作為某種教條就大大的違反了這個思想的初衷,也只能得到擰巴的代碼。
往期推薦