簡述
我認為"封裝"的概念在面向?qū)ο笏枷胫惺亲罨A(chǔ)的概念镰吆,它實質(zhì)上是通過將相關(guān)的一堆函數(shù)和一堆對象放在一起秀撇,對外有函數(shù)作為操作通道盈蛮,對內(nèi)則以變量作為操作原料。只留給外部程序員操作方式咖刃,而不暴露具體執(zhí)行細節(jié)。大部分書舉的典型例子就是汽車和燈泡的例子:你不需要知道不同車子的發(fā)動機原理憾筏,只要踩油門就可以跑嚎杨;你不需要知道你的燈泡是那種燈泡,打開開關(guān)就會亮氧腰。我們都會很直覺地認為這種做法非常棒枫浙,是吧?
但是有的時候還是會覺得有哪些地方不對勁古拴,使用面向?qū)ο笳Z言的時候箩帚,我隱約覺得封裝也許并沒有我們直覺中認為的那么好,也就是說黄痪,面向?qū)ο笃鋵嵅]有我們直覺中的那么好膏潮,雖然它已經(jīng)流行了很多很多年。
1. 將數(shù)據(jù)結(jié)構(gòu)和函數(shù)放在一起是否真的合理满力?
函數(shù)就是做事情的焕参,它們有輸入,有執(zhí)行邏輯油额,有輸出叠纷。 數(shù)據(jù)結(jié)構(gòu)就是用來表達數(shù)據(jù)的,要么作為輸入潦嘶,要么作為輸出涩嚣。
兩者本質(zhì)上是屬于完全不同的東西,面向?qū)ο笏枷雽⑺麄兎诺揭黄鸬嘟沟煤瘮?shù)的作用被限制在某一個區(qū)域里航厚,這樣做雖然能夠很好地將操作歸類,但是這種歸類方法是根據(jù)"作用領(lǐng)域"來歸類的锰蓬,在現(xiàn)實世界中可以幔睬,但在程序的世界中,有些不妥芹扭。
不妥的理由有如下幾個:
在并行計算時麻顶,由于執(zhí)行部分和數(shù)據(jù)部分被綁定在一起,這就使得這種方案制約了并行程度舱卡。在為了更好地實現(xiàn)并行的時候辅肾,業(yè)界的工程師們發(fā)現(xiàn)了一個新的思路:函數(shù)式編程。將函數(shù)作為數(shù)據(jù)來使用轮锥,這樣就能保證執(zhí)行的功能在時序上的正確性了矫钓。但你不覺得,只要把數(shù)據(jù)表達和執(zhí)行部分分開,形成流水線新娜,這不就能夠非常方便地將并行數(shù)提高了么赵辕?
我來舉個例子: 在數(shù)據(jù)和函數(shù)沒有分開時,程序的執(zhí)行流程是這樣:
A.function2() -> A.function3() 最后得到經(jīng)過處理的A
當處于并發(fā)環(huán)境時杯活,假設(shè)有這么多任務同時到達
A.f1() -> A.f2() -> A.f3() 最后得到經(jīng)過處理的A
B.f1() -> B.f2() -> B.f3() 最后得到經(jīng)過處理的B
C.f1() -> C.f2() -> C.f3() 最后得到經(jīng)過處理的C
D.f1() -> D.f2() -> D.f3() 最后得到經(jīng)過處理的D
E.f1() -> E.f2() -> E.f3() 最后得到經(jīng)過處理的E
F.f1() -> F.f2() -> F.f3() 最后得到經(jīng)過處理的F
...
假設(shè)并發(fā)數(shù)是3匆帚,那么完成上面類似的很多個任務熬词,時序就是這樣
| time | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|------|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
| A | A.1 | A.2 | A.3 | | | | | | | | | |
| B | B.1 | B.2 | B.3 | | | | | | | | | |
| C | C.1 | C.2 | C.3 | | | | | | | | | |
| D | | | | D.1 | D.2 | D.3 | | | | | | |
| E | | | | E.1 | E.2 | E.3 | | | | | | |
| F | | | | F.1 | F.2 | F.3 | | | | | | |
| G | | | | | | | G.1 | G.2 | G.3 | | | |
| H | | | | | | | H.1 | H.2 | H.3 | | | |
| I | | | | | | | I.2 | I.2 | I.3 | | | |
| J | | | | | | | | | | J.1 | J.2 | J.3 |
| K | | | | | | | | | | K.1 | K.2 | K.3 |
| L | | | | | | | | | | L.1 | L.2 | L.3 |
當數(shù)據(jù)和函數(shù)分開時旁钧,并發(fā)數(shù)同樣是3,就能形成流水線了互拾,有沒有發(fā)現(xiàn)吞吐量一下子上來了歪今?
| time | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11| 12|
|------|---|---|---|---|---|---|---|---|---|---|---|---|
| f1() | A | B | C | D | E | F | G | H | I | J | K | L |
| f2() | Z | A | B | C | D | E | F | G | H | I | J | K |
| f3() | Y | Z | A | B | C | D | E | F | G | H | I | J |
你要是粗看一下,誒颜矿?怎么到了第13個周期K才剛剛結(jié)束寄猩?上面一種方案在第12個周期的時候就結(jié)束了?不能這么看的哦骑疆,其實在12個周期里面田篇,Y、Z也已經(jīng)交付了箍铭。因為流水線吞吐量的提升是有過程的泊柬,我截取的片段應該是機器在持續(xù)運算過程中的一個片段。
我們不能單純地去看ABCD诈火,要看交付的任務數(shù)量兽赁。在12個周期里面,大家都能夠完成12個任務冷守,在11個周期里面刀崖,流水線完成了11個任務,前面一種只完成了9個任務拍摇,流水線的優(yōu)勢在這里就體現(xiàn)出來了:每個時間段都能穩(wěn)定地交付任務亮钦,吞吐量很大。而且并發(fā)數(shù)越多充活,跟第一種方案比起來的優(yōu)勢就越大或悲,具體的大家也可以通過畫圖來驗證。
數(shù)據(jù)部分就是數(shù)據(jù)部分堪唐,執(zhí)行部分就是執(zhí)行部分巡语,不同類的東西放在一起是不合適的
函數(shù)就是一個執(zhí)行黑盒,只要滿足函數(shù)調(diào)用的充要條件(給夠參數(shù))淮菠,就是能夠確定輸出結(jié)果的男公。面向?qū)ο笏枷雽⒑瘮?shù)和數(shù)據(jù)綁在一起,這樣的封裝擴大了代碼重用時的粒度。如果將函數(shù)和數(shù)據(jù)拆開枢赔,代碼重用的基本元素就由對象變?yōu)榱撕瘮?shù)澄阳,這樣才能更靈活更方便地進行代碼重用。
嗯踏拜,誰都經(jīng)歷過重用對象時碎赢,要把這個對象所依賴的所有東西都要移過來,哪怕你想用的只是這個對象里的一個方法速梗,然而很有可能你的這些依賴是跟你所需要的方法無關(guān)的肮塞。
但如果是函數(shù)的話,由于函數(shù)自身已經(jīng)是天然完美封裝的了姻锁,所以如果你要用到這個函數(shù)枕赵,那么這個函數(shù)所有的依賴你都需要,這才是合理的位隶。
2. 是否所有的東西都需要對象化拷窜?
面向?qū)ο笳Z言一直以自己做到"一切皆對象"為榮,但事實是:是否所有的東西都需要對象化涧黄?
在iOS開發(fā)中篮昧,有一個類叫做NSNumber,它封裝了所有數(shù)值:double笋妥,float懊昨,unsigned int, int...等等類型,在使用的時候它弱化了數(shù)值的類型挽鞠,使得非常方便疚颊。但問題也來了,計算的時候是不能直接對這個對象做運算的信认,你得把它們拆成數(shù)值材义,然后進行運算,然后再把結(jié)果變成NSNumber對象嫁赏,然后返回其掂。這是第一點不合理。第二點不合理的地方在于潦蝇,運算的時候你不知道原始數(shù)據(jù)的類型是什么款熬,拆箱裝箱過程中難免會導致內(nèi)存的浪費(比如原來uint8_t的數(shù)據(jù)變成unsigned int),這也十分沒有必要攘乒。
還有就是我們的file descriptor贤牛,它本身是一個資源的標識號,如果將資源抽象成對象则酝,那么不可避免的就會使得這個對象變得非常龐大殉簸,資源有非常多的用法,你需要將這些函數(shù)都放到對象里去。在真正傳遞資源的時候般卑,其實我們也只是關(guān)心資源標識而已武鲁,其它的真的無需關(guān)心。
我們已經(jīng)有函數(shù)作為黑盒了蝠检,拿著數(shù)據(jù)塞到黑盒里就夠了沐鼠。
3. 類型爆炸
由于數(shù)據(jù)和函數(shù)綁定到了一起,在邏輯上有派生關(guān)系的兩種對象往往可以當作一種叹谁,以派生鏈最上端的那個對象為準饲梭。單純地看這個現(xiàn)象直覺上會覺得非常棒,父親有的兒子都有本慕。但在實際工程中排拷,派生是非常不好控制的侧漓,它導致同一類類型在工程中泛濫:ViewController锅尘、AViewController、BViewController布蔗、ThisViewController藤违、ThatViewController...
你有沒有發(fā)現(xiàn),一旦把執(zhí)行和數(shù)據(jù)拆解開纵揍,就不需要這么多ViewController了顿乒,派生只是給對象添加屬性和方法。但事實上是這樣:
struct A { Class A extends B
struct B b; {
int number; int number;
} {
前者和后者的相同點是:在內(nèi)存中泽谨,它們的數(shù)值部分的布局是一模一樣的璧榄。不同點是:前者更強烈地表達了組合,后者更強烈地表達的是繼承吧雹。然而我們都知道一個常識:組合要比繼承更加合適骨杂,這在我這一系列的第一篇文章中有提到。
上兩者的表達在內(nèi)存中沒有任何不同雄卷,但在實際開發(fā)階段中搓蚪,后者會更容易把項目引入一個壞方向。
總結(jié)
為什么面向?qū)ο髸绱肆餍卸○模课蚁肓艘幌聵I(yè)界關(guān)于這個談論的最多的是以下幾點:
- 它能夠非常好地進行代碼復用
- 它能夠非常方便地應對復雜代碼
- 在進行程序設(shè)計時妒潭,面向?qū)ο蟾臃铣绦騿T的直覺
第一點在理論上確實成立,但實際上大家都懂揣钦,在面向?qū)ο蟮拇蟊尘跋脉ㄔ郑瑢懸欢伪阌趶陀玫拇a比面向過程背景下難多了。關(guān)于第二點冯凹,你不覺得正是面向?qū)ο蠡涯叮虐压こ套儚碗s的么?如果層次清晰,調(diào)用規(guī)范团驱,無論面向?qū)ο筮€是面向過程摸吠,處理復雜業(yè)務都是一樣好,等真的到了非常復雜的時候嚎花,對象間錯綜復雜的關(guān)系只會讓你處理起來更加頭疼寸痢,不如面向過程來得簡潔。關(guān)于第三點紊选,這其實是一個障眼法啼止,因為無論面向什么的設(shè)計,最終落實下來兵罢,還是要面向過程的献烦,面向?qū)ο笾皇窃谔幚碚{(diào)用關(guān)系時符合直覺,在架構(gòu)設(shè)計時卖词,理清需求是第一步巩那,理清調(diào)用關(guān)系是第二步,理清實現(xiàn)過程是第三步此蜈。面向?qū)ο笞屇阍诘诙綍r就產(chǎn)生了設(shè)計完成的錯覺即横,只有再往下落地到實現(xiàn)過程的時候,你才會發(fā)現(xiàn)第二步中都有哪些錯誤裆赵。
所以綜上所述东囚,我的觀點是:面向?qū)ο笫窃诩軜?gòu)設(shè)計時非常好的思想,但如果只是簡單映射到程序?qū)崿F(xiàn)上來战授,引入的缺點會讓我們得不償失页藻。
后記
距離上一次博文更新已經(jīng)快要一個月了,不是我偷懶植兰,實在是太忙份帐,現(xiàn)在終于有時間可以把"跳出面向?qū)ο?系列完成了。針對面向?qū)ο蟮?個支柱概念我寫了三篇文章來挑它的刺钉跷,看上去有一種全盤否定的感覺弥鹦,而我倒不至于希望大家回去下一個項目就開始面向過程的開發(fā),我希望大家能夠針對這一系列文章提出的面向?qū)ο蟮谋锥艘蓿瑖栏褚?guī)范代碼的行為彬坏,知道哪些可行哪些不可行。過去的工作中我深受其苦膝晾,往往沒有時間去詳細解釋為什么這么直覺的東西實際上不可行栓始,要想解釋這些東西就得需要各種長篇大論。最痛苦的是血当,即便長篇大論說完了幻赚,最后對方還無法理解禀忆,照樣寫出垃圾代碼出來害人。
現(xiàn)在好了落恼,長篇大論落在紙上了箩退,說的時候聽不懂,回去總可以翻文章慢慢理解了吧佳谦。