什么是面向?qū)ο?/h4>
這年頭但凡是寫過幾行代碼的,想必都不會對面向?qū)ο螅∣bject-Oriented, OO)這四個字感到陌生。但什么才是面向?qū)ο螅恢烙钟卸嗌偃苏嬲ニ伎歼^。有人以為和女朋友一起敲代碼就是面向?qū)ο缶幊塘耍ù箪F);有人覺得使用C++/Java/C#等面向?qū)ο笳Z言就是在面向?qū)ο罅耍灰灿腥寺犝f繼承、封裝、多態(tài)是面向?qū)ο笕筇卣鳎眉一铮^承不就是各種子類么?封裝不就是各種private么?多態(tài)是啥好像很厲害,哦原來只要子類重寫父類方法然后調(diào)用的時候向上轉(zhuǎn)型就好了嘛……都是小意思啦我整天都在面向?qū)ο蟆?/p>
少年,你的思想很危險啊……
首先,和對象一起敲代碼那是“真·結(jié)對編程”,顯然是比面向?qū)ο缶幊谈呒壍募寄埽绻B這都做到了,人生贏家啊您,我無話可說,您已經(jīng)完全不用管究竟什么是面向?qū)ο罅恕劣谑O碌膬深愌哉撁矗覀冞€可以好好探討一下。
編程語言和編程思想
在編程語言的信仰之爭中有一句經(jīng)典廢話,此言一出,萬籟俱靜,與女神的“呵呵”具有同等的殺傷力,它就是:語言只是工具,思想才是重點!當你在鍵盤上打出這幾個字的時候,一股優(yōu)越感直沖頭頂百會穴,你掐滅了煙邪魅一笑,心道哥說得這么有道理你們這幫低級碼農(nóng)無言以對了吧。
好吧我確實無言以對,我們只是在客觀地討論語言特性,討論其設(shè)計以及應(yīng)用,您瞬間上升到哲學層面了,這怎么玩?
說語言是工具當然沒錯,但哪怕是工具,一個合格的匠人也該對自己手頭的工具有充分的了解才能保證物盡其用。匠人們在茶余飯后討論討論哪個工具趁手些,哪個工具雖然上手難用些容易傷到自己,但一旦耍熟練了卻又好用得緊之類的話題自然也無可厚非。但語言僅僅是工具么?顯然不是的,對于熱愛編程的人來說,語言更是一種“玩具”,如劍之于嗜劍如癡的劍客,如車之于愛車如命的車手。男兒至死是少年啊,對于心愛之物,有一點超出理智的熱衷又怎么了?
當然,喜歡某種語言無可厚非,但因此固步自封,藐視其他一切語言,不去嘗試其他可能卻是十分愚蠢的——你哪怕要噴一種語言,也至少得去用一用研究一下吧?而往往等你真正去深入一門語言,甚至了解它的演化歷史之后,又會對它生出喜愛之情。
有點扯遠了啊,雖然我覺得“語言只是工具,重要的是思想”這句話是不完全正確的,因為語言在很大程度上是會影響使用者的編程思維的。然而對于那些以為使用面向?qū)ο笳Z言就是在面向?qū)ο蟮呐笥褌儯眠@句話告誡他們卻是十分合適的。一般來說,在使用面向?qū)ο笳Z言的時候我們總會偏向于用面向?qū)ο蟮乃季S進行程序設(shè)計,而在使用像C語言這樣的結(jié)構(gòu)化語言(或者稱面向過程語言)的時候,我們會偏向于進行結(jié)構(gòu)化的設(shè)計。然而萬事無絕對,如果你在使用面向?qū)ο笳Z言的時候?qū)懥艘欢裞lass,而每個class內(nèi)部的屬性和方法都亂七八糟,明明應(yīng)該A干的事情卻給放到了B中,那顯然是稱不上面向?qū)ο蟮模欢绻谑褂肅語言的時候使用了諸如struct這樣的數(shù)據(jù)結(jié)構(gòu),合理地在其內(nèi)部封裝了一些表示狀態(tài)的變量和指向某類操作的函數(shù)指針,那這怎么就不是面向?qū)ο罅四兀渴聦嵣螼bjective-C中的類啊對象啊其實都只是struct的類型別名。
(繼承,多態(tài)),封裝
對于所謂的面向?qū)ο笕蠡咎卣髂兀乙灿幸稽c自己的看法。大家總把這三者放在一起講,其實非常別扭。繼承與多態(tài)應(yīng)該是同一個層級的東西,因為在大多數(shù)面向?qū)ο笳Z言中,只要出現(xiàn)了多態(tài),多半會出現(xiàn)繼承(反之并不成立)。因為多態(tài)其實很簡單,語意上是指接口的多種不同實現(xiàn)方式,在面向?qū)ο笳Z言中我們最常用的是子類型多態(tài):允許將子類當作父類使用,在調(diào)用同一個方法時會產(chǎn)生不同效果(注:鴨子類型的多態(tài)和利用范型實現(xiàn)的參數(shù)多態(tài)根本就不需要繼承)。
而封裝呢,顯然是一個更大的話題,它的字面意思類似于“打包”,但又不是簡單地把一堆東西放一起就可以了。我們可以說封裝一個類,也可以說封裝一個模塊。既然要把東西“封”起來,那自然密不透風,也就是要隱藏實現(xiàn)的細節(jié),這就是一層“抽象屏障”,而封起來之后和外界的聯(lián)系很少,這就要保證模塊(或類)之間的松耦合;再者,既然“裝”到了一起,那整個模塊顯然可以看作是一個整體,可以對外提供統(tǒng)一的接口,而作為一個整體,內(nèi)部的狀態(tài)和過程應(yīng)該是有緊密聯(lián)系的,也就要保證模塊內(nèi)部的高聚合。所以個人認為,在談到封裝的時候,我們的理解不應(yīng)該僅僅局限于對訪問權(quán)限的控制(使用private關(guān)鍵字),而至少應(yīng)該包括如下幾點:
- 構(gòu)建抽象屏障,隱藏實現(xiàn)細節(jié)
- 保證不同模塊之間的松耦合
- 保證每個模塊內(nèi)部的高聚合
- 對外提供簡單易用的統(tǒng)一接口
雖然大家都嚷嚷著繼承封裝多態(tài)是面向?qū)ο笕兀娝苤趯嶋H項目中,濫用繼承會讓模塊間的耦合變緊,使整個項目變得很難維護,在某種程度上看,繼承跟封裝甚至是有一點對立的。所以業(yè)界有個好評如潮的最佳實踐,甚至有人把它當做一個設(shè)計原則,那就是:少用繼承,多用組合。這些其實都是有道理的,不過我個人覺得,如果真正做好了封裝這一點,那就已經(jīng)比很多號稱精通面向?qū)ο缶幊毯透鞣N設(shè)計模式的人要強了。
一個面向?qū)ο蟪绦蛟O(shè)計實例
上面說了這么多,我覺得還是舉個小例子說明一下面向?qū)ο笫且环N通用的設(shè)計思想,而不是C++、Java等面向?qū)ο笳Z言所獨有的。我們先來看一張經(jīng)常拿來做例子的表格:
我用Swift來演示,會有兩個版本來實現(xiàn)同樣的功能。第一個版本使用protocol、class等面向?qū)ο蟮恼Z言特性,第二個版本只使用函數(shù),但兩個版本都使用了面向?qū)ο蟮脑O(shè)計思維。
第一個:
protocol Animal {
func speak()
func run()
func eat()
}
class Cat: Animal {
func speak() {
print("喵~")
}
func run() {
print("不跑,貓步走起")
}
func eat() {
print("優(yōu)雅地吃飯")
}
}
class Dog: Animal {
func speak() {
print("單身汪的日常,汪汪~")
}
func run() {
print("撒腿狂奔")
}
func eat() {
print("狼吞虎咽")
}
}
func actAsAnimal(animal: Animal) {
animal.speak()
animal.run()
animal.eat()
}
像java、C#這樣的語言是不允許聲明全局函數(shù)的,所以我這邊這個actAsAnimal
函數(shù)可能在有些人眼中顯得不那么面向?qū)ο螅悄憧梢栽傩陆▊€class,然后把這個函數(shù)放進去。我就不放了,因為在這個例子中這樣做實在很多余。好了,我們看看調(diào)用效果:
let cat = Cat()
actAsAnimal(cat)
print("-----------")
let dog = Dog()
actAsAnimal(dog)
輸出:
喵~
不跑,貓步走起
優(yōu)雅地吃飯
-----------
單身汪的日常,汪汪~
撒腿狂奔
狼吞虎咽
這里其實就用到了多態(tài),但卻沒有用到繼承,而是用了協(xié)議(protocol
),相當于別的語言中的接口(interface
)。這有什么好處呢?在這個小例子中自然是看不出什么好處的,長遠來看,由于Cat
與Dog
是完全解耦的,而且它們的具體實現(xiàn)跟Animal
也沒太大關(guān)系,這樣不管是調(diào)試還是日后進行擴展,都會方便很多。
接下來第二個版本,我用純函數(shù)的方式來實現(xiàn)差不多的效果:
typealias Animal = (String) -> ()
func animal(name: String) -> Animal {
func cat(action: String) {
switch action {
case "speak":
print("喵~")
case "run":
print("不跑,貓步走起")
case "eat":
print("優(yōu)雅地吃飯")
default:
print("喵星人的注視")
}
}
func dog(action: String) {
switch action {
case "speak":
print("單身汪的日常,汪汪~")
case "run":
print("撒腿狂奔")
case "eat":
print("狼吞虎咽")
default:
print("汪星人的注視")
}
}
func unknownAnimal(action: String) {
print("I can not \(action)")
}
switch name {
case "cat":
return cat
case "dog":
return dog
default:
return unknownAnimal
}
}
func actAsAnimal(animal: Animal) {
animal("speak")
animal("run")
animal("eat")
}
現(xiàn)在我們調(diào)用一下看看:
let cat = animal("cat")
actAsAnimal(cat)
print("-------")
let dog = animal("dog")
actAsAnimal(dog)
輸出:
喵~
不跑,貓步走起
優(yōu)雅地吃飯
-------
單身汪的日常,汪汪~
撒腿狂奔
狼吞虎咽
輸出完全一致,而且調(diào)用起來也跟第一個版本差不太多,如果不看我的實現(xiàn)的話,你會想到這是用純函數(shù)實現(xiàn)的么?所以你們覺得這是不是面向?qū)ο竽兀课矣X得是的,更具體一點說,這其實是消息傳遞的編程風格,我自定義一個表示發(fā)送消息的操作符,大家可能看得更清楚些:
//消息傳遞符號
infix operator <- {
//左結(jié)合
associativity left
//乘除優(yōu)先級150,加減優(yōu)先級140
precedence 130
}
func <-(lhs: Animal, rhs: String) {
lhs(rhs)
}
這樣一來我們就可以把actAsAnimal
函數(shù)改成這樣:
func actAsAnimal(animal: Animal) {
animal <- "speak"
animal <- "run"
animal <- "eat"
}
以speak
為例解釋一下:把animal
看成一個對象,然后給他發(fā)送一條消息"speak"
,然后animal
就會在內(nèi)部處理"speak"
這個消息,會根據(jù)它真實的“類型”(其實cat
和dog
是兩個不同的函數(shù))執(zhí)行不同的操作,如果是cat
就打印“喵~”,是dog
就打印“單身汪的日常,汪汪~”。
當然這里我說的消息傳遞是指一種廣義上的編程風格,跟OC中的消息傳遞稍微有點不同,OC中的消息傳遞是基于一個類似于字典的數(shù)據(jù)結(jié)構(gòu)和函數(shù)指針實現(xiàn)的。另外,我覺得OC中實現(xiàn)繼承的方式其實就是組合,挺有趣的,下次我準備寫寫這方面的東西。
關(guān)于并發(fā)和函數(shù)式編程(FP)
近些年函數(shù)式編程又火了起來,很大程度上是因為面向?qū)ο筮@種編程范式在并發(fā)領(lǐng)域?qū)嵲陲@得過于繁瑣,畢竟可變的狀態(tài)在并發(fā)時非常難以控制,而沒有賦值沒有變量的函數(shù)式編程就沒有這種煩惱。函數(shù)式編程不像面向?qū)ο缶幊棠敲粗庇^,也不能用一堆隱喻來幫助你理解它,所以有人覺得它比較高深莫測。
中二的我認為啊,這兩種范式幾乎代表了兩種不同的世界觀,面向?qū)ο蟮氖澜缰校涑庵粋€個對象,它們是離散的。在這個世界中,本身是不存在“時間”這個概念的,對象會以不同的狀態(tài)來對應(yīng)現(xiàn)實中的不同時間點,你需要管理每個對象的狀態(tài)。而在并發(fā)的情況下,就如同出現(xiàn)了多個“平行世界”,而要命的是這些“平行時間”并不完全平行,有一些人(線程共享變量)可能同時生活在許多個世界,他們會被不同世界影響,也會影響不同世界的歷史進程,顯然這很不安全容易亂套,你需要制定一些規(guī)則(同步機制,譬如各種鎖),來保護這些“跨世界人員”不被多個世界同時影響從而精神錯亂。而且你又必須小心翼翼確保各個世界都能正常發(fā)展,不能因為不恰當?shù)谋Wo機制致使某些推動歷史進程的關(guān)鍵人物被關(guān)進了小黑屋永無出頭之日(死鎖),要知道好幾個世界都在等著他帶領(lǐng)人民走向新時代啊!
而在函數(shù)式編程的世界中,如果你使用“流”來模擬時間(一種延時序列,參見《SICP》),那世界連同時間都是一個整體,一切都已注定,一切都無法改變。你就像攤開了一張恢宏的歷史畫卷,每個時間點的世界都在其中,你的眼睛看到哪兒,歷史就發(fā)展到哪兒。就算出現(xiàn)了平行時空,它們也是互不干涉的。
唉說多了感覺有點民科啊,總之呢,不同的編程范式有不同的適用場景,不能說哪種范式就是錯的,畢竟光還存在波粒二象性呢。大體上來說,在需要頻繁改變狀態(tài)的時候,用面向?qū)ο蠛靡恍辉谛枰苊飧淖儬顟B(tài)的時候,用函數(shù)式編程好一些。不要把某種范式神話,但也不要把它妖魔化。
多謝看完我的一通胡言亂語,由于水平有限,如有錯漏,歡迎指教。