一個技術(shù)總監(jiān)的忠告:精通那么多技術(shù)為何還是做不好一個項目?
編寫高質(zhì)量可維護的代碼既是程序員的基本修養(yǎng),也是能決定項目成敗的關(guān)鍵因素,本文試圖總結(jié)出問題項目普遍存在的共性問題并給出相應(yīng)的解決方案。
1. 程序員的宿命?
程序員的職業(yè)生涯中難免遇到爛項目,有些項目是你加入時已經(jīng)爛了,有些是自己從頭開始親手做成了爛項目,有些是從里到外的爛,有些是表面光鮮等你深入進去發(fā)現(xiàn)是個“焦油坑”,有些是此時還沒爛但是已經(jīng)出現(xiàn)問題征兆走在了腐爛的路上。
國內(nèi)基本上是這樣,國外情況我了解不多,不過從英文社區(qū)和技術(shù)媒體上老外同行的抱怨程度看,應(yīng)該是差不多的,雖然整體素質(zhì)可能更高,但是也因更久的信息化而積累了更多問題。畢竟“焦油坑、Shit_Mountain 屎山”這些舶來的術(shù)語不是無緣無故被發(fā)明出來的。
Any way,這大概就是我們這個行業(yè)的宿命——要么改行,要么就是與爛項目爛代碼長相伴。就像宇宙的“熵增加定律”一樣:
孤立系統(tǒng)的一切自發(fā)過程均向著令其狀態(tài)更無序的方向發(fā)展,如果要使系統(tǒng)恢復(fù)到原先的有序狀態(tài)是不可能的,除非外界對它做功。
面對這宿命的陰影,有些人認(rèn)命了麻木了,逐漸對這個行業(yè)失去熱情。
那些不認(rèn)命的選擇與之抗?fàn)?,但是地上并沒有路,當(dāng)年軟件危機的陰云也從未真正散去,人月神話仍然是神話,于是人們做出了各自不同的判斷和嘗試:
掀桌子另起爐灶派:
- 很多人把項目做爛的原因歸咎于項目前期的基礎(chǔ)沒打好、需求不穩(wěn)定一路打補丁、前面的架構(gòu)師和程序員留下的爛攤子難以收拾。
- 他們要么沒有信心去收拾爛攤子,要么覺得這是費力不討好,于是要放棄掉項目,寄希望于出現(xiàn)一個機會能重頭再來。
- 但是他們對于如何避免重蹈覆轍、做出另一個爛項目是沒有把握也沒有深入思考的,只是盲目樂觀的認(rèn)為自己比前任更高明。
激進改革派:
- 這個派別把原因歸結(jié)于爛項目當(dāng)初沒有采用正確的編程語言、最新最強大的技術(shù)?;蚬ぞ摺?/li>
- 他們中一部分人也想著有機會另起爐灶,用上時下最流行最熱門的技術(shù)棧(spring boot、springcloud、redis、nosql、docker、vue)。
- 或者即便不另起爐灶,也認(rèn)為現(xiàn)有技術(shù)棧太過時無法容忍了(其實可能并不算過時),不用微服務(wù)不用分布式就不能接受,于是激進的引入新技術(shù)棧,魯莽的對項目做大手術(shù)。
- 這種對剛剛流行還不成熟技術(shù)的盲目跟風(fēng)、技術(shù)選型不慎重的情況非常普遍,今天在他們眼中落伍的技術(shù)棧,其實也不過是幾年前另一批人趕的時髦。
- 我不反對技術(shù)上的追新,但是同樣的,這里的問題是:他們對于大手術(shù)的風(fēng)險和副作用,對如何避免重蹈覆轍用新技術(shù)架構(gòu)做出另一個爛項目,沒有把握也沒有深入思考的,只是盲目樂觀的認(rèn)為新技術(shù)能帶來成功。
- 也沒人能阻止這種簡歷驅(qū)動的技術(shù)選型浮躁風(fēng)氣,畢竟花的是公司的資源,用新東西顯得自己很有追求,失敗了也不影響簡歷美化,簡歷上只會增加一段項目履歷和幾種精通技能,不會提到又做爛了一個項目,名利雙收穩(wěn)賺不賠。
保守改良派:
- 還有一類人他們不愿輕易放棄這個有問題但仍在創(chuàng)造效益的項目,因為他們看到了項目仍然有維護的價值,也看到了另起爐灶的難度(萬事開頭難,其實項目的冷啟動存在很多外部制約因素)、大手術(shù)對業(yè)務(wù)造成影響的代價、系統(tǒng)遷移的難度和風(fēng)險。
- 同時他們嘗試用溫和漸進的方式逐步改善項目質(zhì)量,采用一系列工程實踐(主要包括重構(gòu)熱點代碼、補自動化測試、補文檔)來清理“技術(shù)債”,消除制約項目開發(fā)效率和交付質(zhì)量的瓶頸。
如果把一個問題項目比作病入膏肓的病人,那么這三種做法分別相當(dāng)于是放棄治療、截肢手術(shù)、保守治療。
2. 一個 35+ 程序員的反思
年輕時候我也是掀桌子派和激進派的,新工程新框架大開大合,一路走來經(jīng)驗值技能樹蹭蹭的漲,跳槽加薪好不快活。
但是近幾年隨著年齡增長,一方面新東西學(xué)不動了,另一方面對經(jīng)歷過的項目反思的多了觀念逐漸改變了。
對我觸動最大的一件事是那個我在 2016 年初開始從零搭建起的項目,在我 2018 年底離開的時候(僅從代碼質(zhì)量角度)已經(jīng)讓我很不滿意了。只是,這一次沒有任何借口了:
- 從技術(shù)選型到架構(gòu)設(shè)計到代碼規(guī)范,都是我自己做的,團隊不大,也是我自己組建和一手帶出來的;
- 最開始的半年進展非常順利,用著我最趁手的技術(shù)和工具一路狂奔,年底前替換掉了之前采購的那個垃圾產(chǎn)品(對的,有個前任在業(yè)務(wù)上做參照也算是個很大的有利因素);
- 做的過程我也算是全力以赴,用盡畢生所學(xué)——前面 13 年工作的經(jīng)驗值和走過的彎路、教訓(xùn),使得公司只用其它同類公司同類項目 20% 的資源就把平臺做起來了;
- 如果說多快好省是最高境界,那么當(dāng)時的我算是做到了多、快、省——交付的功能非常豐富且貼近業(yè)務(wù)需求、開發(fā)節(jié)奏快速、對公司開發(fā)資源很節(jié)省;
- 但是現(xiàn)在看來,“好”就遠遠沒有達到了,到了項目中期,簡單優(yōu)先級高的需求都已經(jīng)做完了,公司業(yè)務(wù)上出現(xiàn)了新的挑戰(zhàn)——接入另一個核心系統(tǒng)以及外部平臺,真正的考驗來了。
- 那個改造工程影響面比較大,需要對我們的系統(tǒng)做大面積修改,最麻煩的是這意味著從一個簡單的單體系統(tǒng)變成了一個分布式的系統(tǒng),而且業(yè)務(wù)涉及資金交易,可靠性要求較高,是難上加難。
- 于是問題開始出現(xiàn)了:我之前架構(gòu)的優(yōu)點——簡單直接——這個時候不再是優(yōu)點了,簡單直接的架構(gòu)在業(yè)務(wù)環(huán)境、技術(shù)環(huán)境都簡單的情況下可以做到多快好省,但是當(dāng)業(yè)務(wù)、技術(shù)環(huán)境都陡然復(fù)雜起來時,就不行了;
- 具體的表現(xiàn)就是:架構(gòu)和代碼層面的結(jié)構(gòu)都快速的變得復(fù)雜、混亂起來了——熵急劇增加;
- 后面的事情就一發(fā)不可收拾:代碼改起來越來越吃力、測試問題變多、生產(chǎn)環(huán)境故障和問題變多、于是消耗在排查測試問題生產(chǎn)問題和修復(fù)數(shù)據(jù)方面的精力急劇增加、出現(xiàn)惡性循環(huán)。。。
- 到了這個境地,項目就算是做爛了!一個我從頭開始做起的沒有任何借口的失敗!
于是我意識到一個非常淺顯的道理:擁有一張空白的畫卷、一支最高級的畫筆、一間專業(yè)的畫室,無法保證你可以畫出美麗的畫卷。如果你不善于畫畫,那么一切都是空想和意淫。
然后我變成了一個“保守改良派”,因為我意識到掀桌子和激進的改革都是不負責(zé)任的,說不好聽的那樣其實是掩耳盜鈴、逃避困難,人不可能逃避一輩子,你總要面對。
即便掀了桌子另起爐灶了,你還是需要找到一種辦法把這個新的爐灶燒好,因為隨著項目發(fā)展之前的老問題還是會一個一個冒出來,還是需要面對現(xiàn)實、不逃避、找辦法。
面對問題不僅有助于你把當(dāng)前項目做好,也同樣有助于將來有新的項目時更好的把握住機會。
無論是職業(yè)生涯還是自然年齡,人到了這個階段都開始喜歡回顧和總結(jié),也變得比過去更在乎項目、產(chǎn)品乃至公司的商業(yè)成敗。
軟件開發(fā)作為一種商業(yè)活動,判斷其成敗的依據(jù)應(yīng)該是:能否以可接受的成本、可預(yù)期的時間節(jié)奏、穩(wěn)定的質(zhì)量水平、持續(xù)交付滿足業(yè)務(wù)需要的功能市場需要的產(chǎn)品。
其實就是項目管理四要素——成本、進度、范圍、質(zhì)量,傳統(tǒng)項目管理理論認(rèn)為這四要素彼此制約難以兼得,項目管理的藝術(shù)在于四要素的平衡取舍。
關(guān)于軟件工程和項目管理的理論和著作已經(jīng)很多很成熟,這里我從程序員的視角提出一個新的觀點——質(zhì)量不可妥協(xié):
- 質(zhì)量要素不是一個可以被犧牲和妥協(xié)的要素——犧牲質(zhì)量會導(dǎo)致其它三要素全都受損,反之同理,追求質(zhì)量會讓你在其它三個方面同時受益。
- 在保持一個質(zhì)量水平的前提下,成本、進度、范圍三要素確確實實是互相制約關(guān)系——典型的比如犧牲成本(加班加點)來加快進度交付急需的功能。
- 正如著名的“破窗效應(yīng)”所啟示的那樣:任何一種不良現(xiàn)象的存在,都在傳遞著一種信息,這種信息會導(dǎo)致不良現(xiàn)象的無限擴展,同時必須高度警覺那些看起來是偶然的、個別的、輕微的“過錯”,如果對這種行為不聞不問、熟視無睹、反應(yīng)遲鈍或糾正不力,就會縱容更多的人“去打爛更多的窗戶玻璃”,就極有可能演變成“千里之堤,潰于蟻穴”的惡果——質(zhì)量不佳的代碼之于一個項目,正如一扇破了的窗之于一幢建筑、一個螞蟻巢之于一座大堤。
- 好消息是,只要把質(zhì)量提上去項目就會逐漸走上健康的軌道,其它三個方面也都會改善。管好了質(zhì)量,你就很大程度上把握住了項目成敗的關(guān)鍵因素。
- 壞消息是,項目的質(zhì)量很容易失控,現(xiàn)實中質(zhì)量不佳、越做越臃腫混亂的項目比比皆是,質(zhì)量改善越做越好的案例聞所未聞,以至于人們將其視為如同物理學(xué)中“熵增加定律”一樣的必然規(guī)律了。
- 當(dāng)然任何事情都有一個度的問題,當(dāng)質(zhì)量低于某個水平時才會導(dǎo)致其它三要素同時受損。反之當(dāng)質(zhì)量高到某個水平以后,繼續(xù)追求質(zhì)量不僅得不到明顯收益,而且也會損害其它三要素——邊際效用遞減定律。
- 這個度需要你為自己去評估和測量,如果目前的質(zhì)量水平還在兩者之間,那么就應(yīng)該重點改進項目質(zhì)量。當(dāng)然,現(xiàn)實世界中很少看到哪個項目質(zhì)量高到了不需要重視的程度。
3. 項目走向衰敗的最常見誘因——代碼質(zhì)量不佳
一個項目的衰敗一如一個人健康狀況的惡化,當(dāng)然可能有多種多樣的原因——比如需求失控、業(yè)務(wù)調(diào)整、人員變動流失。但是作為我們技術(shù)人,如果能做好自己分內(nèi)的工作——編寫出可維護的代碼、減少技術(shù)債利息成本、交付一個健壯靈活的應(yīng)用架構(gòu),那也絕對是功德無量的。
雖然很難估算出這究竟能挽救多少項目,但是在我十多年職業(yè)生涯中,經(jīng)歷的和近距離觀察的幾十個項目,確實看到了大量的項目正是由于代碼質(zhì)量不佳導(dǎo)致的失敗和遺憾,同時我也發(fā)現(xiàn)其實失敗項目的很多問題、癥結(jié)也確確實實都可以歸因到項目代碼的混亂和質(zhì)量低下,比如一個常見的項目腐爛惡性循環(huán):代碼亂》bug 多》排查問題耗時》復(fù)用度低》加班 996》士氣低落……
所謂“千里之堤,毀于蟻穴”,代碼問題就是蟻穴。
接下來,讓我們從項目管理聚焦到項目代碼質(zhì)量這個相對小的領(lǐng)域來深入剖析。編寫高質(zhì)量可維護的代碼是程序員的基本修養(yǎng),本文試圖在代碼層面找到一些失敗項目中普遍存在的癥結(jié)問題,同時基于個人十幾年開發(fā)經(jīng)驗總結(jié)出的一些設(shè)計模式作為藥方分享出來。
關(guān)于代碼質(zhì)量的話題其實很難通過一篇文章闡述明白,甚至需要一本書的篇幅,里面涉及到的很多概念關(guān)注點之間存在復(fù)雜微妙關(guān)系。
推薦《設(shè)計模式之美》的第二章節(jié)《從哪些維度評判代碼質(zhì)量的好壞?如何具備寫出高質(zhì)量代碼的能力?》,這是我看到的關(guān)于代碼質(zhì)量主題最精彩深刻的論述。
4. 一個失敗項目復(fù)盤
先貼幾張代碼截圖,看一下這個重病纏身的項目的病灶和癥狀:
- 這是該項目中一個最核心、最復(fù)雜也是最經(jīng)常要被改動的 class,代碼行數(shù) 4881;
- 結(jié)果就是冗長的 API 列表(列表需要滾動 4 屏才能到底,公有私有 API 180 個);
- 還是那個 Class,頭部的 import 延綿到了 139 行,去掉第一行 package 聲明和少量空行總共 import 引入了 130 個 class!
- 還是那個坑爹的組件,從 156 行開始到 235 行聲明了 Spring 依賴注入的組件 40 個!
這里先不去分析這個類的問題,只是初步展示一下病情嚴(yán)重程度。
我相信這應(yīng)該不算是特別糟糕的情況,比這個嚴(yán)重的項目俯拾皆是,但是這也應(yīng)該足夠拿來暴露問題、剖析成因了。
4.1 癥結(jié) 1:組件粒度過大、API 泛濫
分層的理念早已深入人心,尤其是業(yè)務(wù)邏輯層的獨立,徹底杜絕了之前(不分層的年代)業(yè)務(wù)邏輯與展現(xiàn)邏輯、持久化邏輯等混雜的問題。
但是好景不長,隨著業(yè)務(wù)的復(fù)雜和變更,在業(yè)務(wù)邏輯層的復(fù)雜性也急劇增加,成為了新的開發(fā)效率瓶頸, 問題就出在了業(yè)務(wù)邏輯組件的劃分方式——按領(lǐng)域模型劃分業(yè)務(wù)邏輯組件:
- 業(yè)界關(guān)于如何設(shè)計業(yè)務(wù)邏輯層 并沒有標(biāo)準(zhǔn)和最佳實踐,絕大多數(shù)項目(我自己經(jīng)歷過的項目以及我有機會深入了解的項目)中大家都是想當(dāng)然的按照業(yè)務(wù)領(lǐng)域?qū)ο髞碓O(shè)計;
- 例如:領(lǐng)域?qū)嶓w對象有 Account、Order、Delivery、Campaign。于是業(yè)務(wù)邏輯層就設(shè)計出 AccountService、OrderService、DeliveryService、CampaignService
- 這種做法在項目簡單是沒什么問題,事實上項目簡單時 你隨便怎么設(shè)計都問題不大。
- 但是當(dāng)項目變大和復(fù)雜以后,就會出現(xiàn)問題了:
- 組件臃腫:Service 組件的個數(shù)跟領(lǐng)域?qū)嶓w對象個數(shù)基本相當(dāng),必然造成個別 Service 組件變得非常臃腫——API 非常多,代碼行數(shù)達到幾千行;
- 職責(zé)模糊:業(yè)務(wù)邏輯往往跨多個領(lǐng)域?qū)嶓w,無論放在哪個 Service 都不合適,同樣的,要找一個功能的實現(xiàn)邏輯也無法確定在哪個 Service 中;
- 代碼重復(fù) or 邏輯糾纏的兩難選擇:當(dāng)遇到一個業(yè)務(wù)邏輯,其中的某個環(huán)節(jié)在另一個業(yè)務(wù)邏輯 API 中已經(jīng)實現(xiàn),這時如果不想忍受重復(fù)實現(xiàn)和代碼,就只能去調(diào)用那個 API。但這樣就造成了業(yè)務(wù)邏輯組件之間的耦合與依賴,這種耦合與依賴很快會擴散——新的 API 又會被其它業(yè)務(wù)邏輯依賴,最終形成蜘蛛網(wǎng)一樣的復(fù)雜依賴甚至循環(huán)依賴;
- 復(fù)用代碼、減少重復(fù)雖然是好的,但是復(fù)雜耦合依賴的害處也很大——趕走一只狼引來了一只虎。兩杯毒酒給你選!
前面截圖的那個問題組件 ContractService 就是一個典型案例,這樣的組件往往是熱點代碼以及整個項目的開發(fā)效率的瓶頸。
4.2 藥方 1:倒金字塔結(jié)構(gòu)——業(yè)務(wù)邏輯組件職責(zé)單一、禁止層內(nèi)依賴
問題根源的反面其實就藏著解決方案,只是需要我們有意識的去改變習(xí)慣、遵循新的設(shè)計風(fēng)格,而不是憑直覺去設(shè)計:
- 業(yè)務(wù)邏輯層應(yīng)該被設(shè)計成一個個功能非常單一的小組件,所謂小是指 API 數(shù)量少、代碼行數(shù)少;
- 由于職責(zé)單一因此必然組件數(shù)量多,每一個組件對應(yīng)一個很具體的業(yè)務(wù)功能點(或者幾個相近的);
- 復(fù)用(調(diào)用、依賴)只應(yīng)該發(fā)生在相鄰的兩層之間——上層調(diào)用下層的 API 來實現(xiàn)對下層功能的復(fù)用;
- 于是系統(tǒng)架構(gòu)就自然呈現(xiàn)出倒立的金字塔形狀:越接近頂層的業(yè)務(wù)場景組件數(shù)量越多,越往下層的復(fù)用性高,于是組件數(shù)量越少。
4.3 癥結(jié) 2:低內(nèi)聚、高耦合
經(jīng)典面向?qū)ο罄碚摳嬖V我們,好的代碼結(jié)構(gòu)應(yīng)該是“高內(nèi)聚、低耦合”的:
- 高內(nèi)聚:組件本身應(yīng)該盡可能的包含其所實現(xiàn)功能的所有重要信息和細節(jié),以便讓維護者無需跳轉(zhuǎn)到其它多個地方去了解必要的知識。
- 低耦合:組件之間的互相依賴和了解盡可能少,以便在一個組件需要改動時其它組件不受影響。
其實這兩者就是一體兩面,做到了高內(nèi)聚基本也就做到了低耦合,相反如果內(nèi)聚度很低,勢必存在大量高耦合的組件。
我觀察發(fā)現(xiàn),很多項目都存在低內(nèi)聚、高耦合的問題。根本原因在于很多程序員,甚至是很多經(jīng)驗豐富的程序員也缺少這方面的意識——對“內(nèi)聚性”概念不甚清楚,對內(nèi)聚性被破壞的危害沒有意識,對如何避免更是無從談起。
很多人從一開始就憑直覺寫程序,有了一定經(jīng)驗以后一般能認(rèn)識到重復(fù)代碼的危害,對復(fù)用性有很強的認(rèn)識,于是就會掉進一個陷阱——盲目追求復(fù)用,結(jié)果破壞了內(nèi)聚性。
- 業(yè)界關(guān)于“復(fù)用性”的認(rèn)識存在一個誤區(qū)——認(rèn)為包括業(yè)務(wù)邏輯組件在內(nèi)的任何層面的組件都應(yīng)該追求最大限度的可復(fù)用性;
- 復(fù)用當(dāng)然是好的,但那應(yīng)該有個前提條件:不增加系統(tǒng)復(fù)雜度的情況下的復(fù)用,才是好的。
- 什么樣的復(fù)用會增加系統(tǒng)復(fù)雜性、是不好的呢?前面提到的,一個業(yè)務(wù)邏輯 API 被另一個業(yè)務(wù)邏輯 API 復(fù)用——就是不好的:
- 損害了穩(wěn)定性:因為業(yè)務(wù)邏輯本身是跟現(xiàn)實世界的業(yè)務(wù)掛鉤的,而業(yè)務(wù)會發(fā)生變化;當(dāng)你復(fù)用一個會發(fā)生變化的 API,相當(dāng)于在沙子上建高樓——地基是松動的;
- 增加了復(fù)雜性:這樣的依賴還造成代碼可讀性降低——在一個本就復(fù)雜的業(yè)務(wù)邏輯代碼中,包含了對另一個復(fù)雜業(yè)務(wù)邏輯的調(diào)用,復(fù)雜度會急劇增加,而且會不斷泛濫和傳遞;
- 內(nèi)聚性被破壞:由于業(yè)務(wù)邏輯被打散在了多個組件的方法內(nèi),變得支離破碎,無法在一個地方看清整體邏輯脈絡(luò)和實現(xiàn)步驟——內(nèi)聚性被破壞,同時也意味著,這個調(diào)用鏈條上涉及的所有組件之間存在高耦合。
4.4 藥方 2:復(fù)用的兩種正確姿勢——打造自己的 lib 和 framework
軟件架構(gòu)中有兩種東西來實現(xiàn)復(fù)用——lib 和 framework,
- lib 庫是供你(應(yīng)用程序)調(diào)用的,它幫你實現(xiàn)特定的能力(比如日志、數(shù)據(jù)庫驅(qū)動、json 序列化、日期計算、http 請求)。
- framework 框架是供你擴展的,它本身就是半個應(yīng)用程序,定義好了組件劃分和交互機制,你需要按照其規(guī)則擴展出特定的實現(xiàn)并綁定集成到其中,來完成一個應(yīng)用程序。
- lib 就是組合方式的復(fù)用,framework 則是繼承式的復(fù)用,繼承的 Java 關(guān)鍵字是 extends,所以本質(zhì)上是擴展。
- 過去有個說法:“組合優(yōu)于繼承,能用組合解決的問題盡量不要繼承”。我不同意這個說法,這容易誤導(dǎo)初學(xué)者以為組合優(yōu)于繼承,其實繼承才是面向?qū)ο笞顝姶蟮牡胤?,?dāng)然任何東西都不能亂用。
- 典型的繼承亂用就是為了獲得父類的某個 API 而去繼承,繼承一定是為了擴展,而不是為了直接獲得一個能力,獲得能力應(yīng)該調(diào)用 lib,父類不應(yīng)該去實現(xiàn)具體功能,那是 lib 該做的事。
- 也不應(yīng)該為了使用 lib 而去繼承 lib 中的 Class。lib 就是用來被組合被調(diào)用的,framework 就是用來被繼承、擴展的。
- 再展開一下:lib 既可以是第三方的(log4j、httpclient、fastjson),也可是你自己工程的(比如你的持久層 Dao、你的 utils);
- framework 同理,既可以是第三方的(springmvc、jpa、springsecurity),也可以是你項目內(nèi)封裝的面向具體業(yè)務(wù)領(lǐng)域的(比如 report、excel 導(dǎo)出、paging 或任何可復(fù)用的算法、流程)。
- 從這個意義上說,一個項目中的代碼其實只有 3 種:自定義的 lib class、自定義的 framework 相關(guān) class、擴展第三方或自定義 framework 的組件 class。
- 再擴展一下:相對于過去,現(xiàn)在我們已經(jīng)有了足夠多的第三方 lib 和 framework 來復(fù)用,來幫助項目節(jié)省大量代碼,開發(fā)工作似乎變成了索然無味、沒技術(shù)含量的 CRUD。但是對于業(yè)務(wù)非常復(fù)雜的項目,則需要有經(jīng)驗、有抽象思維、懂設(shè)計模式的人,去設(shè)計面向業(yè)務(wù)的 framework 和面向業(yè)務(wù)的 lib,只有這樣才能交付可維護、可擴展、可復(fù)用的軟件架構(gòu)——高質(zhì)量架構(gòu),幫助項目或產(chǎn)品取得成功。
4.5 癥結(jié) 3:抽象不夠、邏輯糾纏——High Level 業(yè)務(wù)邏輯和 Low Level 實現(xiàn)邏輯糾纏
當(dāng)我們說“代碼中包含的業(yè)務(wù)邏輯”的時候,我們到底在說什么?業(yè)界并沒有一個標(biāo)準(zhǔn),大家經(jīng)常講的 CRUD 增刪改查其實屬于更底層的數(shù)據(jù)訪問邏輯。
我的觀點是:所謂代碼中的業(yè)務(wù)邏輯,是指這段代碼所表現(xiàn)出的所有輸入輸出規(guī)則、算法和行為,通??梢苑譃橐韵?5 類:
- 輸入合法性校驗;
- 業(yè)務(wù)規(guī)則校驗:典型的如檢查交易記錄狀態(tài)、金額、時限、權(quán)限等,通常包含數(shù)據(jù)庫或外部接口的查詢作為參考;
- 數(shù)據(jù)持久化行為:數(shù)據(jù)庫、緩存、文件、日志等任何形式的數(shù)據(jù)寫入行為;
- 外部接口調(diào)用行為;
- 輸出/返回值準(zhǔn)備。
當(dāng)然具體到某一個組件實例,可能不會包括上述全部 5 類業(yè)務(wù)邏輯,但是也可能每一類業(yè)務(wù)邏輯存在多個。
單這樣看你可能覺得并不是特別復(fù)雜,但是現(xiàn)實中上述 5 類業(yè)務(wù)邏輯中的每一個通常還包含著一到多個底層實現(xiàn)邏輯,如 CRUD 數(shù)據(jù)訪問邏輯或第三方 API 的調(diào)用。
例如輸入合法性校驗,通常需要查詢對應(yīng)記錄是否存在,外部接口調(diào)用前通常需要查詢相關(guān)記錄以獲得調(diào)用接口需要的參數(shù),調(diào)用接口后還需要根據(jù)結(jié)果更新相關(guān)記錄狀態(tài)。
顯然這里存在兩個 Level 的邏輯——High Level 的與業(yè)務(wù)需求對應(yīng)且關(guān)聯(lián)緊密的邏輯、Low Level 的實現(xiàn)邏輯。
如果對兩個 Level 的邏輯不加以區(qū)分、混為一談,代碼質(zhì)量立刻就會遭到嚴(yán)重損害:
- 可讀性變差:兩個維度的復(fù)雜性——業(yè)務(wù)復(fù)雜性和底層實現(xiàn)的技術(shù)復(fù)雜性——被摻雜在了一起,復(fù)雜度 1+1>2 劇增,給其他人閱讀代碼增加很大負擔(dān);
- 可維護性差:可維護性通常指排查和解決問題所需花費的代價高低,當(dāng)兩個 level 的邏輯糾纏在一起,會使排查問題變的更困難,修復(fù)問題時也更容易出錯;
- 可擴展性無從談起:擴展性通常指為系統(tǒng)增加一個特性所需花費的代價高低,代價越高擴展性越差;與排查修復(fù)問題類似,邏輯糾纏顯然也會使添加新特性變得困難、一不小心就破壞了已有功能。
下面這段代碼就是一個典型案例——High Level 的邏輯流程(參數(shù)獲取、反序列化、參數(shù)校驗、緩存寫入、數(shù)據(jù)庫持久化、更新相關(guān)交易記錄)完全淹沒在了 Low Level 的實現(xiàn)邏輯(字符串比較、Json 反序列化、redis 操作、dao 操作以及前后各種瑣碎的參數(shù)準(zhǔn)備和返回值處理)。下一節(jié)我會針對這段問題代碼給出重構(gòu)方案。
- @Override
- public void updateFromMQ(String compress) {
- try {
- JSONObject object = JSON.parseObject(compress);
- if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))){
- throw new AppException("MQ返回參數(shù)異常");
- }
- logger.info(object.getString("mobile")+"<<<<<<<<<獲取來自MQ的授權(quán)數(shù)據(jù)>>>>>>>>>"+object.getString("type"));
- Map map = new HashMap();
- map.put("type",CrawlingTaskType.get(object.getInteger("type")));
- map.put("mobile", object.getString("mobile"));
- List<CrawlingTask> list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map);
- redisClientTemplate.set(object.getString("mobile") + "_" + object.getString("type"),CompressUtil.compress( object.getString("data")));
- redisClientTemplate.expire(object.getString("mobile") + "_" + object.getString("type"), 2*24*60*60);
- //保存成功 存入redis 保存48小時
- CrawlingTask crawlingTask = null;
- // providType:(0:新顏,1XX支付寶,2:ZZ淘寶,3:TT淘寶)
- if (CollectionUtils.isNotEmpty(list)){
- crawlingTask = list.get(0);
- crawlingTask.setJsonStr(object.getString("data"));
- }else{
- //新增
- crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), object.getString("data"),
- object.getString("mobile"), CrawlingTaskType.get(object.getInteger("type")));
- crawlingTask.setNeedUpdate(true);
- }
- baseDAO.saveOrUpdate(crawlingTask);
- //保存芝麻分到xyz
- if ("3".equals(object.getString("type"))){
- String data = object.getString("data");
- Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
- Map param = new HashMap();
- param.put("phoneNumber", object.getString("mobile"));
- List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
- if (list1 !=null){
- for (Dperson dperson:list1){
- dperson.setZmScore(zmf);
- personBaseDaoI.saveOrUpdate(dperson);
- AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);//查詢多租戶表 身份認(rèn)證、淘寶認(rèn)證 為0 置為1
- }
- }
- }
- } catch (Exception e) {
- logger.error("更新my MQ授權(quán)信息失敗", e);
- throw new AppException(e.getMessage(),e);
- }
- }
4.6 藥方 3:控制邏輯分離——業(yè)務(wù)模板 Pattern of NestedBusinessTemplate
解決“邏輯糾纏”最關(guān)鍵是要找到一種隔離機制,把兩個 Level 的邏輯分開——控制邏輯分離,分離的好處很多:
- 根據(jù)經(jīng)驗,當(dāng)我們著手維護一段代碼時,一定是想先弄清楚它的整體流程、算法和行為,而不是一上來就去研究它的細枝末節(jié);
- 控制邏輯分離后,只需要去看 High Level 部分就能了解到上述內(nèi)容,閱讀代碼的負擔(dān)大幅度降低,代碼可讀性顯著增強;
- 讀懂代碼是后續(xù)一切維護、重構(gòu)工作的前提,而且一份代碼被讀的次數(shù)遠遠高于被修改的次數(shù)(高一個數(shù)量級),因此代碼對人的可讀性再怎么強調(diào)都不為過,可讀性增強可以大幅度提高系統(tǒng)可維護性,也是重構(gòu)的最主要目標(biāo)。
- 同時,根據(jù)我的經(jīng)驗,High Level 業(yè)務(wù)邏輯的變更往往比 Low Level 實現(xiàn)邏輯變更要來的頻繁,畢竟前者跟業(yè)務(wù)直接對應(yīng)。當(dāng)然不同類型項目情況不一樣,另外它們發(fā)生變更的時間點往往也不同;
- 在這樣的背景下,控制邏輯分離的好處就更明顯了:每次維護、擴充系統(tǒng)功能只需改動一個 Levle 的代碼,另一個 Level 不受影響或影響很小,這會大幅降低修改成本和風(fēng)險。
我在總結(jié)過去多個項目中的教訓(xùn)和經(jīng)驗后,總結(jié)出了一項最佳實踐或者說是設(shè)計模式——業(yè)務(wù)模板 Pattern of NestedBusinessTemplat,可以非常簡單、有效的分離兩類邏輯,先看代碼:
- public class XyzService {
- abstract class AbsUpdateFromMQ {
- public final void doProcess(String jsonStr) {
- try {
- JSONObject json = doParseAndValidate(jsonStr);
- cache2Redis(json);
- saveJsonStr2CrawingTask(json);
- updateZmScore4Dperson(json);
- } catch (Exception e) {
- logger.error("更新my MQ授權(quán)信息失敗", e);
- throw new AppException(e.getMessage(), e);
- }
- }
- protected abstract void updateZmScore4Dperson(JSONObject json);
- protected abstract void saveJsonStr2CrawingTask(JSONObject json);
- protected abstract void cache2Redis(JSONObject json);
- protected abstract JSONObject doParseAndValidate(String json) throws AppException;
- }
- @SuppressWarnings({ "unchecked", "rawtypes" })
- public void processAuthResultDataCallback(String compress) {
- new AbsUpdateFromMQ() {
- @Override
- protected void updateZmScore4Dperson(JSONObject json) {
- //保存芝麻分到xyz
- if ("3".equals(json.getString("type"))){
- String data = json.getString("data");
- Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
- Map param = new HashMap();
- param.put("phoneNumber", json.getString("mobile"));
- List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
- if (list1 !=null){
- for (Dperson dperson:list1){
- dperson.setZmScore(zmf);
- personBaseDaoI.saveOrUpdate(dperson);
- AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);
- }
- }
- }
- }
- @Override
- protected void saveJsonStr2CrawingTask(JSONObject json) {
- Map map = new HashMap();
- map.put("type",CrawlingTaskType.get(json.getInteger("type")));
- map.put("mobile", json.getString("mobile"));
- List<CrawlingTask> list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map);
- CrawlingTask crawlingTask = null;
- // providType:(0:xx,1yy支付寶,2:zz淘寶,3:tt淘寶)
- if (CollectionUtils.isNotEmpty(list)){
- crawlingTask = list.get(0);
- crawlingTask.setJsonStr(json.getString("data"));
- }else{
- //新增
- crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), json.getString("data"),
- json.getString("mobile"), CrawlingTaskType.get(json.getInteger("type")));
- crawlingTask.setNeedUpdate(true);
- }
- baseDAO.saveOrUpdate(crawlingTask);
- }
- @Override
- protected void cache2Redis(JSONObject json) {
- redisClientTemplate.set(json.getString("mobile") + "_" + json.getString("type"),CompressUtil.compress( json.getString("data")));
- redisClientTemplate.expire(json.getString("mobile") + "_" + json.getString("type"), 2*24*60*60);
- }
- @Override
- protected JSONObject doParseAndValidate(String json) throws AppException {
- JSONObject object = JSON.parseObject(json);
- if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))){
- throw new AppException("MQ返回參數(shù)異常");
- }
- logger.info(object.getString("mobile")+"<<<<<<<<<獲取來自MQ的授權(quán)數(shù)據(jù)>>>>>>>>>"+object.getString("type"));
- return object;
- }
- }.doProcess(compress);
- }
如果你熟悉經(jīng)典的 GOF23 種設(shè)計模式,很容易發(fā)現(xiàn)上面的代碼示例其實就是 Template Method 設(shè)計模式的運用,沒什么新鮮的。
沒錯,我這個方案沒有提出和創(chuàng)造任何新東西,我只是在實踐中偶然發(fā)現(xiàn) Template Method 設(shè)計模式真的非常適合解決廣泛存在的邏輯糾纏問題,而且也發(fā)現(xiàn)很少有程序員能主動運用這個設(shè)計模式;一部分原因可能是意識到“邏輯糾纏”問題的人本就不多,同時熟悉這個設(shè)計模式并能自如運用的人也不算多,兩者的交集自然就是少得可憐;不管是什么原因,結(jié)果就是這個問題廣泛存在成了通病。
我看到一部分對代碼質(zhì)量有追求的程序員 他們的解決辦法是通過"結(jié)構(gòu)化編程"和“模塊化編程”:
- 把 Low Level 邏輯提取成 private function,被 High Level 代碼所在的 function 直接調(diào)用;
- 問題 1 硬連接不靈活:首先,這樣雖然起到了一定的隔離效果,但是兩個 level 之間是靜態(tài)的硬關(guān)聯(lián),Low Level 無法被簡單的替換,替換時還是需要修改和影響到 High Level 部分;
- 問題 2 組件內(nèi)可見性造成混亂:提取出來的 private function 在當(dāng)前組件內(nèi)是全局可見的——對其它無關(guān)的 High Level function 也是可見的,各個模塊之間仍然存在邏輯糾纏。這在很多項目中的熱點代碼中很常見,問題也很突出:試想一個包含幾十個 API 的組件,每個 API 的 function 存在一兩個關(guān)聯(lián)的 private function,那這個組件內(nèi)部的混亂程度、維護難度是難以承受的。
- 把 Low Level 邏輯抽取到新的組件中,供 High Level 代碼所在的組件依賴和調(diào)用;更有經(jīng)驗的程序員可能會增加一層接口并且借助 Spring 依賴注入;
- 問題 1 API 泛濫:提取出新的組件似乎避免了“結(jié)構(gòu)化編程”的局限性,但是帶來了新的問題——API 泛濫:因為組件之間調(diào)用只能走 public 方法,而這個 API 其實沒有太多復(fù)用機會根本沒必要做成 public 這種最高可見性。
- 問題 2 同層組件依賴失控:組件和 API 泛濫后必然導(dǎo)致組件之間互相依賴成為常態(tài),慢慢變得失控以后最終變成所有組件都依賴其它大部分組件,甚至出現(xiàn)循環(huán)依賴;比如那個擁有 130 個 import 和 40 個 Spring 依賴組件的 ContractService。
下面介紹一下 Template Method 設(shè)計模式的運用,簡單歸納就是:
- High Level邏輯封裝在抽象父類AbsUpdateFromMQ的一個final function中,形成一個業(yè)務(wù)邏輯的模板;
- final function保證了其中邏輯不會被子類有意或無意的篡改破壞,因此其中封裝的一定是業(yè)務(wù)邏輯中那些相對固定不變的東西。至于那些可變的部分以及暫時不確定的部分,以abstract protected function形式預(yù)留擴展點;
- 子類(一個匿名內(nèi)部類)像“做填空題”一樣,填充模板實現(xiàn)Low Level邏輯——實現(xiàn)那些protected function擴展點;由于擴展點在父類中是abstract的,因此編譯器會提醒子類的程序員該擴展什么。
那么它是如何避免上面兩個方案的 4 個局限性的:
- Low Level 需要修改或替換時,只需從父類擴展出一個新的子類,父類全然不知無需任何改動;
- 無論是父類還是子類,其中的 function 對外層的 XyzService 組件都是不可見的,即便是父類中的 public function 也不可見,因為只有持有類的實例對象才能訪問到其中的 function;
- 無論是父類還是子類,它們都是作為 XyzService 的內(nèi)部類存在的,不會增加新的 java 類文件更不會增加大量無意義的 API(API 只有在被項目內(nèi)復(fù)用或發(fā)布出去供外部使用才有意義,只有唯一的調(diào)用者的 API 是沒有必要的);
- 組件依賴失控的問題當(dāng)然也就不存在了。
SpringFramework 等框架型的開源項目中,其實早已大量使用 Template Method 設(shè)計模式,這本該給我們這些應(yīng)用開發(fā)程序員帶來啟發(fā)和示范,但是很可惜業(yè)界沒有注意到和充分發(fā)揮它的價值。
NestedBusinessTemplat 模式就是對其充分和積極的應(yīng)用,前面一節(jié)提到過的復(fù)用的兩種正確姿勢——打造自己的 lib 和 framework,其實 NestedBusinessTemplat 就是項目自身的 framework。
4.7 癥結(jié) 4:無處不在的 if else 牛皮癬
無論你的編程啟蒙語言是什么,最早學(xué)會的邏輯控制語句一定是 if else,但是不幸的是它在你開始真正的編程工作以后,會變成一個損害項目質(zhì)量的壞習(xí)慣。
幾乎所有的項目都存在 if else 泛濫的問題,但是卻沒有引起足夠重視警惕,甚至被很多程序員認(rèn)為是正常現(xiàn)象。
首先我來解釋一下為什么 if else 這個看上去人畜無害的東西是有害的、是需要嚴(yán)格管控的:
- if else if ...else 以及類似的 switch 控制語句,本質(zhì)上是一種 hard coding 硬編碼行為,如果你同意“magic number 魔法數(shù)字”是一種錯誤的編程習(xí)慣,那么同理,if else 也是錯誤的 hard coding 編程風(fēng)格;
- hard coding 的問題在于當(dāng)需求發(fā)生改變時,需要到處去修改,很容易遺漏和出錯;
- 以一段代碼為例來具體分析:
- if ("3".equals(object.getString("type"))){
- String data = object.getString("data");
- Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
- Map param = new HashMap();
- param.put("phoneNumber", object.getString("mobile"));
- List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
- if (list1 !=null){
- for (Dperson dperson:list1){
- dperson.setZmScore(zmf);
- personBaseDaoI.saveOrUpdate(dperson);
- AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);
- }
- }
- }
- if ("3".equals(object.getString("type")))
- 顯然這里的"3"是一個 magic number,沒人知道 3 是什么含義,只能推測;
- 但是僅僅將“3”重構(gòu)成常量 ABC_XYZ 并不會改善多少,因為 if (ABC_XYZ.equals(object.getString("type"))) 仍然是面向過程的編程風(fēng)格,無法擴展;
- 到處被引用的常量 ABC_XYZ 并沒有比到處被 hard coding 的 magic number 好多少,只不過有了含義而已;
- 把常量升級成 Enum 枚舉類型呢,也沒有好多少,當(dāng)需要判斷的類型增加了或判斷的規(guī)則改變了,還是需要到處修改——Shotgun Surgery(霰彈式修改)
- 并非所有的 if else 都有害,比如上面示例中的 if (list1 !=null) { 就是無害的,沒有必要去消除,也沒有消除它的可行性。判斷是否有害的依據(jù):
- 如果 if 判斷的變量狀態(tài)只有兩種可能性(比如 boolean、比如 null 判斷)時,是無傷大雅的;
- 反之,如果 if 判斷的變量存在多種狀態(tài),而且將來可能會增加新的狀態(tài),那么這就是個問題;
- switch 判斷語句無疑是有害的,因為使用 switch 的地方往往存在很多種狀態(tài)。
4.8 藥方 4:充血枚舉類型——Rich Enum Type
正如前面分析呈現(xiàn)的那樣,對于代碼中廣泛存在的狀態(tài)、類型 if 條件判斷,僅僅把被比較的值重構(gòu)成常量或 enum 枚舉類型并沒有太大改善——使用者仍然直接依賴具體的枚舉值或常量,而不是依賴一個抽象。
于是解決方案就自然浮出水面了:在 enum 枚舉類型基礎(chǔ)上進一步抽象封裝,得到一個所謂的“充血”的枚舉類型,代碼說話:
- 實現(xiàn)多種系統(tǒng)通知機制,傳統(tǒng)做法:
- enum NOTIFY_TYPE { email,sms,wechat; } //先定義一個enum——一個只定義了值不包含任何行為的“貧血”的枚舉類型
- if(type==NOTIFY_TYPE.email){ //if判斷類型 調(diào)用不同通知機制的實現(xiàn)
- 。。。
- }else if (type=NOTIFY_TYPE.sms){
- 。。。
- }else{
- 。。。
- }
- 實現(xiàn)多種系統(tǒng)通知方式,充血枚舉類型——Rich Enum Type 模式:
- enum NOTIFY_TYPE { //1、定義一個包含通知實現(xiàn)機制的“充血”的枚舉類型
- email("郵件",NotifyMechanismInterface.byEmail()),
- sms("短信",NotifyMechanismInterface.bySms()),
- wechat("微信",NotifyMechanismInterface.byWechat());
- String memo;
- NotifyMechanismInterface notifyMechanism;
- private NOTIFY_TYPE(String memo,NotifyMechanismInterface notifyMechanism){//2、私有構(gòu)造函數(shù),用于初始化枚舉值
- this.memo=memo;
- this.notifyMechanism=notifyMechanism;
- }
- //getters ...
- }
- public interface NotifyMechanismInterface{ //3、定義通知機制的接口或抽象父類
- public boolean doNotify(String msg);
- public static NotifyMechanismInterface byEmail(){//3.1 返回一個定義了郵件通知機制的策的實現(xiàn)——一個匿名內(nèi)部類實例
- return new NotifyMechanismInterface(){
- public boolean doNotify(String msg){
- .......
- }
- };
- }
- public static NotifyMechanismInterface bySms(){//3.2 定義短信通知機制的實現(xiàn)策略
- return new NotifyMechanismInterface(){
- public boolean doNotify(String msg){
- .......
- }
- };
- }
- public static NotifyMechanismInterface byWechat(){//3.3 定義微信通知機制的實現(xiàn)策略
- return new NotifyMechanismInterface(){
- public boolean doNotify(String msg){
- .......
- }
- };
- }
- }
- //4、使用場景
- NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
充血枚舉類型——Rich Enum Type 模式的優(yōu)勢:
- 不難發(fā)現(xiàn),這其實就是 enum 枚舉類型和 Strategy Pattern 策略模式的巧妙結(jié)合運用;
- 當(dāng)需要增加新的通知方式時,只需在枚舉類 NOTIFY_TYPE 增加一個值,同時在策略接口 NotifyMechanismInterface 中增加一個 by 方法返回對應(yīng)的策略實現(xiàn);
- 當(dāng)需要修改某個通知機制的實現(xiàn)細節(jié),只需修改 NotifyMechanismInterface 中對應(yīng)的策略實現(xiàn);
- 無論新增還是修改通知機制,調(diào)用方完全不受影響,仍然是 NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
與傳統(tǒng) Strategy Pattern 策略模式的比較優(yōu)勢:常見的策略模式也能消滅 if else 判斷,但是實現(xiàn)起來比較麻煩,需要開發(fā)更多的 class 和代碼量:
- 每個策略實現(xiàn)需單獨定義成一個 class;
- 還需要一個 Context 類來做初始化——用 Map 把類型與對應(yīng)的策略實現(xiàn)做映射;
- 使用時從 Context 獲取具體的策略;
Rich Enum Type 的進一步的充血:
- 上面的例子中的枚舉類型包含了行為,因此已經(jīng)算作充血模型了,但是還可以為其進一步充血;
- 例如有些場景下,只是要對枚舉值做個簡單的計算獲得某種 flag 標(biāo)記,那就沒必要把計算邏輯抽象成 NotifyMechanismInterface 那樣的接口,殺雞用了牛刀;
- 這時就可以在枚舉類型中增加 static function 封裝簡單的計算邏輯;
策略實現(xiàn)的進一步抽象:
- 當(dāng)各個策略實現(xiàn)(byEmail bySms byWechat)存在共性部分、重復(fù)邏輯時,可以將其抽取成一個抽象父類;
- 然后就像前一章節(jié)——業(yè)務(wù)模板 Pattern of NestedBusinessTemplate 那樣,在各個子類之間實現(xiàn)優(yōu)雅的邏輯分離和復(fù)用。
5. 重構(gòu)前的火力偵察:為你的項目編制一套代碼庫目錄/索引——CODEX
以上就是我總結(jié)出的最常見也最影響代碼質(zhì)量的 4 個問題及其解決方案:
- 職責(zé)單一、小顆粒度、高內(nèi)聚、低耦合的業(yè)務(wù)邏輯層組件——倒金字塔結(jié)構(gòu);
- 打造項目自身的 lib 層和 framework——正確的復(fù)用姿勢;
- 業(yè)務(wù)模板 Pattern of NestedBusinessTemplate——控制邏輯分離;
- 充血的枚舉類型 Rich Enum Type——消滅硬編碼風(fēng)格的 if else 條件判斷;
接下來就是如何動手去針對這 4 個方面進行重構(gòu)了,但是事情還沒有那么簡單。
上面所有的內(nèi)容雖然來自實踐經(jīng)驗,但是要應(yīng)用到你的具體項目,還需要一個步驟——火力偵察——弄清楚你要重構(gòu)的那個模塊的邏輯脈絡(luò)、算法以致實現(xiàn)細節(jié),否則貿(mào)然動手,很容易遺漏關(guān)鍵細節(jié)造成風(fēng)險,重構(gòu)的效率更難以保證,陷入進退兩難的尷尬境地。
我 2019 年一整年經(jīng)歷了 3 個代碼十分混亂的項目,最大的收獲就是摸索出了一個梳理爛代碼的最佳實踐——CODEX:
所謂結(jié)構(gòu)化注釋,就是在注釋內(nèi)容中通過規(guī)范命名的編號前綴、分隔符等來體現(xiàn)出其所對應(yīng)的項目、模塊、流程步驟等信息,類似文本編輯中的標(biāo)題 1、2、3; - 然后設(shè)置 IDE 工具識別這種特殊的注釋,以便結(jié)構(gòu)化的顯示。Eclipse 的 Tasks 顯示效果類似下圖;
- 這個結(jié)構(gòu)化視圖,本質(zhì)上相對于是代碼庫的索引、目錄,不同于 javadoc 文檔,CODEX 具有更清晰的邏輯層次和更強的代碼查找便利性,在 Eclipse Tasks 中點擊就能跳轉(zhuǎn)到對應(yīng)的代碼行;
- 這些結(jié)構(gòu)化注釋隨著代碼一起提交后就實現(xiàn)了團隊共享;
- 這樣的一份精確無誤、共享的、活的源代碼索引,無疑會對整個團隊的開發(fā)維護工作產(chǎn)生巨大助力;
- 進一步的,如果在 CODEX 中添加 Markdown 關(guān)鍵字,甚至可以將導(dǎo)出的 CODEX 簡單加工后,變成一張業(yè)務(wù)邏輯的 Sequence 序列圖,如下所示。
6. 總結(jié)陳詞——不要辜負這個程序員最好的時代
毫無疑問這是程序員最好的時代,互聯(lián)網(wǎng)浪潮已經(jīng)席卷了世界每個角落,各行各業(yè)正在越來越多的依賴 IT。過去只有軟件公司、互聯(lián)網(wǎng)公司和銀行業(yè)會雇傭程序員,隨著云計算的普及、產(chǎn)業(yè)互聯(lián)網(wǎng)和互聯(lián)網(wǎng)+興起,已經(jīng)有越來越多的傳統(tǒng)企業(yè)開始雇傭程序員搭建 IT 系統(tǒng)來支撐業(yè)務(wù)運營。
資本的推動 IT 需求的旺盛,使得程序員成了稀缺人才,各大招聘平臺上,程序員的崗位數(shù)量和薪資水平長期名列前茅。
但是我們這個群體的整體表現(xiàn)怎么樣呢,捫心自問,我覺得很難令人滿意,我所經(jīng)歷過的以及近距離觀察到的項目,鮮有能夠稱得上成功的。這里的成功不是商業(yè)上的成功,僅限于作為一個軟件項目和工程是否能夠以可接受的成本和質(zhì)量長期穩(wěn)定的交付。
商業(yè)的短期成功與否,很多時候與項目工程的成功與否沒有必然聯(lián)系,一個商業(yè)上很成功的項目可能在工程上做的并不好,只是通過巨量的資金資源投入換來的暫時成功而已。
歸根結(jié)底,我們程序員群體需要為自己的聲譽負責(zé),長期來看也終究會為自己的聲譽獲益或受損。
我認(rèn)為程序員最大的聲譽、最重要的職業(yè)素養(yǎng),就是通過寫出高質(zhì)量的代碼做好一個個項目、產(chǎn)品,來幫助團隊、幫助公司、幫助組織創(chuàng)造價值、增加成功的機會。
希望本文分享的經(jīng)驗和方法能夠?qū)Υ擞兴鶐椭?
本文是我的一位技術(shù)總監(jiān)好友:權(quán)哥花了半個月時間寫出來的良心文章,強烈推薦給大家,文章很長很硬很有價值,大家可以收藏多看幾遍。希望大家看完之后轉(zhuǎn)發(fā)、點在看,好文章要讓更多的人看到。
工作15年以上的老程序猿,百人技術(shù)團隊管理者。
分享工作,生活的干貨,每篇文章都有趣。
歡迎關(guān)注作者公眾號,看到更多文章。