現(xiàn)在許多國內(nèi)互聯(lián)網(wǎng)公司的項(xiàng)目都持續(xù)了五年左右,美國老牌公司如 IBM 的項(xiàng)目甚至持續(xù)維護(hù)了十五年埋同,然而這些項(xiàng)目卻有著截然不同的維護(hù)成本涯保,有的公司項(xiàng)目運(yùn)作幾年后維護(hù)成本依然與初創(chuàng)期不大即硼,可以保持較為高效的迭代速度,但有的項(xiàng)目甚至改幾個(gè)文案都會(huì)導(dǎo)致線上事故空郊,研發(fā)效率變得越來越慢。
根據(jù)筆者的經(jīng)驗(yàn)切揭,嘗試總結(jié)一些持續(xù)維護(hù)項(xiàng)目變得難以維護(hù)的原因狞甚,以及如何設(shè)計(jì)才能保持良好的可維護(hù)性。
精讀
心態(tài)
如果不真心對(duì)待自己的項(xiàng)目廓旬,其實(shí)是很難做到良好可維護(hù)性的哼审,所以第一點(diǎn)就是需要一個(gè)良好的心態(tài)。
作為項(xiàng)目管理者,一個(gè)項(xiàng)目一旦交給一位同學(xué)開發(fā)涩盾,那么就要完全信任這位同學(xué)的能力十气,因?yàn)閷?shí)際上你已經(jīng)不可能實(shí)質(zhì)性的影響到開發(fā)細(xì)節(jié)了。有人可能覺得好的流程或者事后 CodeReview 能發(fā)現(xiàn)一些問題春霍,但這永遠(yuǎn)是杯水車薪砸西,比如下面這個(gè)例子:
小張接到任務(wù)研發(fā)透視表,要求這個(gè)透視表具有良好的開發(fā)體驗(yàn)并做好單測址儒。
那怎么樣做單測才算是有效的芹枷,如何同時(shí)保證開發(fā)體驗(yàn)?zāi)兀坎煌藭?huì)有不同的想法莲趣,也會(huì)有不同的結(jié)果鸳慈。
有主人翁心態(tài)的小張
對(duì)于一個(gè)有一定經(jīng)驗(yàn),又對(duì)項(xiàng)目真正上心的小張來說喧伞,開發(fā)過程可能是這樣的走芋。
首先上來先寫主要功能,比如考慮數(shù)據(jù)模型絮识、繪圖技術(shù)方案后绿聘,決定采用圖形語法方式定義數(shù)據(jù)結(jié)構(gòu),在做了一系列高性能前置考慮后次舌,快速做出來了一個(gè)原型熄攘,包含表格的渲染、操作彼念、翻頁挪圾、凍結(jié)等等功能。
但隨著需求的深入逐沙,小張發(fā)現(xiàn)做到下鉆哲思、排序時(shí),不知道為何影響到了列凍結(jié)的功能吩案,而代碼架構(gòu)其實(shí)沒什么大問題棚赔,抽象的也很好,主要就是一些細(xì)節(jié)的代碼調(diào)用漏掉了徘郭,只要補(bǔ)上就立馬打通了任督二脈靠益,整套功能再度行云流水了起來。但不知道下次做樹狀展示結(jié)構(gòu)時(shí)會(huì)不會(huì)又把之前的功能影響了残揉,這始終是個(gè)隱患胧后,于是小張開始思考先把單測加上再繼續(xù)開發(fā)功能。
由于出問題的場景有很小部分是大量操作后偶然引發(fā)的抱环,普通的函數(shù)式單測也無法保證覆蓋的全面壳快,因此小張決定做一個(gè)單測錄制功能纸巷,他首先把對(duì)表格的所有操作 Action 化,讓一套 json 可以描述所有用戶操作眶痰,然后又在本地開發(fā)界面做了一個(gè)單測錄制功能瘤旨,即在頁面上對(duì)表格功能拖拖拽拽時(shí),就會(huì)實(shí)時(shí)生成這套用戶操作 json凛驮,再把當(dāng)時(shí)頁面結(jié)構(gòu)與內(nèi)部狀態(tài)記錄下來作為對(duì)比依據(jù)裆站,單測就還原這套 json 并與基準(zhǔn)狀態(tài)做對(duì)比就行了。
小張很快錄制了很多原子操作的單測黔夭,比如表格的各種空數(shù)據(jù)狀態(tài)宏胯、單行單列渲染、列凍結(jié)行凍結(jié)本姥;然后又把一些功能混合的場景結(jié)合起來肩袍,比如列凍結(jié)時(shí)排序,翻頁后進(jìn)行下鉆婚惫;最后又把一些隨機(jī)復(fù)雜的功能組合在一起氛赐,形成一些日常容易出問題的特殊單測 case,比如表格單頁后突然清空數(shù)據(jù)先舷,再強(qiáng)制凍結(jié)第二列艰管,再灌入3列數(shù)據(jù)并對(duì)第2行做排序,再取消列凍結(jié)并翻到第4頁蒋川。以后每當(dāng)遇到一個(gè)邊界 case 時(shí)牲芋,小張都會(huì)把這個(gè)問題 case 記錄到單測,驗(yàn)證確實(shí)運(yùn)行失敗捺球,再進(jìn)行修復(fù)缸浦,直到包含這個(gè)單測在內(nèi)的所有單測都驗(yàn)證通過后,才算開發(fā)完成氮兵。
打工人小張
對(duì)于為了混口飯吃的小張來說裂逐,開發(fā)過程可能是這樣的。
首先上來寫主要功能泣栈,把各種表格功能做完后卜高,也遇到了一樣的邊界 case 難題,此時(shí)小張本來想 case by case 修復(fù)南片,但又想到 leader 要求他寫單測篙悯,覺得倒也不壞,就創(chuàng)建了單測目錄铃绒。
怎么寫單測呢?首先小張把遇到的問題修了螺捐,畢竟誰也不希望自己手里的 bug 太多颠悬,但至于錄到單測就太麻煩了矮燎,反正大家也不知道這個(gè) case,修掉了就再也不會(huì)出來了吧赔癌,那就只把 leader 要求的幾個(gè)基本功能單測加上去诞外,看下覆蓋率也達(dá)到硬性指標(biāo)就行了。
大團(tuán)隊(duì)代碼總是容易走向混亂
假設(shè)你是 leader灾票,你不知道自己團(tuán)隊(duì)的小張到底是主人翁小張還是打工人小張峡谊,企圖通過 code review 來統(tǒng)一提升團(tuán)隊(duì)的代碼質(zhì)量,實(shí)際上可行嗎刊苍?
如果不幸遇上了打工人小張既们,他在 code review 時(shí)展示的代碼結(jié)構(gòu)就不是能做整體單測的抽象,你只能看著單測文件硬提一些比如 “多加一些單測正什,多考慮一些情況” 的建議啥纸,實(shí)際上完全達(dá)不到主人翁小張做的效果。
這背后的原因是影響代碼質(zhì)量的因素太多婴氮,比如 Action 化斯棒,比如各種極端 case 的錄入,比如全流程的單測形式主经,這些對(duì)代碼來說都是質(zhì)變荣暮,但 code review 時(shí)看到的代碼就是不夠抽象,不夠 Action 化的罩驻,不可能把代碼推翻重寫一遍穗酥,只能在已有代碼基礎(chǔ)上提優(yōu)化建議,而到這個(gè)時(shí)候鉴腻,神仙也沒法讓打工人小張的代碼優(yōu)化為主人翁小張的迷扇,除非推翻重寫。
這就是心態(tài)的影響力爽哎,能把項(xiàng)目做好的細(xì)節(jié)很多蜓席,而且細(xì)節(jié)之間還是環(huán)環(huán)相扣的,比如不把代碼 Action 化就不方便做整體單測课锌,但如果開發(fā)者打一開始就沒想好好設(shè)計(jì)厨内,code review 時(shí)又有多少人能想到這一點(diǎn)呢?想到了此時(shí)再提可能也為時(shí)太晚渺贤,一切都已成定局雏胃。
這些年筆者看過不少久經(jīng)歷史的代碼,因?yàn)榇蠊居写罅康拈_發(fā)者維護(hù)同一個(gè)項(xiàng)目志鞍,每個(gè)人開發(fā)時(shí)的心態(tài)都各有不同瞭亮,會(huì)發(fā)現(xiàn)總能看到那些模塊是用打工人心態(tài)做出來的,而你想徹底優(yōu)化就只能徹底重寫固棚,但礙于項(xiàng)目體量太大時(shí)間上不允許统翩,只能沿著打工人思路洋洋灑灑的繼續(xù)寫下去仙蚜。
所以擁有一個(gè)良好,正面或者說積極的主人翁心態(tài)來寫代碼厂汗,一般來說都可以維護(hù)好復(fù)雜項(xiàng)目委粉。
解耦
復(fù)雜項(xiàng)目的復(fù)雜指的是什么呢?是指功能多嗎娶桦?其實(shí)不然贾节。
如果僅從功能多就判定這個(gè)項(xiàng)目復(fù)雜,那我們身處的社會(huì)才是最復(fù)雜的系統(tǒng)衷畦,但社會(huì)中的每個(gè)玩家都沒有覺得吃穿住行很難栗涂,核心原因就在于了解我們用到的場景只需要少量的知識(shí),而做出一個(gè)行動(dòng)要得到正確的結(jié)果霎匈,也不會(huì)造成太大的影響戴差。比如出門買菜,只要做個(gè)公交車到菜市場铛嘱,掃一下碼就完成了交易暖释,而不需要對(duì)背后的城市公交體系與菜市場背后的金融體系有任何深入的了解,你不需要理解公交車是哪兒來的墨吓,菜農(nóng)手里的菜是從哪兒收購的球匕。
但代碼世界就很有趣了,在代碼世界買個(gè)菜可能會(huì)導(dǎo)致世界毀滅帖烘。這就導(dǎo)致每一個(gè)項(xiàng)目開發(fā)人員亮曹,哪怕是去買個(gè)菜,也要受過總統(tǒng)級(jí)訓(xùn)練秘症,對(duì)各種國家級(jí)大事做出正確的預(yù)案照卦,為什么會(huì)這樣呢?
因?yàn)榇a世界的邏輯是不同開發(fā)者碼出來的乡摹,在實(shí)現(xiàn)世界底層邏輯時(shí)可能就埋下了耦合的種子役耕,導(dǎo)致你不知道為什么買菜會(huì)觸發(fā)那么嚴(yán)重的事情。舉個(gè)例子聪廉,改一個(gè)文案導(dǎo)致系統(tǒng)崩潰瞬痘,原因可能是某處錯(cuò)誤兜底邏輯用字面量判斷了這個(gè)文案,而你把文案改了板熊,這個(gè)判斷就失效了框全。有的程序員挺難的,在這種項(xiàng)目環(huán)境下生存干签,每一步修改都要小心翼翼津辩。
這個(gè)問題的解決辦法就是解耦,在這里我們不細(xì)說具體怎么解耦,因?yàn)槊總€(gè)場景的解耦方式都不同丹泉。我們只需要理解幾乎所有的業(yè)務(wù)邏輯都可以用解耦的方式做情萤,就行了。只要你按照這樣的大思路去設(shè)計(jì)系統(tǒng)摹恨,不論路徑是怎樣的,最終都能設(shè)計(jì)出一個(gè)漂亮的系統(tǒng)級(jí)方案娶视。
比如做一個(gè) BI 系統(tǒng)晒哄,看上去里面有各種復(fù)雜的模塊可能會(huì)產(chǎn)生相互影響,比如數(shù)據(jù)處理肪获、儀表盤搭建寝凌、大屏搭建、圖表孝赫、GIS 地圖等较木,在設(shè)計(jì)之初就要假裝其他模塊不存在,來考慮每個(gè)模塊必要的輸入是哪些青柄。
比如布局伐债,它僅僅用于對(duì)畫布進(jìn)行布局,為了保證布局系統(tǒng)是完全解耦的致开,必須讓項(xiàng)目支持在無布局的環(huán)境下運(yùn)行峰锁。為了做到這一點(diǎn),就必須讓布局真的 “只做布局”双戳,而不存儲(chǔ)當(dāng)前畫布結(jié)構(gòu)虹蒋,這樣才不會(huì)因?yàn)椴季窒到y(tǒng)被移除時(shí),影響組件的聯(lián)動(dòng)飒货,因?yàn)榻M件聯(lián)動(dòng)需要利用畫布結(jié)構(gòu) API魄衅。
圖層列表也可以和布局解耦,因?yàn)閳D層列表只關(guān)心畫布的組件樹結(jié)構(gòu)塘辅,而不關(guān)心布局是如何實(shí)現(xiàn)的晃虫,所以畫布的組件樹結(jié)構(gòu)就像生活中的金錢,大家都可以用它交易莫辨,而無需關(guān)心它流向了何方傲茄,被誰使用。
數(shù)據(jù)邏輯與畫布結(jié)構(gòu)無關(guān)沮榜,只需要關(guān)心表達(dá)式以及用戶對(duì)維度度量的配置盘榨、聚合方式以及圖表本身的特性進(jìn)行查詢 sql 拼接即可,唯一用到的通用資源是當(dāng)前組件實(shí)例信息修改后蟆融,需要更新到畫布的組件樹上草巡。
社會(huì)也是建立在這種底層認(rèn)同上,才能這么解耦的型酥,所以在復(fù)雜項(xiàng)目中一定要有一個(gè)大家都認(rèn)可的底層概念山憨,這個(gè)概念應(yīng)該盡可能通用化(想想金錢什么都能買查乒,如果只能買蔬菜就麻煩了)、貫穿整個(gè)業(yè)務(wù)邏輯(金錢是現(xiàn)代社會(huì)任何交易都必須的媒介)郁竟。
許多項(xiàng)目被詬病難改玛迄,往往是沒有遵循這條邏輯,硬生生把可以不相關(guān)的概念耦合了棚亩。比如某個(gè)篩選器條件變化時(shí)蓖议,對(duì)某個(gè)組件做特殊操作,這個(gè)場景可以控制反轉(zhuǎn)為讥蟆,這個(gè)組件在接收到某些篩選條件時(shí)勒虾,自己做特定的操作。因?yàn)閷?duì) BI 系統(tǒng)來說瘸彤,篩選器的輸出要作為圖表繪圖的輸入修然,在這個(gè)底層框架下,就不要再開辟一條篩選器關(guān)心到具體圖表的邏輯了质况。
總結(jié)
維護(hù)好一個(gè)復(fù)雜項(xiàng)目很難愕宋,這次分享了兩個(gè)實(shí)踐中有用的方案,第一個(gè)抱有主人翁心態(tài)設(shè)計(jì)代碼拯杠,要在設(shè)計(jì)之初就做好考量掏婶,不要寄希望于對(duì)沒有好好設(shè)計(jì)的系統(tǒng)做縫縫補(bǔ)補(bǔ)。第二是深入理解為什么現(xiàn)代社會(huì)的運(yùn)作巧妙之處潭陪,盡可能把代碼架構(gòu)組織一定程度映射到社會(huì)的運(yùn)作機(jī)制上雄妥,目前來看,社會(huì)最適合代碼借鑒的思路就是解耦依溯,再利用龐大的分工協(xié)作網(wǎng)絡(luò)完成單人無法完成的工作老厌。
轉(zhuǎn)自:精讀《維護(hù)好一個(gè)復(fù)雜項(xiàng)目》·