如何通俗易懂地舉例說明「面向對象」和「面向過程」有什麼區別?

謝謝!


樓主諸位說的都太複雜,我們應該從編程的發展史來談面向對象的出現。

當軟體還非常簡單的時候,我們只需要面向過程編程:

定義函數
函數一
函數二
函數三
函數四

定義數據
數據一
數據二
數據三
數據四

最後
各種函數,數據的操作。

當軟體發展起來後,我們的軟體變得越來越大,代碼量越來越多,複雜度遠超Hello World的時候,我們的編寫就有麻煩了:

函數和數據會定義得非常多,面臨兩個問題。

首先是命名衝突,英文單詞也就那麼幾個,可能寫著寫著取名時就沒合適的短詞用了,為了避免衝突,只能把函數名取得越來越長。

然後是代碼重複,比如你做一個計算器程序,你的函數就要確保處理的是合理的數據,這樣最起碼加減乘除四個函數里,你就都要寫對參數進行檢測的代碼,寫四遍或者複製粘貼四遍不會很煩,但多了你就痛苦了,而且因為這些檢測代碼是跟你的加減乘除函數的本意是無關的,卻一定要寫在那裡,使代碼變得不好閱讀,意圖模糊。

就算一個網路小說的作者,他每次寫新章節時也不大可能直接打開包含著前面幾百章文字的文檔接著寫。更正常的做法是新建一個文檔,寫新的一章,為的是減小複雜性,減少干擾。更何況代碼那麼複雜那麼容易出錯的東西。

隨著軟體業的發展,解決辦法就要出來了。

代碼重複,我們可以用函數裡面調用函數的方法,比如把檢測代碼抽出來成一個獨立函數,然後加減乘除四個函數運行時只要調用一下檢測函數對參數進行檢查就可以了。分布在四個函數里的重複代碼變成了一個函數,是不是好維護多了。

命名衝突,我們就把這一堆函數們進行分類吧。

比如沒有分類時候,我們取名只能取名:

檢測
整數加
整數減
整數乘
整數除
複數加
複數減
複數乘
複數除
小數加
...

進行歸類後

整數 {
檢測




}
複數 {
檢測




}
小數 {
檢測




}
分數 {
檢測




}

是不是一種叫做類的概念就呼之欲出了,這樣我們打開一個整數類代碼文件,裡面就是簡簡單單的加減乘除四個函數,簡單清晰而不會跟外面的其他加減乘除函數命名衝突。

當然,進行歸類後,又有各種的問題和解決辦法了,比如四個類中的檢測也是應該提取出來的,所以簡單的起因最終發展出什麼繼承衍生之類挺複雜的一套編程模式。然後學術界那幫人就又亂起什麼高大上的名字了,所謂面向對象程序設計去禍害大學裡那幫孩子。

就算未來出來一個什麼新的面向XX編程,我們也不用多想為什麼會出現,肯定是為了解決麻煩而已。

2016年5月23日更新:

上面進行歸類後,代碼其實還是不好維護的,然後我們就繼續提取為:

數 {
檢測




}
整數 {
沿用上面數的設計
}
小數 {
沿用上面數的設計
}

所謂繼承,就是數這個類的整體設計,沿用給整數,分數小數這些類,作為他們的編寫大綱去編寫加減乘除這些函數的具體代碼。根據整數,分數,小數各自的性質,做出各自的調整。

這時數這個類,如果你給它裡面的加減乘除函數的寫了一些很粗糙簡單的代碼,就叫做父類,基礎類。子類們「繼承」了父類(把代碼進行了複雜化)。

如果沒寫,那這個類其實就只是個設計圖,叫做抽象類。子類們「實現」了抽象類(把空空的設計變成了具體代碼)。

模版是什麼?像C++這種複雜成狗的語言是強類型的,就是給變數進行了類型區分的,比如整數類型,雙整數類型。很明顯這兩種變數所能容納的數據體積是不一樣的,單個函數不能通吃多種類型的參數,我們就可能會面臨下面兩套代碼並存的局面。

單整數類 {
單整數加
單整數減
單整數乘
單整數除
}
雙整數類 {
雙整數加
雙整數減
雙整數乘
雙整數除
}

所以C艹跟其他強類型語言為我們提供了一個所謂模版功能:

&<變數類型&>整數 {
&<變數類型&>加
&<變數類型&>減
&<變數類型&>乘
&<變數類型&>除
}

整數類等於把變數類型設置為整數,套上模版
雙整數類等於把變數類型設置為雙整數,套上模版

這樣就寫了一份代碼,得到了兩份類的代碼。

當然,弱類型的編程語言,比如JavaScript或者PHP是沒有這種煩惱的,因為變數沒有類型之分。但變數類型有時候還是很重要的,弱類型語言里就會出現類似數加字元串這種運算,可能並不是程序員的預期和本意,所以比起強類型性語言而言經常會出現很多無聊的BUG。

再更新:

上面發展出了父類之後,我們發現編程還是有問題的,小數類:

小數類 {




}

如果我們需要一個能自動實現結果四捨五入的小數計算類,同時又需要一個不需要的,怎麼辦呢,難道要寫兩個類嗎?不要。

所以做出了「實例」或者「對象」這一東西,首先把類改成:

小數類 {
標識變數:是否四捨五入
標識變數:是否限定小數點後位數
構造函數(設置上面的標識)
加(會根據上面兩個標識變數輸出不同結果)
減(會根據上面兩個標識變數輸出不同結果)
乘(會根據上面兩個標識變數輸出不同結果)
除(會根據上面兩個標識變數輸出不同結果)
}

這樣,我們就寫一個類,但是通過構造函數,把一份代碼,構造出了行為稍微有點不同的兩個實例供我們使用,這時候名詞來了,不能進行實例化微調化的類,叫做靜態類,函數們的行為是固定的。不能實例化的類,其實只是函數們的一個集合歸納,只是對函數進行了整理,功能的強大和編碼的自由靈活度是不夠的。

能夠進行實例化,變化出各種行為各自不大一樣的實例的類,我們一般就把它們叫做類了,因為最常見。

程序員們也就能保持代碼簡單的同時而又可以很方便進行代碼行為微調了。


你的程序要完成一個任務,相當於講一個故事。

面向過程是編年史。

面向對象是紀傳史。

對於複雜的程序/宏大的故事,事實都證明了,面向對象/紀傳是更合理的表述方法。


如何大象裝進冰箱?

面向過程:

為了把大象裝進冰箱,需要3個過程。

1) 把冰箱門打開(得到打開門的冰箱)

2) 把大象裝進去(打開門後,得到裡面裝著大象的冰箱)

3) 把冰箱門關上(打開門、裝好大象後,獲得關好門的冰箱)

每個過程有一個階段性的目標,依次完成這些過程,就能把大象裝進冰箱。

1:

冰箱開門(冰箱)

冰箱裝進(冰箱, 大象)

冰箱關門(冰箱)

==換個寫法

(冰箱開門 冰箱)

(冰箱裝進 冰箱 大象)

(冰箱關門 冰箱)

2:

冰箱關門(冰箱裝進(冰箱開門(冰箱), 大象))

==換個寫法

(冰箱關門 (冰箱裝進 (冰箱開門 冰箱) 大象))

面向對象:

為了把大象裝進冰箱,需要做三個動作(或者叫行為)。

每個動作有一個執行者,它就是對象。

1) 冰箱,你給我把門打開

2) 冰箱,你給我把大象裝進去(或者說,大象,你給我鑽到冰箱里去)

3) 冰箱,你給我把門關上

依次做這些動作,就能把大象裝進冰箱。

1:

冰箱.開門()

冰箱.裝進(大象)

冰箱.關門()

2:

冰箱.開門().裝進(大象).關門()


題主要求通俗易懂,那麼用汽車工業的概念去解釋面向對象是最靠譜的方法。

首先科普一下汽車中的一個概念,叫「總成」,就是由若干零件、部件、組合件或附件組合裝配而成,並具有獨立功能的汽車組成部分,如發動機、變速器、轉向器、燈光等。總成就是一系列產品零件組成一個組合,實現一些特定功能的集合體。

一輛汽車的功能包括很多,包括行駛、制動、燈光等等,以制動剎車功能為例,如果按照面向過程的方式來開發,整個是以事件來驅動的,如下:

1. 踩下剎車踏板。

2. 啟動助力泵加壓

3. 啟動制動液壓管傳輸動力。

4. 啟動剎車片摩擦。

5. 汽車減速。

偽代碼表示這個過程的話就是:

pushBrakePedal();
startSteeringPump();
startHydraulic();
startBrakePads();

這時候沒用總成的概念,就是一個零件一個零件按照順序啟動而完成了制動功能,這個過程的每一一個元件的啟動可能都是行車電腦發起的,行車電腦要負責每一個零件的啟動、掛起、回收等操作。

(註:圖片來自網路)

如果按照面向對象方式的方式開發,「制動總成」就是一個對象(Object), 剎車踏板就是一個「制動總成」開放給外部的一個介面,汽車駕駛員通過調用"制動總成"這個對象的doBraking()方法就可以完成自動,至於助力泵、制動液壓管、剎車片這些都是「制動總成」的私有屬性,由制動總成負責,外界根本不用關心。設計這個制動總成的時候只需要開放一個Public的doBraking()方法給外界就可以。甚至制動系統可以有自己的控制晶元。

從上面可以看到,如果按照面向過程的方法去設計汽車,汽車廠商需要採購一大堆零件,然後研究如何調試、調用這一大堆零件以完成一個功能。

但是如果採用面向對象的方法去設計汽車,那麼汽車廠商可以採用外包的方式交給專業的制動系統廠商來設計,只需要約定需要開放哪些public方法,輸入什麼輸出什麼就可以了,在這個例子裡面就是要求制動系統廠商公開doBraking(String breakingMessage)方法,實現輸入制動消息(制動或者釋放制動),制動系統根據收到的breakingMessage判斷是輸出制動力還是釋放制動力。

代碼表示的話就是:

public class BreakingSystem {

private BrakePedal bp;
private SteeringPump sp;
private Hydraulic hdlc;
private BrakePads bps;

public void doBreaking(String breakingMessage)
{
if("push".equals(breakingMessage))
{
startSteeringPump();
startHydraulic();
startBrakePads();
}
}
}

這樣做的好處就是制動系統廠商可以集中精力設計制動總成,汽車設計廠商可以集中力量進行汽車整體設計,如果制動系統出現故障,只需要更換制動總成就可以解決問題。體現了面向對象設計的封裝、單一職責原則、開放封閉原則和介面分離原則?

評論裡面反對比較多啊,因為答案的描述還不完整,還沒有描述面向對象的多態、集成、抽象,其實這些思想在汽車工業裡面已經有體現了很多。

比如同一款發動機針對不同車型的調教體現多態。

比如發動機核心機的研發和以核心機為基礎研發系列型號發動機型體現抽象和繼承。

類似的例子還有很多.....問題是講的時候就沒有那麼生動了。


面向過程的程序中,代碼和數據是分離的,好比你請了一個修冰箱的修理工(repair_fridge),結果它是空著手來的,啥都沒帶,你不僅要給它輸入一個冰箱,還要負責給它準備一堆螺絲刀、扳手、線材、備件,等它修完了,你還得負責在合適的時候把這些不用了的東西清理掉。這其中哪一步出問題了都修不成。

後來你請了一個面向對象的修理工(FridgeRepairer.repair),他只要求你提供一個壞的冰箱(以及錢),其他的他自己解決:自帶工具,自己從備件庫里找合適的備件,自己負責替換掉壞的工具等等,完全不需要你操心。這樣雖然做的事情完全沒有變化,但是在你看來整個過程就簡單多了。這就叫做封裝。

更妙的是,你後來發現修電視、修空調的修理工都遵循相同的介面(Repairer.repair),這樣不管修什麼,操作都一致了。這就叫做多態。


「面向過程」是做一件事,「面向對象」是造一堆東西。

我的一個同事,既是程序員,又是phd家屬,聊起來這些phd寫的代碼,多少有些惱火。幾千行一個函數,寫完了還特自豪。一份代碼,師兄傳師姐,師姐傳師妹,越改越看不懂,還都指著它發paper。

說幫她們重構一下吧,還不樂意。

這類做研究用的代碼,演算法複雜,但目標簡單,就是要結果好看。要做的事情不變,一步步寫,也是可以完成的,她們也沒有動力重構。

而對於軟體項目,需求是不斷變化的,要做的事情越來越多,每件事都寫一個過程,就很痛苦。

比如怒鳥,前幾年流行的時候,沒幾天就出個新版,什麼太空呀,聖誕呀,鳥越來越多,景越來越炫。一隻鳥,一塊磚,一頭豬,還能寫個過程實現。但鳥有會變身的,能下蛋的,磚有鐵的,木的,冰的,豬有大的,小的,戴頭盔的... 混在一起,什麼事都能發生,寫出所有過程就不現實了。

面向對象就是造出不同的鳥,磚,豬,各自怎麼飛,怎麼撞,有什麼特技,都定義好,交給關卡設計師,給豬壘個樓,擺一擺造型,就可以上線收錢了。


搖狗尾巴

狗搖尾巴

的區別

搖(狗尾巴)

狗.搖尾巴()

的區別


首先那種矩形,正方形,鴨子之類的面向對象,不知道害了多少人,願沉迷其中之人早日出來。

自己總結了一下,大概三種面向對象方式:

1. 靜態函數包對象

將功能有聯繫的一批函數放在一起封裝成一個類。

這種類可以完全沒有內部數據,也可以有數據。當有數據時,這些數據充當的其實就是配置(配置對於一個設計優秀的對象,是透明的,對象本身內部的函數根本不知道有配置這個東西,它只知道它需要的每一個數據在它new之後就已經存在this里了,隨取隨用。配置的給予或獲取方式,是構建對象(new)時才需要去考慮的)

這種對象的特點是,它的每一個函數(或方法)對這些數據都是只讀的,所以不管方法有無被調用,被誰調用,被調用多少次,它也不會改變它的狀態。

2.領域模型對象

這個概念是相對於傳統的面向資料庫的系統分析和設計而言的。資料庫雖然只用了外鍵就描述了複雜的大千世界,但軟體開發的難點在於適應變化,並且能夠安全地修改。關係模型看似簡單,但它卻像一張蜘蛛網一樣將所有table和欄位包在一塊,牽一髮而動全身,讓你在修改時如履薄冰,一不小心就會顧此失彼,bug此起彼伏。

而OO的封裝特性則剛好可以用來解決這個問題。將業務數據整理成一個個獨立的對象,讓它們的數據只能被自己訪問。留給外界的基本上只是一些介面(方法),數據除非萬不得已,一個都不會公開。外界只能向它發送消息,它自己則通過修改自身數據來響應這種消息。

這種對象與第一種對象剛好相反,它一定有數據,而且它的每一個函數存在的目的就是修改自己的數據。且每一次修改都是粗粒度的,每一次修改後,對象也還是處在valid狀態。

3.其它用來解決過程式開發時,超多的變數,超複雜的流程而整理出來的小對象,臨時對象。這些對象一起協作,最後完成一個傳統成千上萬行的過程式代碼才能完成的功能。

例如現在要連接sql server執行查詢語句並取得結果返回。不使用任何類庫和工具,所有步驟都自己進行,例如解析協議,socket網路連接,數據包收發等。這時候從頭到尾用一個個函數來完成,絕對沒有先劃分出一個個職責分明的對象,讓各對象協作完成這件事情來得更簡單。


炒菜是面向過程。

先放油,再爆蔥姜蒜,接著放入食材或燜或炒,期間加入調味料,最後起鍋裝盤。

做包子是面向對象。

把食材做成餡兒包進麵皮里形成生包子坯 (對象),然後對生包子坯進行或蒸或煎或炸或煮或烤等處理(操作),最後出鍋開吃。


————————————————理想情況————————————————

面向過程:你買了輛汽車,說明書告訴你掛一檔起步然後換二檔三檔四檔五擋,就可以控制車輛速度。

面向對象:你買了輛汽車,說明書告訴你把開關扳到起步讓汽車起步,然後把開關扳到快點、更快點、特別快、最快,就可以控制車輛速度。

————————————————實際情況————————————————

面向過程:你需要掀開儀錶盤,從一團亂麻里找到藍白線和綠白線,把它們碰到一起才可以發動汽車。

面向對象:你可以按「高級操作」按鈕,儀錶盤自動彈開;然後你要從眼花繚亂的幾千個按鈕中找到「點火正」和「點火負」,保持它們的按下狀態,然後按「綁定」按鈕,汽車就發動了——汽車發動後,不要忘了按下「反綁定」哦。



德語是面向對象的。

中文:「我 拿起 杯子」

德語:「主動語態的我,被動語態的杯子,拿起」。


區別就是,對稍微複雜一點的問題,我們其實根本就寫不出來標準的面向過程的代碼……

我記得我剛工作那會兒負責帶我的前輩說 Windows API 是面向過程的,delphi 是面向對象的,當時我就信了,既而形成了「面向對象就是 obj.method(),面向過程就是method(obj)」的印象。

後來我發現這完全是胡扯淡,Windows API 哪裡面向過程了?把各種狀態都封裝成內核對象、UI對象的句柄,各種「過程」 API 接受的第一個參數就是句柄,無非就是沒有 C++的「.」和「-&>」語法,這不還是面向對象的嗎,欺負 C 語言沒有成員函數嘛?

要說多態,C 語言寫的 API 一樣有多態啊,同樣是 CloseHandle,不同的 handle 當然有不同的 close 方法了。

後來我看了一點 C 代碼,發現很多 C 代碼是這麼設計的:

int close_handle(void* handle) {
if (!handle) return -1;
close_func_t close = ((some_base-struct*)handle)-&>close;

if (close) return close(handle);
else return -1;
}

這 tm 跟各種面向對象語言教程里的代碼,有毛區別?


面向過程可以理解為我不停的寫函數完成我要的功能。注重函數實現。

面向對象則是我在寫這個功能的時候考慮把這個功能封裝起來。以後我就可以直接拿來用了。注重封裝好後調用,不用管函數怎麼實現,只要結果。


又要通俗易懂,又要舉例說明,那你去看 SICP Python 版吧。

http://composingprograms.com/

首先最重要的一點就是,不要以為叫做「面向對象」就一定得要弄個對象。名字這種東西你就當他是個符號罷了,夫妻肺片裡面還沒有夫妻呢,老婆餅裡面還沒有妹紙呢。更何況是翻譯成中文的名字。

核心的東西就記住一點,任何一種新的理論,無非就是一組新的函數抽象和數據抽象的方法集罷了。抽象,封裝和消息傳遞才是正事。

———————

也不能讓翻譯來背這個鍋。應該是應試教育和死背答案害死人啊。我們都熟悉的面向對象的那一套東西對於Java來說並沒有錯。問題在於背答案的問題是,當我們學到新東西的時候,普遍的反應不是欣欣然,去想想我們以前學的為什麼不一樣,然後明白更多的道理,知道還有很多不同的世界線。反而是,凡是跟教參標準答案不一致就是不對!不對!大笨蛋!連面向對象是什麼都不知道!這麼簡單的東西都不懂,笨的都沒邊兒了!


一個人去電影院看電影。

面向過程:

出門,尋找公交站,上車,買票,下車,買電影票,進場。

面向對象。

{

移動()

定址()

支付()

}

流程:

定址(公交站)

移動(公交站)

支付()

定址(電影院)

支付()

移動(座位)


『其實都一樣』,區別在於抽象層級高低。

剛開始寫代碼(小小小功能):面向過程,零散語句,整理成函數,需要共用數據,數據和函數又需要公開和隱藏,不是就類了么。

熟悉了以後(業務邏輯較複雜時)從每一個語句細節去思考太累,乾脆抽象提高一階,從對象角度思考它們之間的互動,再分別實現對象。

回頭想想,語句不也是挺高階的抽象,封裝了機器實現細節。

面向介面類似,多想一層就明白了,略。


比如計算a+b,

面向過程是這樣的:你要有一個數字a,要有一個數字b,然後把他們加起來放到c中去。

面向對象是這樣的:你要設計一個計算器,這個計算器有一個動作add,這個add負責完成a+b這件事。

這兩種方式的區別在哪裡發現了嗎?面向過程是從怎麼計算這件事情去考慮的。面向對象是從我需要做計算這個件事,我需要用到什麼東西去考慮的。

一個著重點在實現,一個著重點在設計。

那有人會說同樣都是計算,為什麼還要再設計一個計算器,然後通過一個動作add來計算呢。

那我再舉個例子。讓你實現一個電梯系統。

面向過程是這樣的:要先有一個數字a表示樓層,要有一個b表示到達樓層,如果b小於a就讓a減到b,如果b大於a就讓a加到b。

這樣也好實現,那如果我要讓這個系統有人去乘坐,我要記錄這個人所在的樓層。你是不是開始感覺有點複雜了?那如果又有很多人呢?是不是感覺這個系統已經很難實現了?

可是面向對象可以這樣實現:設計一個電梯,這個電梯又兩個動作up和down。我還可以設計一個人,這個人有一個動作,那就是乘電梯。 具體的變數和內部實現都交給它們內部處理。

這樣,一個電梯系統是不是變得非常簡單明了了。因為你是基於現實世界中人乘坐電梯的行為去分析的。

這也是為什麼大型系統都要用面向對象來實現的緣故,因為你會發現當你把系統中每個對象都關係理清的時候,這個系統差不多就出來了。而用面向過程的方式你會發現舉步維艱,甚至無從下手。


今晚我要和女朋友去開房 -面向對象

今晚我要擼一發 - 面向過程


面向過程是坐火車,然後汽車,然後走回家

面向對象是先把火車分類 綠皮,空調特快,高鐵,動車。動車該划到高鐵還是單獨出來,這是個問題。高鐵跟動車是父子關係還是兄弟關係,也很糾結。該從什麼地方開始繼承呢,有很多輪子的有軌裝備?以後新高鐵搞磁懸浮沒有輪子怎麼辦?

好不容易上了火車,該考慮下一站的公共汽車,的士,滴滴,私家車的關係問題了。哦,最近又有電動汽車了,又有事情幹了。回家就重構下汽車。

終於到了靠腳走路了,就一雙腿,媽的,太不優雅了。放全局變數?影響耦合!要不做個單例?


推薦閱讀:

面向對象的RAII怎麼處理阻塞型的資源獲取過程?
在面向對象編程時對於類的劃分有哪些心得?
arraylist和array在內存分配和調用、編譯上有什麼本質區別?
c++為什麼要讓struct可以定義成員函數?
如何把思維從面向過程轉向面向對象?

TAG:程序員 | 編程語言 | 編程 | Java | 面向對象編程 |