面向?qū)ο缶幊淌且环N處理復(fù)雜問(wèn)題的設(shè)計(jì)工具,本身沒(méi)有什么好壞之分,只有用的好壞之分。但面向?qū)ο蟮膯?wèn)題在于長(zhǎng)期以來(lái)的技術(shù)環(huán)境、編程語(yǔ)言、一些工具的推廣、培訓(xùn)和教育都大大的過(guò)分樂(lè)觀的強(qiáng)調(diào)了面向?qū)ο缶幊瘫旧砜梢詭?lái)的好處。以至于很多學(xué)習(xí)編程的人都深深的相信“只要用了面向?qū)ο缶幊蹋ㄒ约盎谄浠A(chǔ)之上的的一系列設(shè)計(jì)模式、規(guī)范、工具、框架),就能得到非常容易維護(hù)、可以復(fù)用、明晰可理解的代碼“。
但,這并不是真的。
如果你經(jīng)歷過(guò)很多,就會(huì)發(fā)現(xiàn)“只要如何如何,就一定能如何如何”這個(gè)提法一旦出現(xiàn),基本上就不靠譜,不管是編程還是別的什么事情。
在大量的場(chǎng)景中,可以偏執(zhí)的認(rèn)為“萬(wàn)物皆對(duì)象”(或者萬(wàn)物皆別的什么),但是哲學(xué)上的單純并不一定能讓現(xiàn)實(shí)中的工程變得更“好”。如果說(shuō)非得有個(gè)“萬(wàn)物皆XX”,那么這個(gè)XX八成就是根據(jù)眾多需求綜合到一起的“折衷”。
簡(jiǎn)單從工程講的話,如果程序(或者說(shuō)工作)是一次性的,那么怎么寫(xiě)得快,能work就怎么來(lái)。這個(gè)相對(duì)好理解。但是,如果程序是要長(zhǎng)期維護(hù)的,那么如何管理其復(fù)雜性是核心的問(wèn)題。而管理復(fù)雜性的要點(diǎn)在于
- 讓事情本身變得簡(jiǎn)單。這說(shuō)白了就是砍需求,研發(fā)和PM之間要經(jīng)常溝通去避免nicetohave的需求變動(dòng)帶來(lái)的程序復(fù)雜性的劇烈變化(比如一個(gè)1對(duì)1的實(shí)體關(guān)系,需求變動(dòng)一點(diǎn)就變成了麻煩的多的“有時(shí)1對(duì)1,有時(shí)1對(duì)多”的混合關(guān)系)。
- 運(yùn)用隔離的手段將復(fù)雜性拆解為互相影響很小的單元。一個(gè)單元對(duì)外只暴露一個(gè)簡(jiǎn)單的“接口”,隱藏內(nèi)部復(fù)雜性。這就是“抽象”或者“封裝“的力量。但是問(wèn)題在于,這個(gè)抽象本身是否做的合適是由于問(wèn)題決定的,而不是代碼本身決定的。
即便是抽象,也有很多種做法。可以定義一組接口,這個(gè)接口是一組函數(shù)、一組服務(wù)的RPC還是一個(gè)class的publicmethod都可以根據(jù)實(shí)際情況商討。面向?qū)ο笾皇沁@里面其中一種做法而已。一個(gè)想要把程序編好的人,需要注重的是理解問(wèn)題,然后嘗試做出幾種不同的抽象,評(píng)估各自優(yōu)缺點(diǎn)后得到一個(gè)當(dāng)時(shí)可行解的能力。而現(xiàn)有的大環(huán)境、教育體系,沒(méi)有那么多真實(shí)的、復(fù)雜的案例,只能用一些簡(jiǎn)單的samplecode來(lái)教授。并且在說(shuō)明問(wèn)題本身時(shí),簡(jiǎn)化問(wèn)題本身,而突出代碼設(shè)計(jì)的“模式”。這就好像是在用視頻教人游泳一樣。學(xué)習(xí)者自己需要認(rèn)識(shí)到這些培訓(xùn)只是個(gè)參考,玩真的還是要到項(xiàng)目里去體會(huì)。
即便是用面向?qū)ο笞龀橄笠矔?huì)有問(wèn)題。很多時(shí)候,面向?qū)ο缶幊滩⒉皇且环N好的“抽象”。如果抽象做得好,透過(guò)抽象出來(lái)的“接口”就可以輕易的使用這個(gè)系統(tǒng)。這時(shí)“大量的復(fù)雜性”被隱藏到接口后的實(shí)現(xiàn)里。這就像是你看電視從來(lái)都不需要拆開(kāi)殼子看里面液晶屏幕和視頻信號(hào)的轉(zhuǎn)換,只需要知道【電源】、【調(diào)臺(tái)】、【調(diào)音量】就能用。一個(gè)抽象做得好,往往要“deep”,隱藏足夠的復(fù)雜度。而面向?qū)ο蟮奈幕?教育往往會(huì)鼓勵(lì)程序員做很多無(wú)意義的,無(wú)性價(jià)比的抽象。看看有些代碼里完全不知所云的adaptor,factory,builder等就是這種做法的產(chǎn)物。
此外,在大量使用繼承作為設(shè)計(jì)方法時(shí),也沒(méi)有起到任何實(shí)質(zhì)的隔離作用。如果你嘗試擴(kuò)展一個(gè)繼承體系,往往需要了解整個(gè)繼承體系才能寫(xiě)對(duì)代碼——這時(shí),復(fù)雜性并沒(méi)有被隱藏起來(lái)。你也許只是代碼寫(xiě)的少了而已。對(duì)于這種復(fù)雜度沒(méi)有降低,編寫(xiě)代碼只是寫(xiě)的少,但是要看懂還是得結(jié)合整個(gè)體系才能做到的方式,不是抽象,是“壓縮”。壓縮只能少寫(xiě)代碼,卻會(huì)讓系統(tǒng)更難以理解了。
也許不太容易理解壓縮在這里意思。比如在一段被壓縮的數(shù)據(jù)中有3個(gè)bytes是“A”,“1”,“8”。但是他們的意思可能是A連續(xù)出現(xiàn)18次,也許是A1連續(xù)出現(xiàn)8次。至于到底是哪個(gè)意思,必須從頭讀所有的數(shù)據(jù)才能弄明白。編碼也是這個(gè)道理。
再說(shuō)說(shuō)類型本身。一些面向?qū)ο缶幋a對(duì)類型的定義要求的比較嚴(yán)格。其本質(zhì)假設(shè)是“如果一個(gè)Object的類型是XXXX”,則其行為模式必然是“YYYY”。但現(xiàn)實(shí)當(dāng)中,一個(gè)Object的行為模式不光與他的類型有關(guān),還與這個(gè)Object“如何被使用”有關(guān)。比方說(shuō),一個(gè)User的Object,如果是用戶自己看自己,就可以登陸、登出,修改昵稱;如果是其他普通用戶看,就只能看看看昵稱和頭像;如果是管理員來(lái)操作,可以reset密碼、注銷或者踢出登陸。這時(shí)就得界定一個(gè)Scope,來(lái)說(shuō)明現(xiàn)在的User到底是哪個(gè)scope的User。DDD的一些理念就源自于此——找到某個(gè)上下文的某個(gè)實(shí)體概念,不能有歧義。但是即便不用DDD,也必須用各種變通的手段,把“如何用”的信息與類型信息結(jié)合到一起來(lái)實(shí)現(xiàn)邏輯。很郁悶的是,這個(gè)“如何用”完全沒(méi)有章法,可能是“iOSApp登陸“,也可能是“第一次下單時(shí)”,或者是“系統(tǒng)處于降級(jí)狀態(tài)”時(shí)。你永遠(yuǎn)也猜不到下一次可能會(huì)有個(gè)什么條件是要納入到上下文的。大家都知道大量用if不好,容易讓代碼變成麻花,無(wú)法維護(hù)。但面向?qū)ο缶幊瘫旧頉](méi)解決這個(gè)問(wèn)題。很多文章提出面向?qū)ο竽硞€(gè)模式可以少寫(xiě)if,讓代碼容易維護(hù)。但是這其實(shí)是建立在那個(gè)問(wèn)題的上下文已經(jīng)明確的基礎(chǔ)之上。上下文易變的問(wèn)題沒(méi)有解決,換一個(gè)上下文,招數(shù)便不靈了,到時(shí)還得處理一坨“模式代碼”,非常惡心。
最后,面向?qū)ο髸?huì)傾向于將不同的代碼抽象為不同相互作用的Object,但是有一些現(xiàn)實(shí)因素會(huì)讓這么面向?qū)ο蟮玫椒浅2焕硐氲男Ч?/p>
- 安全-如果你的代碼要求非常安全,那么所有的Object都要耦合安全控制的代碼;要不就是在一層對(duì)外的接口之前攔截一道處理安全問(wèn)題,內(nèi)部Object都無(wú)視安全問(wèn)題。這也就相當(dāng)于放棄了一部分的安全性。
- 性能-如果強(qiáng)調(diào)性能的話,是要盡量減少隔離的層次的。無(wú)論抽象如何做,只要隔離發(fā)生,就要經(jīng)歷一次轉(zhuǎn)換以及相應(yīng)的性能損耗。比如早期的Hibernate不支持“bulkinsert”和“bulkupdate”,只能逼著程序員做forloopIO;而native的sql卻可以輕易辦到。在每多一次IO都很傷的場(chǎng)景下,這種隔離只能把事情做的更糟。
- 數(shù)據(jù)為中心-很多業(yè)務(wù)場(chǎng)景都是以數(shù)據(jù)為中心。也就是說(shuō)DB里的那坨數(shù)據(jù)是唯一的truth。在代碼層面做的只是為處理數(shù)據(jù)更加方便。這時(shí)做的很多抽象意義不大。比如你可以在ORM層強(qiáng)制聲明讀取出來(lái)的一個(gè)數(shù)據(jù)少了某個(gè)字段是invalid的。但是你沒(méi)法阻止你的第三方數(shù)據(jù)提供商源給你invalid的數(shù)據(jù)。對(duì)Invalid數(shù)據(jù)的處理遠(yuǎn)不是一個(gè)Annotation就能搞定的,必須引入復(fù)雜的業(yè)務(wù)流程。
- 靈活性和成本-每次做某種抽象都意味著對(duì)一個(gè)系統(tǒng)“要做某種變化的能力做出優(yōu)化”,但是同時(shí),也就意味著或多或少對(duì)其他種變化適應(yīng)性做“劣化“。如果系統(tǒng)變化的方向和預(yù)期的不一致,那么浪費(fèi)掉的工作不說(shuō),為了再次調(diào)整設(shè)計(jì)方向的代價(jià)也會(huì)相當(dāng)?shù)拇蟆_@種情況比比皆是。
總結(jié)下,我希望所有的程序員都要理解自己的工作的最終目的是干什么的,并且活用自己所能用到的一切工具來(lái)達(dá)成自己的目標(biāo)。不要在各種編程范式里迷了路。如果是初學(xué)編程的人,我衷心的希望你的編程課程講授的是解決一些實(shí)際的問(wèn)題,多了解業(yè)務(wù),多嘗試對(duì)業(yè)務(wù)的變動(dòng)作出合理和準(zhǔn)確的預(yù)。不要過(guò)早的接觸高層的思想和哲學(xué)層面的問(wèn)題——一個(gè)小孩看《紅樓夢(mèng)》又能真的看懂多少呢。