C++從零開始(十二)何謂面向對象編程思想
原始出處:網絡
前面已經說明了C++中最重要的概念——類,並且介紹了大部分和類相關的知識,至此,已經可以開始做些編程方面比較高級的應用——設計程序,而不再只是將算法變成代碼。要說明如何設計程序,有必要先了解何謂編程思想。
編程思想
編程,即編寫程序,而之前已經說過,程序就是方法的描述,那麼編程就是編寫方法的描述。我知道如何到人民公園,然後我就編寫了到人民公園的方法的描述——從市中心開始向東走400米再向右轉走200米就是。接著另一個人也知道如何去,但他編的程序卻是——從市中心沿人民東路走過兩個交叉口,在第三個交叉口處右轉,直走就能在右手方看到。很明顯,兩個程序不同,但最後走的路線是相同的,即大家的方法相同,但描述不同。
所謂的編程思想,就是如何編程,即編寫程序的方法。那麼之前在《C++從零開始(八)》中說的編程的三個步驟其實就是一種編程思想。這也是為什麼不同的人對同一算法編寫出的程序不同(指程序邏輯,不是簡單的變量或函數名不同),不同的人的編程思想不同。
如果多編或多看一些程序,就會發現編程思想是很重要的。好的編程思想編出的程序條理分明,可維護性高;差的編程思想編出的程序晦澀難懂,可維護性低。注意,這裡是從程序的易讀性來比較的,實際出於效率,是會使用不符合人腦思維習慣的編程思想,進而導致代碼的難於維護,但為了效率還是會經常在程序的瓶頸位置使用被優化了的代碼(這種代碼一般使用匯編語言編寫,算法則很大程度上是數學上的優化,丟棄了大部分其在人類世界中的意義)。
本系列一直堅持並推薦這麼一個編程思想——一切均按照語義來編寫。而語義是語言的意義,之前說它是代碼在人類世界中的意義。比如桌子,映射成一個結構,有桌腳數、顏色等成員變量,那麼為什麼沒有質量、材料、價格、生產日期等成員?對此有必要說明一下“人類世界”的含義。
世界
我們生活在一個四維的客觀物理世界中,游戲中的怪物生活在游戲定義的游戲世界中,白雪公主生活在一個童話世界中。什麼叫世界?世界即規則的集合。比如客觀世界中,力可作用於有質量的物體上,並進而按照運動學定律改變物體的速度;電荷異性相吸同性相斥;能量守恆等,這些都是對客觀世界這個規則的集合中的某些規則的描述。注意它們都只是規則的描述,不是規則,就好像程序是方法的描述,但不是方法。即方法和規則都是抽象的邏輯概念,各自通過程序和論調來表現。程序就是我們要編寫的,而論調就是一門理論,如概率論、運動學、流體力學等。而前面所說的游戲世界,是因為游戲也是一系列規則,關於這點,我在我寫的另一篇文章《游戲論》中做了詳細的闡述,如果還未理解世界的概念,《游戲論》中關於何謂游戲的闡述希望能有所幫助。同樣,童話世界也是由一系列的規則組成。如白雪公主能吃東西,能睡覺,並且能因為吃了毒蘋果而中毒;魔鏡能回答問題等。
那麼就算了解了世界這個概念又怎樣?有什麼用?前面說了本系列是推薦按照語義來編寫程序,即知道了算法後按照《C++從零開始(八)》中說的三步來編寫程序。而算法是基於某些規則的,如給出1到100求和的算法是(1+100)*100/2,這裡就暗示已經有那麼些規則說明什麼是加減乘除,什麼是求和。即一個算法一定是就一個世界來說的,它在另一個世界可能毫無意義。因為算法就是方法,是由之前說的命令和被操作的資源組成,而命令和資源就是由世界來定義的。
前面說根據算法寫代碼,其實是先制訂了一個世界來做為算法展示的平台。如之前的商人過河,其就在如下的一個規則集上表現的:有一只能坐兩人的船可以載人過河;有三個商人和三個僕人在河的一邊;河的任意一邊僕人數多過商人數商人就會被殺。這是對過河問題所基於的世界的嚴重不准確的描述,但在這過於抽象並沒什麼好處,只用注意:上面的商人和僕人不是現實世界中的商人和僕人,他們不能吃飯不能睡覺不能講話,甚至連走路都不會,唯一會的是通過坐船過河來改變自身的位置。當某一位置(即河的某一岸)的僕人的實例多於商人的實例時(且商人的實例至少有一個),則稱商人被殺。上面的描述暫且稱為商人僕人論,它是對過河問題所基於的世界的一個描述。
另一個人卻不像上面那樣看待問題。河有兩個岸,每個河岸總對應著兩個數字——商人數和僕人數。有一個途徑能按照某個規則改變河岸對應的兩個數字(就是坐只能坐兩人的船過河),而當任何一個河岸所對應的僕人數多於商人數時(且商人數不為零),則稱商人被殺。此人沒有定義商人和僕人這麼兩個概念,而只定義了一個概念——河岸,此概念具有兩個屬性——商人數和僕人數。這是另一個論調,暫且稱為河岸論。
什麼意思?上面就是對商人過河問題所基於的世界的兩個不同論調。注意上面論調不同,但描述的都是同一個世界,就好像動力學和量子力學,都是對客觀世界物體之間作用規則的描述,但大相徑庭。算法總是基於一個世界,但更准確點的說法應是算法總是基於一個世界的描述,而所謂的設計程序就是編寫算法所基於的世界描述,即論調,而論調其實就是問題的描述。
現在考慮前面說的商人僕人論和河岸論,它們都是同一世界的描述,但前者提出兩個名詞性概念——商人和僕人,各自具有“位置”這個狀態和“坐船”這麼一個功能以及“商人被殺”這個動詞性概念;後者提出一個名詞性概念——河岸,具有“商人數”和“僕人數”兩個屬性和“商人被殺”及“坐船”兩個動詞性概念。在此,說後者比前者好,因為後者定義的名詞性概念更少(即名詞性概念比動詞性概念更容易增加架構的復雜性,因為其代表了世界中東西的種類,種類越繁多則世界越復雜,越難以實現),雖說不一定更容易理解,但結構更簡單。
易發現,所有的論調都可以只由“名詞性概念”和“動詞性概念”組成,其中前者在數學中就是數、實數、復數等,後者是加減乘除、求導等,它們都被稱作定義。在《游戲論》中,我將前者稱為類,而類的實例就是方法中被操作的的資源,後者稱為命令。而在方法中,前者是資源的類型,後者是操作的類型。一個論調,提出的概念越少,結構就越簡單,也就越好。但應注意,就電腦編程來說,由於電腦並不是抽象的概念,而是存在效率因素的,因此基於前述的好的論調的算法而編出的代碼的運行效率並不一定高。
因此,所謂的程序設計,就是設計算法所基於的論調,而好的程序設計,就是相應的論調設計得好。但前面說了,效率並不一定高,對此,一般僅在代碼的瓶頸位置另外設計,而程序的整體架構依舊按照之前的設計。隨著程序的日趨龐大,清晰簡明的程序架構越顯重要,而要保持程序架構的簡明,就應設計好的論調;要保持架構的清晰,就應按照語義來編寫代碼。下面,介紹如此風靡的面向對象編程思想來幫助設計程序。
何謂對象
要說明面向對象,首先應了解何謂對象。對象就是前述的“名詞性概念”的實現,即一個實例。如商人僕人論中有商人和僕人兩個“名詞性概念”,其有三個商人和三個僕人,則稱有六個對象,分別是三個商人的實例和三個僕人的實例。應注意對象和實例的區別,其實它們沒有區別,如果非要說區別,可以認為對象能夠沒有狀態,但實例一定有狀態。
那麼什麼叫狀態?還是先來看看什麼叫屬性。桌子有個屬性叫顏色,這張桌子是紅色的而那張是綠的。人有個狀態叫臉色,這個人的臉色紅潤而那個的慘白。都是顏色,但一個是屬性一個是狀態,什麼區別?如果把桌子和人都映射成類,那麼桌子的顏色和人的臉色都應映射成相應類的成員變量,而兩者的區別就是桌子的實例在主要運作過程中顏色都不變化,其主要用於讀;人的臉色在人的實例的主要運作過程中可能變化,其主要用於寫。什麼叫運作過程?類映射的是資源,資源可以具有功能,即成員函數,當一個實例的功能執行時,就是這個實例的運作過程。
桌子有個功能是“放東西”,當調用這個成員函數時,其中會讀取顏色這個屬性的值以判斷放在桌子上的東西的顏色是否和桌子的顏色搭配協調。人有個功能是“泡澡”,其可以使相應實例的臉色從慘白向紅潤轉變。但桌子也有個功能是“改變顏色”,調用它可以改變桌子的顏色。按照前面所說,顏色是屬性,應該被讀,但這裡卻在實例的運作過程中對它進行了寫操作。注意前面說的是“主要運作過程”,即桌子的目的是用來“放東西”,不是“改變顏色”。如果桌子這個概念在其相應世界中主要是用來改變其顏色而不是放東西,此時桌子只不過是一個能記錄顏色值的容器,而這時桌子的顏色就是狀態,不是屬性了。
有何意義?屬性和狀態都映射為成員變量,從代碼上是看不出它們的區別的,但它們的語義是有嚴重區別的。屬性是用來配置實例而狀態是用來表現實例。在面向對象編程思想中,只是簡單地說對象是具有屬性和功能(也被稱作方法)的實例,這在編寫的程序所基於的世界比較復雜時顯得非常地孱弱,而且就是對“屬性”的錯誤理解,再加上“封裝”這個詞匯的席卷,導致出現大量的荒謬代碼,後面說明。
屬性和狀態的差別導致出現所謂的無狀態對象(在MTS——Microsoft Transaction Server中提出,稱作Stateless Component,無狀態組件),這正是對象和實例的差別——對象是實現,因此可以是一個抽象概念的實現;實例是實際存在,不能是抽象概念的實現。這在C++代碼上就表現為沒有成員變量的類和有成員變量的類。如下:
struct Search { virtual int search( int*, int, int ); };
Search a, b; int c[3] = { 10, 20, 5 }; a.search( c, 3, 20 );
這裡就生成兩個對象a和b,它們都是抽象