你為什么不敢重構(gòu)代碼?

來源:ES2049 / 黑石
https://juejin.cn/post/6951373058544730125
代碼重構(gòu)有兩大難點(diǎn),一個(gè)是「考古」,也就是如何快速梳理出代碼的原有邏輯,還有一點(diǎn)就是「發(fā)布」,如何讓新的代碼可以穩(wěn)定的發(fā)布到線上,而不產(chǎn)生故障。下面我們就聊聊我一個(gè)朋友的故事,看看他是怎么把代碼穩(wěn)定搞上線的。為了表達(dá)更為親切,你現(xiàn)在就是我那個(gè)朋友。
重構(gòu)代碼對(duì)很多人來說,絕對(duì)是一件臟活、累活。沒有可以大幅度提效的方法,難以沉淀有效的體系化的可復(fù)用的技術(shù)抓手,對(duì)業(yè)務(wù)來說沒有明顯的增量,精力和時(shí)間消耗巨大,沒有測(cè)試用例,也不一定能得到測(cè)試的支持,自測(cè)很難做到充分,最后開發(fā)完了很難上線,主要原因是害怕!當(dāng)然并不是我們不自信,是真的恐懼。
一、你為什么不敢發(fā)代碼?
通過代碼還原當(dāng)時(shí)完整的產(chǎn)品邏輯太難了
你重構(gòu)的代碼是誰的?鬼知道是誰的!能讓你重構(gòu)的代碼大概率不是你寫的代碼,而且是遠(yuǎn)古代碼,用的是一種過時(shí)的技術(shù)棧。當(dāng)然一般情況下,當(dāng)年的開發(fā)、測(cè)試、甚至產(chǎn)品早已不見了蹤跡,只能在注釋的代碼里看見了了數(shù)語。言語中透露著無奈,用一個(gè)程序員的良心提醒著后來人,「小心前面的臟東西」??戳诉@些話,你只能收回口中馬上要吐出的芬芳,默默離開工位,倒點(diǎn)熱水。



從此你會(huì)發(fā)現(xiàn),注釋不僅能夠幫你讀懂代碼,還能有警示作用,告訴你重構(gòu)代碼的同時(shí),記得把 bug 一并改了。你想要通過注釋來梳理出原始需求的愿望宣告失敗,接下來你只能死磕了,祈禱千萬不要漏掉業(yè)務(wù)邏輯。
沒有自測(cè)用例
別以為大公司制度完善,測(cè)試都有完整的測(cè)試用例,現(xiàn)實(shí)會(huì)狠狠的夾你腦門。頻繁的迭代,功能早已面目全非,老的用例根本不可用,更何況根本找不到老的測(cè)試用例。沒有用例怎么自測(cè)呢?全靠個(gè)人想象。
沒有測(cè)試同學(xué)跟進(jìn)
多一個(gè)人多一分力量,讓一個(gè)有經(jīng)驗(yàn)的測(cè)試參與到功能回歸中來,無疑會(huì)給你的重構(gòu)事業(yè)吃上定心丸,但真實(shí)的情況是,測(cè)試同學(xué)根本不想?yún)⑴c這種臟活累活。他自己手里的需求還測(cè)不過來,怎么會(huì)把時(shí)間奉獻(xiàn)給一個(gè)前端發(fā)起的重構(gòu)工作上呢。無增量,無抓手,純體力,他們同樣心知肚明。
沒有穩(wěn)定發(fā)布方案
在沒有上述保障的前提下,如果你還能硬著頭皮上線,就會(huì)遇到更大的難題,如何上線?直接全量替換嗎?如果線上出問題怎么辦?好在前端的回滾是非常迅速的,但是即使再迅速的回滾,從發(fā)布完成到發(fā)現(xiàn)問題回滾,在提醒用戶重新刷新頁面,這個(gè)過程也足以造成難以估量的后果,尤其是那些高頻使用,且極易產(chǎn)生臟數(shù)據(jù)的場(chǎng)景。這就是沒有一個(gè)有效的發(fā)布方案所導(dǎo)致的常見后果,這個(gè)后果還有可能導(dǎo)致你背上故障,這一年加過的班,熬過的夜,掉的頭發(fā),什么也換不來,只能催生你換個(gè)地方重新做人的念頭。
綜上因素直接導(dǎo)致開發(fā)者極度缺乏安全感,一個(gè)不敢上線自己代碼的程序員,就像半夜被自己一個(gè)月大孩子的哭聲吵醒,那時(shí)那刻你只想裝死摸魚。更何況你的工作往往不是只有重構(gòu)這一件事,寫寫新需求他不香嗎?就這樣你眼看著一個(gè)頁面重構(gòu)了兩個(gè)星期,遲遲不能收尾,你變得越來越不自信,越來越害怕了起來,不敢面對(duì)那些重構(gòu)了一半的代碼,開始恐懼老板的問題:「重構(gòu)搞的怎么樣了?」,你簡直不像個(gè)程序員。
終于到了年底,你的重構(gòu)事業(yè)還未完成,更可怕的是,這件事還被打上了「承諾型」OKR 的標(biāo),于是你痛定思痛,做了個(gè)夢(mèng)。
時(shí)間回到年初你剛剛接到重構(gòu)任務(wù)的時(shí)候。
二、尋求組織保障
你的重構(gòu)工作是把 177 個(gè) jQuery 頁面用 React 重寫一遍。你立馬想到,自己一個(gè)人一年時(shí)間,一定是做不完的,此時(shí)此刻,切記不要滿口答應(yīng),一定實(shí)事求是,甚至向著最壞的方向想,讓老板充分認(rèn)識(shí)到這項(xiàng)任務(wù)的艱巨性,不要抱有太高的期望。最重要的是保證人力的投入,必須有更多的同學(xué)一起參與進(jìn)來,有效的分工才有可能完成這項(xiàng)艱巨的任務(wù)。有人參與進(jìn)來,也只是基礎(chǔ),因?yàn)樗麄儤O有可能會(huì)像上面描述的一樣,從興致勃勃到唯唯諾諾,因此一定要確保時(shí)間的投入,必要時(shí)把老板也拉進(jìn)來跟你一起做,老板一旦參與進(jìn)來,就會(huì)更有體感,能體會(huì)到大家的不易。接下來,就應(yīng)了那就老話,「別忘了,你是一個(gè) owner!」做好基礎(chǔ)設(shè)施建設(shè),讓每個(gè)同學(xué)有趁手的工具,有安全的保障,去除他們的后顧之憂至關(guān)重要。因此,你要做下面幾件事。
三、劃分重構(gòu)頁面優(yōu)先級(jí)
你通過細(xì)致的研究發(fā)現(xiàn),這些頁面中,有 77 個(gè)頁面是用戶使用較多的頁面,也是相對(duì)比較復(fù)雜的頁面,剩下的 100 個(gè)頁面,大部分是給開發(fā)用的增刪改查頁面,用戶的使用頻率不高。于是你做了如下劃分:
優(yōu)先級(jí)劃分好優(yōu)先級(jí)以后,就要對(duì)不同優(yōu)先級(jí)的頁面使用不同的穩(wěn)定發(fā)布策略。
復(fù)雜高頻頁面:重兵壓上,細(xì)致還原原始需求,摳代碼,拉測(cè)試同學(xué)一起整理測(cè)試用例,按照測(cè)試用例自測(cè),測(cè)試同學(xué)回歸所有功能。但其實(shí)這部分頁面中,也可以分為兩種頁面: 編輯頁面:這樣的頁面是風(fēng)險(xiǎn)最高的頁面,一旦因?yàn)楹蠖私涌跊]有做完整的數(shù)據(jù)校驗(yàn),就會(huì)編輯出臟數(shù)據(jù),或者錯(cuò)誤的數(shù)據(jù)被保存,導(dǎo)致線上運(yùn)行異常,這種后果將是不堪設(shè)想的,即使非常短的時(shí)間內(nèi)回滾,也會(huì)造成難以挽回的故障,因此必須要像新需求一樣測(cè)試到位。 展示頁面:這樣的頁面不會(huì)影響運(yùn)行時(shí),不會(huì)產(chǎn)生臟數(shù)據(jù),是風(fēng)險(xiǎn)相對(duì)低一點(diǎn)點(diǎn)的頁面,本著不麻煩合作方的原則,畢竟資源有限,可以讓測(cè)試幫你出完整的用例,然后你自己自測(cè),或者多找?guī)讉€(gè)同學(xué)幫你自測(cè)。 高頻簡單頁面:自測(cè),當(dāng)然最好是能綁架幾個(gè)經(jīng)常用這個(gè)功能的開發(fā),來幫你點(diǎn)點(diǎn),但是自己測(cè)總是會(huì)有可能會(huì)有遺漏,因此就需要下面的步驟來保證了。 低頻運(yùn)維頁面:選擇性重構(gòu),因?yàn)楹芏囗撁婊旧喜粫?huì)有迭代,且使用頻率較低,基本上不需要重構(gòu),即使是有新的需求,也可以在做新需求的時(shí)候順便重構(gòu)下,以為并不能占用太多時(shí)間。
將頁面劃分完畢后,你會(huì)發(fā)現(xiàn)重構(gòu)的工作量降低了很多,因?yàn)楸局笩o需求,勿變更」的原則,很多頁面都可以不需要重構(gòu)。且上述重構(gòu)完的頁面都必須做灰度發(fā)布。
四、單測(cè)
前端不太喜歡寫單測(cè),你大概總結(jié)了一下,主要有下面幾方面的原因:
當(dāng)下的收益不高。 相比后端接口的單測(cè),前端單測(cè)寫起來相對(duì)復(fù)雜。 前端更多是面向 UI 的編程,但 UI 變動(dòng)大,難以使用 TDD (測(cè)試驅(qū)動(dòng)開發(fā)) 的開發(fā)模式。 沒有寫單測(cè)的習(xí)慣,可能是因?yàn)閱螠y(cè)增加了工作量,且沒有寫純函數(shù)的意識(shí),不利于測(cè)試。 單測(cè)的工具難學(xué)又難用。
你發(fā)現(xiàn)前端不喜歡寫單測(cè),有各種各樣的原因,但是當(dāng)你重構(gòu)那些復(fù)雜頁面,尤其是 jQuery 技術(shù)棧重構(gòu)為 React 技術(shù)棧的時(shí)候,單測(cè)真的非常有用。
比如這里有一個(gè)編輯頁面,包含兩部分:基本信息和運(yùn)行邏輯,在重構(gòu)運(yùn)行邏輯時(shí)候,你首先要保證的是重構(gòu)過后的頁面在保存的時(shí)候,保存的數(shù)據(jù)結(jié)構(gòu)必須跟之前的接口參數(shù)必須一致,所以在重構(gòu)運(yùn)行邏輯這個(gè)組件的時(shí)候就會(huì)有很多數(shù)據(jù)轉(zhuǎn)換邏輯。
可以看到為了保證你的新組件不影響保持原有功能,就要保證原始數(shù)據(jù)通過新組件的一頓操作最終保留了原來的結(jié)構(gòu),此時(shí)你就可以寫單測(cè)來保證這個(gè)過程。
describe('utils', () => {
it('流程圖:轉(zhuǎn)換為提交的數(shù)據(jù) transformForm', () => {
const result = transformForm(canvasData);
expect(result).toEqual(settingData);
});
it('流程圖:轉(zhuǎn)換為需要的數(shù)據(jù) parseRuleSetData', () => {
const [result] = parseRuleSetData(settingData, rules);
expect(result).toEqual(canvasData);
});
it('流程圖:反復(fù)轉(zhuǎn)換 transformForm - parseRuleSetData', () => {
const [result] = parseRuleSetData(visualSettings, rulesData);
const newResult = transformForm(result);
expect(newResult).toEqual(visualSettings);
});
});
復(fù)制代碼
前端單元測(cè)試寫起來復(fù)雜,其實(shí)只是 UI 的單測(cè)復(fù)雜而已,如果你把代碼做好了足夠的拆分,拆出更多函數(shù),更多 hooks ,單測(cè)就是輕而易舉了。
五、測(cè)試用例
你在的團(tuán)隊(duì),一直測(cè)試資源都不是充足,測(cè)試用例似乎一直都是一種可遇不可求的東西,尤其是在敏捷開發(fā)的趨勢(shì)下,產(chǎn)品功能變動(dòng)快,很少有測(cè)試會(huì)一直去維護(hù)那個(gè)最初的測(cè)試用例,往往是寫過用過就再也找不到了。但測(cè)試用例在重構(gòu)這個(gè)場(chǎng)景下,真的非常重要,他解決的核心問題是把測(cè)試同學(xué)拉到重構(gòu)的質(zhì)量保障中,一起梳理老的邏輯。這份寶貴的測(cè)試用例,可以成為你自測(cè)的依據(jù),也可以為你提供對(duì)于同一個(gè)功能的不同視角,如果你通過代碼看到的是實(shí)現(xiàn)細(xì)節(jié)的邏輯,那測(cè)試看到的就是整個(gè)鏈路的流程圖。很多中后臺(tái)系統(tǒng)都有管理態(tài)和運(yùn)行態(tài)之分,管理態(tài),前端是非常熟悉的,但是運(yùn)行態(tài),測(cè)試往往更加熟悉。
六、自測(cè)
拿到測(cè)試用例,你就可以自測(cè)了,但是這里有個(gè)坑,就是如果你完全依賴測(cè)試同學(xué)給你的測(cè)試用例。只要保證測(cè)試用例驗(yàn)證通過就行了,這種想法會(huì)出大問題,因?yàn)樨?fù)責(zé)這塊功能的測(cè)試可能是個(gè)新手,可能并不是一直負(fù)責(zé)這塊功能的測(cè)試,他們的測(cè)試用例可能只是浮于表面的。所以你需要把通過代碼考古發(fā)現(xiàn)的測(cè)試用例里沒有的邏輯,暴露給測(cè)試同學(xué),并補(bǔ)充到測(cè)試用例里。并且如果發(fā)現(xiàn)有一些看不懂的邏輯,就應(yīng)該搞懂他,那些你不懂的死角,往往上線后就會(huì)有大問題,不要心存僥幸。自測(cè)非常重要,但是往往你會(huì)覺得開發(fā)完了,就算是把這個(gè)事做完了,然后就去忙別的事了,并沒有好好的自測(cè),心想還有測(cè)試呢,等他們提問題,我再改吧。這是一種很普遍的程序員心理,其實(shí)很難避免,畢竟事情有很多。這個(gè)時(shí)候你可以找同組的開發(fā)同學(xué)幫你點(diǎn)一點(diǎn),先解決那些顯而易見的問題,也算是一個(gè)認(rèn)真負(fù)責(zé)的程序員了,不要讓測(cè)試同學(xué)給你提太多低級(jí) bug。
七、回歸測(cè)試
能有測(cè)試同學(xué)幫你做功能的回歸測(cè)試真是一件可遇而不可求的事,一定要珍惜,拿出你的大塊時(shí)間配合好。這其中最重要的就是多交流,測(cè)試同學(xué)也不一定知道所有的邏輯,在做回歸測(cè)試的時(shí)候,就需要開發(fā)和測(cè)試反復(fù)核對(duì)每個(gè)邏輯死角,弄清楚,才敢上線。當(dāng)然,能夠有測(cè)試幫你回歸的功能都是極易引起故障的功能,這里就有一個(gè)技巧就是如何拉測(cè)試參與你的重構(gòu)中來。像這樣重要的功能如果測(cè)試知道里面的邏輯,你可以懷著請(qǐng)教的心態(tài)去問對(duì)方,如果對(duì)方并不了解,那你就可以講給他聽,一個(gè)負(fù)責(zé)任的測(cè)試,應(yīng)該都非常想了解自己負(fù)責(zé)系統(tǒng)的重要模塊的來龍去脈。
八、灰度發(fā)布
即使你做了再多的測(cè)試,都有可能有沒有考慮到的遺漏點(diǎn),這個(gè)時(shí)候灰度就非常重要了,灰度就必須要有灰度工具才行。重構(gòu)一般是以頁面或者區(qū)塊為粒度按照人來進(jìn)行的。所以你的灰度工具必須要包含這些功能:
配置用戶或者用戶組 配置老路由和新路由 配置灰度狀態(tài)提示 新老頁面的自動(dòng)打點(diǎn)
灰度配置頁面,新老動(dòng)態(tài)路由的參數(shù)需要保持一致,這樣才能把參數(shù)傳遞下去。
展示灰度提示,并提供一個(gè)快速「返回舊版」的按鈕,為了更快速解決問題,可以給出開發(fā)者聯(lián)系方式。
當(dāng)用戶訪問老路由的時(shí)候,按照灰度配置驗(yàn)證當(dāng)前用戶是否在灰度中,如果在灰度中,則立即跳轉(zhuǎn)到新的路由,并顯示灰度提示。如果重構(gòu)的是頁面中的區(qū)塊,則可以提供灰度命中的方法,在頁面調(diào)用區(qū)塊的部分做判斷。
灰度策略可以按照以下用戶級(jí)別分布進(jìn)行:
L1:所有項(xiàng)目開發(fā),測(cè)試,設(shè)計(jì)師,內(nèi)部運(yùn)營人員 L2:核心用戶,建立釘釘群,觀察用戶反饋,及時(shí)解決用戶問題。 L3:適當(dāng)加入更多用戶,直到全量后,刪除灰度策略的配置。
發(fā)布后,注意觀察打點(diǎn)數(shù)據(jù):

打點(diǎn)的時(shí)候需要注意,要按照動(dòng)態(tài)路由來打點(diǎn),并分成命中灰度的,點(diǎn)擊使用舊版的,不在灰度內(nèi)的三個(gè)維度來看數(shù)據(jù),同時(shí)每天調(diào)整灰度用戶,這樣就能保證頁面是有人用的。如果有很多用戶使用了返回舊版的功能,那你就得找找這些用戶了解下情況了,到底是有 bug 還是交互不舒服,一對(duì)一的解決用戶問題,在反復(fù)去優(yōu)化你的頁面,慢慢擴(kuò)大用戶灰度范圍,直到老的路由訪問數(shù)據(jù) PV 為 0。
九、全量上線
全量上線并不是灰度所有人,而是真正下線老的頁面,并刪除老的代碼,只有到這一步才算重構(gòu)完成了。
十、總結(jié)
經(jīng)歷千難萬險(xiǎn),你終于把重構(gòu)好的頁面上線了,經(jīng)歷了這個(gè)過程,感慨良多,只求以后再也不要做重構(gòu)了,好好做需求不香嗎?后頭看看整個(gè)過程,要想重構(gòu)的頁面上線,不僅要下苦功夫,還要克服人性的一些弱點(diǎn),要做到這幾點(diǎn):
Double Check:讓其他人參與進(jìn)來,多一個(gè)人就能幫你發(fā)現(xiàn)更多問題。重構(gòu)面前,不要相信自己,相信伙伴。 邏輯無死角:不要還有不懂的代碼,不清楚的邏輯,按照程序員的第六感,不確定的都會(huì)出大問題。 集中注意力:重構(gòu)不能碎片化進(jìn)行,要集中大塊時(shí)間來做,并一做到底,不然過個(gè)幾天,你自己的代碼都會(huì)不認(rèn)識(shí)。 一跟到底:開發(fā)完成不是重點(diǎn),全量上線,并下掉老的頁面才是結(jié)束。
致敬每一位重構(gòu)路上的勇士。
