什麼是物件導向?
20 世紀 80 年代,「物件導向」是指「物件導向程式設計」,主流是如何從 C 程式語言跨進 C++ 物件導向程式語言。以 C 傳統結構化程式設計為基礎,選擇性引用 C++ 物件導向程式設計的方便功能,其實對軟體工程和專案管理的影響不大。
90 年代,資訊產業紛紛採用 Java 開發系統,在全面導入物件導向程式語言後,對軟體工程和專案管理產生重大影響,演變出不同於傳統開發的另一套產業標準!客戶需求分析與系統規格設計也物件導向化,物件導向程式設計只是物件導向開發的一環,於是「物件導向」現在是指「物件導向開發」。
所以要通盤學習物件導向技術的話,有三大主題:物件導向分析、物件導向設計、物件導向編程。
這使得物件導向變成一種龐然大物,物件導向程式語言原本訴求的其實是能簡化開發,現在反而複雜化。所以,在物件導向技術中迷失方向的話,別放棄,讓我們回到初衷,重新了解「物件導向程式設計」吧!
什麼是物件導向程式設計?
物件(Object)
什麼叫「物件導向程式設計」?答案很簡單,程式全用「物件」來設計,就叫物件導向程式設計!
雖說答案簡單,但很多人還是在大量的寫下面這些程式,根本不是物件導向程式設計:
程式碼只要出現這些,就該想方設法把資料和行為寫在類別裡,讓程式碼看上去整片都是對物件進行操作,才是物件導向程式設計:
物件導向程式設計其實就是這麼簡單,只要這麼做,就是物件導向程式設計了!只是我們不知道原來這樣就叫做物件導向程式設計,以為把重點放在如何巧妙的設計類別才是物件導向,結果並不是。類別只是讓你打造出整片程式碼看上去都是 物件.操作()
用的,如果你沒能用類別實現這樣的程式設計,就不是物件導向程式設計。
所以,物件導向程式設計的重點是「操作物件」,不是「設計類別」!如果你沒有「用物件來設計程式」的概念,腦中只想著該怎樣設計類別,是稱不上物件導向程式設計的!其實類別並不重要,不是每個物件導向程式語言都有類別,有些只有物件和原型,物件導向程式設計卻做得比 C++ 和 Java 到位,反而把重點放在全程用操作物件的方式完成程式!
封裝(Encapsulation)
封裝可以讓你把資料和行為寫在類別裡。我們要致力於藉由這個機制,讓程式碼看上去只有整片的 物件.操作()
。
這帶來的好處,就如物件導向程式語言宣傳的一樣:能簡化開發的流程、減少維護的成本。
但如果你不致力於讓程式碼整片都是 物件.操作(),那再怎麼封裝、再怎麼照著 Design Pattern 寫出一堆類別,也發揮不了物件導向程式語言宣傳的效果,因為實際上並未隱藏細節,程式碼都是整片的 int、string、if、for,不是資料就是流程控制,其實跟 C 語言寫傳統結構化程式設計沒兩樣,並不是物件導向程式設計。
致力於巧妙地設計出一堆類別,卻沒能只靠操作物件的方式來寫程式,反而會讓物件導向程式設計變成複雜化開發流程、增加維護成本的失敗方案,因為從一開始就錯用物件導向程式語言…很遺憾的,物件導向方法論就是在幹這種事。
繼承(Inheritance)
當你就是要整座森林,不是一根香蕉,就繼承吧!物件導向程式設計不會只需要香蕉,有時候就是需要整座森林~
當你明確想套用 is a 的關係,就繼承吧!is a 的場合不繼承,硬要用 has a 的合成,那才叫蠢。
當你希望修改一處程式碼能連帶修改多處程式碼,就繼承吧!誰說這不是一種程式設計需求?一句高耦合就禁用?
該用就用!繼承在物件導向程式語言是很好的東西,不需要為了滿足物件導向方法論而不用。物件導向方法論本身才是比繼承更糟糕的問題來源1,才會到現在 2020 年代了還在發展、分支中,從沒成熟過。為了一個很爛的方法論,去否定物件導向程式語言認為很好的語法功能?
多型(Polymorphism)
繼承有個好處,就是多型!雖然不同的子類別各自設計出不同的實作,但繼承同樣父類別的話,就能以父類別為基準,視為同一種型別的物件來操作:
這項機制,可以讓我們程式碼出現更少的流程控制,變成更多的物件操作,讓物件導向程式設計的成分更加提高。
此外,有了繼承和多型,能讓我們用新增類別的方式擴充程式功能,而不是複製貼上程式碼來修改,甚至不用修改到舊原始碼的部分,只管寫新程式碼就好,對傳統結構化程式設計來說,這或許是物件導向程式設計最夢幻的功能!
多載(Overload)
函式參數不同,視為不同函式:
這語法機制能讓我們重複使用同樣名稱來操作物件,跟物件導向三神器「封裝、繼承、多型」一樣重要!致力於整片程式碼都是「物件.操作()」時,沒有多載的話,在為物件設計操作時會因為大量的命名而吐血,方便性更勝三者。
然而,只靠資料決定動作,是會搞爛設計的!像本節範例,明明是三種不同的動作,就應該各自命名為 hello()、bye()、intro() 才對,而不是 f(string)、f()、f(int)。
請優先依動作命名,其次才依資料多載。
合成(Composition)
當你想要一根香蕉,不是整座森林,自然就是 new 一個物件來用,不是繼承。
「多用合成,少用繼承。」這經典名句是對的!但我們本來就不會輕易使用繼承,都用合成,畢竟類別主要用途,就是 new 出一個物件,而不是繼承出另一個類別。
不過,這是因為 Java 帶頭禁止了多重繼承,所以只能 new 一堆物件來用。想像在 C++ 直接用多重繼承組合出一個類別來用,你就能理解這句名言非常有道理!
濫用多重繼承會破壞型別的制約效果,你這個物件在型別上變得什麼都是,本該水火不兩立的,因為你多重繼承水和火,結果你這個物件到底是水還是火?真是非常糟糕的物件導向用法~
所以這句名言或許該改為「多用合成,少用多重繼承」會比較容易理解,在只許單一繼承的 Java 和 C#,你不會想先繼承水,然後再繼承火,搞出好幾個類別來,而是在一個類別中用合成 new 出水和火。既然你本來就多用合成、少用繼承了,自然體會不出來這句名言的意義何在?
但 Java 有可以多重繼承和實作的介面!不過介面本身沒有功能,所以你更不可能想多重繼承一堆介面來代替合成,因此介面並不存在上述多重繼承的問題。會去實作多種介面,就表示你有意讓這類別在型別上又是水又是火,既然是有意的,多少會經過深思熟慮,在物件導向的使用上不會有太大問題。
最後,在「合成或繼承?」的議題上,我們會探討「內聚」和「耦合」。高內聚意味著外部更改時不太影響內部,低耦合意味著內部修改時不太影響外部。
修改父類別等於修改子類別的繼承,無疑是高耦合又低內聚,所以 new 個物件來操作想要的功能,要好過繼承一份程式碼來取得功能。但子類別的修改不會破壞父類別,要修改既有程式碼來擴充功能時,繼承是高內聚且低耦合的!合成沒辦法取代繼承做這檔事,不願繼承只要合成的話,就只能修改外部類別,這遠不如用繼承的,拿子類別改來用。所以:「多用合成,該用繼承的時候就繼承。」
屬性(Properties)
C# 的 Property 是 setter() 和 getter() 的變形,具有隱藏實作細節的特性,日後依然可以修改實作,不影響屬性名稱做為公開介面的特性。
所以 C# 的話,物件.屬性=資料
可視為整片的 物件.操作()
。
變數的處理資料方式是寫死的,日後不適用時,只能用換型別的方式處理,這麼一來變數就變成另一種物件或資料,會破壞依賴它的關係者。
C# 的屬性看似變數,但它不是,日後不適用時,屬性就跟函式一樣可以在內部修改實作,不會違反與關係者約定好的型別和介面名稱。
如何設計類別?
不談物件導向系統分析與設計,單就物件導向程式設計來講的話,其實類別應該依「狀態」來設計,比如有表示座標狀態的 x 和 y,那就設計成 Position 類別。之後呢,再視狀態的操作需要,不時設計「行為」上去。像這樣,在思考如何設計類別時,不是看行為決定,而是看狀態。這很合理,你無法根據會跑會叫來決定是貓或狗,而是根據姿勢和叫聲來決定。
有一樣的狀態和性質,但有不一樣的行為和操作時,就是用「繼承」重構程式碼的絕佳時機!比如,我們依 HP、MP、EXP 設計了「勇者」類別,後來想追加「戰士」「法師」類別時,就把 HP、MP、EXP 設計為「職業」類別,再繼承出「勇者」「戰士」「法師」等類別,並分別設計各自專屬的「行為、操作」,比如為戰士設計「格擋」、為法師設計「施咒」…像這樣,依狀態來決定類別的話,繼承是自然而然發生的事,沒什麼問題;依行為來決定類別的話,行為不同就不能列為同類關係,繼承起來會找不到合理的共通點而變得困難。其實只要性質相同,即使行為不同也能同屬一種類別。
在 C++ 正開始引領物件導向程式設計的時代,類別的用處,就是可以定義自己的資料型態,所以它當然應該依狀態、依資料來設計,而不是行為!
遺憾的是,只有自己寫程式,才能依狀態來設計類別。如果是團隊專案,被迫照「物件導向系統分析與設計」來進行「物件導向程式設計」的話,那類別就會「依介面設計」,介面沒有狀態,只有行為,也就是變相依「行為」來決定類別了!這就是為什麼物件導向程式設計被導入軟體工程後,就開始走樣並且變調的原因,然後自己錯在依行為決定類別,導致繼承變得很難用,卻指責說繼承是邪惡的東西,會搞砸設計,不該用它~
物件導向設計原則
不要用有學習曲線的功能和語法糖!
看不懂(不知道怎麼下手去修改程式碼)、搞不清(得費神研究程式碼在幹嘛)、猜不透(只能實際執行程式碼來檢測結果,光用看的沒辦法推算出結果)、會忘記(日後還要再複習才會用)…這樣的功能不要用!
它能把事做好沒錯,但那樣的程式碼其實跟一坨屎沒兩樣,只差在那是官方給的一坨屎,大家不敢說臭。
除非為了優化性能,才不得已去用,封裝在平常碰不到的地方,不要放在外邊讓人經常修改它。
確實有頭腦聰明的人,看得懂、搞得清、猜得透、記得牢,但如果不是大家都會,只有聰明人才會,那就不要用它。
像 LINQ 的 Join() 和 GroupBy(),它能把事情做得很好,但得花費時間再三研究,才有辦法用它、改它,這不符合物件導向程式設計的精神:「一看就知道從哪邊怎樣下手修改!」雖然寫的時候要多寫好幾行,但比濃縮成一行卻難以下手修改好。一堆 lambda 而且還串接來串接去的,簡直是物件導向程式設計的一場災難,不是該有的風貌。
不談 SOLID 嗎?是的,不談。
對「多用繼承」的「印象派物件導向程式設計」來說,真正重要的物件導向設計原則就是:「不要用有學習曲線的功能。」這句話可以換種說法:「物件的功能不要有學習曲線。」以此為物件導向設計原則!
物件導向設計模式
其一,不要把物件導向設計模式掛在嘴邊,擺一副懂設計模式才是行家的嘴臉~
物件導向設計模式不是什麼值得吹噓的專業,那是物件導向程式語言在應用上出現限制,只好腦補些東西出來的產物。物件導向設計模式的出現,只是證明物件導向程式語言用在大型專案上不是完美的解決方案,而是本身存在缺陷,得另外靠學術經驗來彌補。因此學了以後,我不曾掛在嘴邊,安安靜靜的用在物件導向程式設計解決問題就好。
也因此,不要在非物件導向程式語言把物件導向設計模式搬進去用,像是在 JavaScript 和 Python 中實現 Singletion、Decorator、Strategy 之類。既然程式語言本身沒那個缺陷,你把填補缺陷用的東西搬進去,不是跟沒事挖個坑一樣?
JavaScript 本身有基於原型鍊的物件繼承方案,Python 有自己語言先天表達能力強的解決方案,基本上沒有物件導向程式語言會遭遇到的功能限制,根本不需要用設計模式去表述介面和實作分離的各種想法。所以我建議不要為了炫耀自己懂物件導向設計模式,而把它們搬進動態腳本程式裡面,那不會讓你顯得專業,只會覺得你還不夠熟悉這些語言,不懂得用更好的做法解決問題,才會搬過去習慣來寫程式,然後寫得又臭又長的,卻自認為做法很漂亮。
其二,在使用物件導向設計模式時,不要假設每個同仁都懂這些設計模式,否則不但無法提升溝通能力,反而造成障礙,倒不如不用設計模式。2
尤其是使用某個設計模式詞彙做為術語,來抽象掉程式結構的交代細節時,你不能把責任丟給閱讀程式碼的人,預期他應該會懂這是什麼、猜測他應該了解我用它的理念。反而是自己要負責確認對方是否懂得這個設計模式,不懂的話,要教他這個設計模式是什麼,以及自己基於怎樣理念導入這個設計模式。如果不這樣做,錯的不是不懂物件導向設計模式的人,而是自以為物件導向設計模式是萬靈丹的你!
因為物件導向設計模式有些在結構與性質上很相似,非常有可能每個同仁的解讀與看法會有落差,所以要拿物件導向設計模式做為詞彙來溝通的話,事前要先溝通一番:「或者說徵求對方同意我導入它的理念。」畢竟,肯下功夫釐清每個設計模式定位的人,比你想像的還要少,幾乎都是活像應付學校期中期末考的程度而已。在經過確認後,有時候甚至會決定不要使用物件導向設計模式做為默契上的溝通,而慶幸:「沒想到物件導向設計模式不是萬靈丹,亂服用反而變成毒藥。」
或者,盡量在程式結構上,就能輕鬆簡單表現出物件導向設計模式的魅力,讓閱讀程式碼的人自己看出運用物件導向手法怎會如此精妙,同時就把這招給偷學起來,過程感覺不到這就是物件導向設計模式。而不是塞個 Strategy 或 Bridge 等詞彙進來,整個設計結構寫得錯綜複雜看不出是在幹嘛,卻要對方很有慧根地了解你在做什麼。
物件導向開發精神
當下動態型別和函數式風格盛行,大家一面倒批評 Java 程式碼又臭又長。要嘛改用 Python 或 Go 爽敲程式,不然就用 Kotlin 延續 Java 老命,好像繼續用 Java 語言寫程式很不可取一樣~
這我同意,Java 程式寫起來就是囉唆、冗餘,如果只是一個人寫個自己用的程式,隨便一個表述語言都是更好的選擇!
但如果是團隊開發大型軟體,不得不用物件導向來管理程式碼時,請把程式碼寫得又臭又長!動態型別函數式風格一行就能搞定的事,在物件導向程式設計就是得故意反過來,把一行拆成多行,這種風格易於修改、閱讀,能寫出比動態型別函數式風格更容易維護的程式碼,於是發揮管理的效果。
不要因為 Java 被批評得一無是處,就鄙視物件導向、用函數式風格去寫物件導向程式語言。當你進行團隊開發,需要一門故意把一行拆成多行的程式語言,自然會知道物件導向程式設計有多神,被批評得要死的缺點其實是優點。物件導向開發的心法,就是又臭又長、一行拆成多行的「狀態.操作()」,騰出修改的空間!物件導向專不專家、成不成功,盡在你懂不懂多行的價值。如果你沒有這點認知,為自己多行寫成一行的函數式風格沾沾自喜,等到開發出狀況,維護起來難如登天,就知道什麼叫自作虐不可活了。
題外話,其實物件導向和函數式並不衝突,可以在物件導向的規劃架構下,用函數式實作每個細節。
問題出在函數式太好用,直接就可以做出功能了,所以把功能混在整體裡也不以為意,還十分鄙視拆分整體為不同個體的物件導向很多餘。
哪天開發的案子出狀況,面對像在拆定時炸彈的局面時,物件導向肯定會有個很多餘的遙控器可以停止計時,得先解題的函數式則是不曉得該剪哪一條線…祝你還有三十分鐘可以確定剪哪一條,而不是只剩三十秒鐘。
當然,因為遙控器很多餘,所以第一時間往往找不出來放在哪,這也是物件導向的問題所在。但既然都有遙控器了,表示一定還有個用來接收訊號的開關,所以還是不用剪線、不怕剪錯線。因此,物件導向又臭又長不見得是缺點,許多場合就是需要它這一點~
從語法層面來教物件導向程式語言
由於物件導向系統分析與設計扭曲了物件導向程式語言的基本精神,若我站在程式語言的立場介紹物件導向,會和 21 世紀整票程式設計師在認知上有落差。若站在系統分析與設計的立場介紹物件導向,又會讓物件導向程式語言用起來很彆扭。因此在教程式設計時,我會把封裝、繼承、多型當一種語法來教授,就像變數、函式、流程控制那樣的東西,一個接一個教下去,完全不去談物件導向的事。
只有在教軟體工程時,才會大談物件導向是怎麼一回事。
我發現這種教學方式效果很好!語法給你了,該怎麼用?不需要什麼物件導向學說,寫程式的人自然會找到出路,合理、正確地使用它。反而灌輸一堆物件導向觀念,會變得不敢去用類別、繼承、多型這些語法,遲遲不曉得怎樣合理、正確地使用上。
這也證明了物件導向程式設計比你想的還要簡單!是物件導向方法論把事情給複雜化,搞砸了物件導向程式這一門語言!物件導向語言依然是一門結構化程式語言,只是比程序導向語言有更多語法而已,是一門更進步的結構化程式語言。而物件導向方法論者,在抨擊程序導向語言的同時,忘了它只是結構化程式語言的其中一種,連帶想推翻整個結構化程式語言,殊不知物件導向語言和程序導向語言兩者都是結構化程式語言,你推翻結構化程式設計,就寫不出正確的物件導向程式設計,於是搞砸了這一門程式語言。
物件導向程式設計的美學與藝術
程式碼應該易於閱讀和修改,而不是炫技。
我們不該寫出讓別人花很多很多時間去解讀的程式碼,最好是一眼看了就明白在做什麼。我們也不該寫出別人不敢修改,怕會影響到其它環節的程式。
要做到這一點,很簡單,把程式碼當成 GUI 一樣去配置!你怎樣配置直覺的使用者操作介面,就用同樣美學精神去配置你的程式碼,便可以真正寫出美觀的程式碼!不要用程式碼的結構去美化程式碼,因為那依然還是看起來顯得艱澀的程式碼。
在這一方面,為人詬病說程式碼過於冗長的物件導向程式設計,要比被捧成什麼都好棒棒的函數式程式設計,來得有用!把開放修改的程式碼,寫成一行又一行排得整整齊齊的「物件.操作(參數,參數)」,不覺得這樣就像圖形化介面整排按鈕一樣,簡單易懂嗎?
必須花很多時間去解讀的程式碼,就用物件導向封裝起來,只提供易於閱讀和使用的操作,然後從操作的流程,一眼就能了解程式碼的目的和樣貌!若想進一步了解艱澀的部分,再去讀封裝起來的那段程式碼。這件事雖然「依功能設計」的程序導向程式設計也做得到,但顯然「依狀態設計」的物件導向程式設計做得更好!
話說,從程式碼的結構去看程式碼美學,你會覺得整件事都不切實際:「程式碼要花更多時間理解、心理負擔大」,於是便用「藝術」這一詞,呼嚨自己這樣的設計就是一種美學。但「美觀」與否明明就是很直覺的,怎會是藝術的?用 GUI 的美學去配置程式碼的外觀,那美不美觀再實際不過,視覺上的美、心理上的無負擔,很直覺的呈現在面前看得到。
函數式程式設計就是專門在優化程式碼結構,寫得時候很爽,讀的時候很花時間,改的時候牽連廣。物件導向程式設計擅長排列程式碼功能,寫的時候很幹,要讀要改的時候很輕鬆!
像上面範例一樣,每一行理解起來不費神,而且修改起來無壓力,就像整排開關能夠切換一樣,寫出這樣的程式碼是哪裡蠢了?非要用函數式擠成一團黑箱!所以,別看不起物件導向,我從不認為函數式是更好的程式設計方案!函數式是很好的程式優化方案,但並不是很好的程式碼美化方案~