踩坑實(shí)戰(zhàn):如何走出“萬劫不復(fù)”的代碼重構(gòu)深淵?
原創(chuàng)【51CTO.com原創(chuàng)稿件】 Martin Fowler 在其經(jīng)典著作《重構(gòu):改善既有代碼的設(shè)計(jì)》中給出了下述定義。
“重構(gòu)是一個(gè)改變軟件系統(tǒng)的過程,它旨在不改變代碼外部行為的前提下,改善代碼內(nèi)部的結(jié)構(gòu)。它用一種規(guī)范的方式來清理代碼,從而***限度地減少錯(cuò)誤出現(xiàn)的幾率。從本質(zhì)上說,重構(gòu)是在代碼寫完之后對(duì)其設(shè)計(jì)的改善。”
我最近有幸參與了一個(gè)復(fù)雜的代碼重構(gòu)項(xiàng)目,它是由 React 和 Redux 來構(gòu)建的一個(gè)銷售預(yù)算管理與分析的應(yīng)用系統(tǒng)。
該系統(tǒng)大約由 300 個(gè)文件,共計(jì) 13000 行代碼所組成。由于能夠定期地接觸到代碼庫(kù),因此我對(duì)于在遵從代碼標(biāo)準(zhǔn)的基礎(chǔ)上如何進(jìn)行代碼改進(jìn)比較熟悉。
以下是我的最終目標(biāo)列表:
- 更新文件夾結(jié)構(gòu),以集中 Redux 的各種文件(actions、reducers、selectors)。
- 將 Foundation 框架轉(zhuǎn)換為 Semantic UI React。
- 實(shí)現(xiàn) CSS 模塊。
- 升級(jí)到 webpack3(一種模塊打包器)。
- 將 Fetch 替換為 jQuery,以處理各種 HTTP 請(qǐng)求(使用 polyfill)。
- 刪除不必要的抽象類和“死代碼”。
- 安裝并配置 Jest 測(cè)試框架,編寫各種單元測(cè)試。
經(jīng)過一番周折之后,我最終完成了上述目標(biāo)。在此,我將自己從該項(xiàng)目以及其他過往項(xiàng)目中所獲取的寶貴經(jīng)驗(yàn)與技術(shù)分享給大家。
提出正確的問題
在決定開始進(jìn)行代碼重構(gòu)項(xiàng)目之前,讓我們先理清一些重要的問題。
首先,捫心自問:我需要重構(gòu)嗎?
所有程序員都希望能寫出干凈優(yōu)雅的代碼,但是事實(shí)并非如此。各種截止日期的臨近和需求的變更,往往只是大量問題的開始而已。
面對(duì)龐大的代碼庫(kù),您可能早已喪失了重構(gòu)的動(dòng)力。那么請(qǐng)您在閱讀了下列問題并能夠回答“是”之后,再考慮如何開展重構(gòu)工作吧:
- 是否存在重大的技術(shù)缺陷而造成了系統(tǒng)的巨大問題?
- 添加各種新功能是否困難?
- 對(duì)于某部分代碼庫(kù)的細(xì)微更改,是否會(huì)破壞應(yīng)用中另一部分的某個(gè)不相關(guān)的功能?
- 您是否還使用了那些存在著安全與性能問題的過時(shí)依賴關(guān)系?
- 在 JavaScript 環(huán)境中,ES5 的語法是否能夠被箭頭函數(shù)(arrow functions)和解構(gòu)(destructuring)等新的語言功能所增強(qiáng)?
我建議您多與同事,特別是該領(lǐng)域的高級(jí)工程師討論,而不是盲目開始。他們可能會(huì)詳細(xì)地說明為什么會(huì)以此種方式來編寫代碼,或者給您提供一些能夠影響決斷的有價(jià)值的見解。
某位經(jīng)驗(yàn)豐富的工程師甚至還會(huì)提醒您他們?cè)诖a重構(gòu)中失敗過,因此在某種程度上說這次也不適合再次進(jìn)行重構(gòu)。
接下來,問問自己:我能夠重構(gòu)嗎?
現(xiàn)在您面臨著下一個(gè)障礙是:確定重構(gòu)是否可行??尚械臈l件包括如下限制因素:
- 根據(jù)我目前的技能組合,我能勝任重構(gòu)嗎?
- 根據(jù)我的時(shí)間安排,我能勝任重構(gòu)嗎?
- 重構(gòu)時(shí),我可以添加新的功能嗎?
- 根據(jù)我當(dāng)前的預(yù)算,我能勝任重構(gòu)嗎?
如果您不能對(duì)上述所有的問題回答“Yes”,那么重構(gòu)可能對(duì)您來說就是自尋煩惱了。
我的經(jīng)驗(yàn)是:您需要得到軟件產(chǎn)品所有者的批準(zhǔn)與支持,因?yàn)樗麄儠?huì)經(jīng)常與客戶直接合作并能管理預(yù)算,切忌擅自行事!
***,自問自答:我愿意重構(gòu)嗎?
代碼重構(gòu)向來是一項(xiàng)巨大的工程。那些嚴(yán)苛的預(yù)算和時(shí)間表往往會(huì)給人們帶來難以想象的壓力。
因此,您可能會(huì)時(shí)常詢問自己如下的問題:
為什么代碼不是在一開始就以正確的方式被編寫呢?
如果應(yīng)用能夠正常工作的話,何必要重構(gòu)它們呢?
如果它沒那么糟糕的話,就不能只是添加點(diǎn)新的功能嗎?
這樣做能夠增加企業(yè)的價(jià)值嗎?
面對(duì)上述問題,代碼重構(gòu)常被人們誤解為一項(xiàng)吃力不討好的任務(wù)。而如果一些現(xiàn)有的功能因?yàn)榇a重構(gòu)而產(chǎn)生中斷的話,那么重構(gòu)工作就會(huì)變得更加“萬劫不復(fù)”了。
盡管困難重重、弊端多多,但是在完成了上述“盡職調(diào)查”之后,您會(huì)發(fā)現(xiàn)代碼重構(gòu)還是非常值得我們投入寶貴的時(shí)間去開展的。
在清理代碼庫(kù)的同時(shí),添加良好的單元測(cè)試會(huì)有益于新功能的輕松添加,以及回歸類錯(cuò)誤的(regression bug)大幅降低。
制定計(jì)劃
“如果沒有計(jì)劃,您就是在計(jì)劃失敗。”
--本杰明富蘭克林如是說。
一旦開始決定重構(gòu)某個(gè)應(yīng)用程序,您的***本能應(yīng)該是:深挖代碼并清理不適合的代碼。然而事實(shí)證明:如果沒有一個(gè)事先的計(jì)劃,您將難逃“劫難”。
在按照時(shí)間和預(yù)算的范圍制定出行動(dòng)計(jì)劃之前,您先別急著修改代碼。代碼重構(gòu)的目的是為了讓整個(gè)團(tuán)隊(duì)受益于代碼價(jià)值的***化。
如果代碼中的某些部分雖然顯得特別混亂,但是能夠提供正常服務(wù)的話,那么就沒有必要迅速對(duì)它們進(jìn)行重構(gòu)。
我們稍后會(huì)討論區(qū)分優(yōu)先級(jí)的問題,而當(dāng)前您至少應(yīng)該先對(duì)代碼的潛在價(jià)值有所預(yù)判。
以下列出了能夠保證您的重構(gòu)項(xiàng)目正確開始的若干步驟。即使您的重構(gòu)項(xiàng)目相對(duì)較小,我仍然建議您參考并使用下列技術(shù)。
選擇一種項(xiàng)目管理工具
無論代碼重構(gòu)的范圍是大還是小,您都需要用一種工具來跟蹤項(xiàng)目進(jìn)度。我是 Trello 的忠實(shí)粉絲,并認(rèn)為其“看板(Kanban)”風(fēng)格界面非常適合于項(xiàng)目管理。
當(dāng)然,您完全可以挑選自己喜歡的工具,而且***具有對(duì)任務(wù)進(jìn)行排序、分組、添加注釋、以及說明的功能。同時(shí),如果它能夠添加附件或創(chuàng)建標(biāo)簽的話,那就更是錦上添花了。
以我的項(xiàng)目為例,我創(chuàng)建了一個(gè) Trello 看板,并通過如下列表名稱來跟蹤各項(xiàng)任務(wù):
- 積壓工作
- 下一步
- 進(jìn)行中
- 拉取請(qǐng)求(PullRequest)
- 關(guān)閉
我還創(chuàng)建了一個(gè)標(biāo)簽專門用來表示某個(gè)任務(wù)是否涉及到 React 組件、Redux元素(如 actions、reducers、selectors)、以及應(yīng)用配置等方面。
如果您和我一樣屬于“視覺系”的,則可以通過給標(biāo)簽添加顏色,來迅速了解其表征的特性,而不需仔細(xì)閱讀其標(biāo)題或相關(guān)說明。
例如,我創(chuàng)建了一個(gè)將 webpack 從 1.0 升級(jí)到 3.0 的任務(wù),并為 Configuration 標(biāo)簽設(shè)定了特殊顏色,以便我能在面板上快速地識(shí)別出來。
查找邏輯上下文
如果代碼量非常大的話,您可以試著將應(yīng)用程序解構(gòu)為多個(gè)模塊或上下文的關(guān)系。
此處“上下文”表示為:隸屬于某個(gè)特定業(yè)務(wù)實(shí)體或是應(yīng)用程序配置的代碼段。如果您所面對(duì)的代碼庫(kù)頗為混亂的話,該過程將具有挑戰(zhàn)性。
不過,即使只是粗略地研究與劃分,也會(huì)有助于您更進(jìn)一步熟悉代碼庫(kù),甚至讓您受益匪淺。在多數(shù)情況下,您可以根據(jù)應(yīng)用的服務(wù)流程來推斷出上下文關(guān)系。
例如,一個(gè)為牙科診所安排日程表的應(yīng)用,就可以被分為如下的上下文關(guān)系:
- 病人
- 約診
- 用戶導(dǎo)航
- 牙科記錄
- 用戶管理
在實(shí)踐中,我們經(jīng)常會(huì)碰到的棘手部分是:如何正確把控粒度。對(duì)于我所開發(fā)過的應(yīng)用而言,我一般會(huì)根據(jù) API 的調(diào)用和現(xiàn)有 Redux 的 reducer 來確定上下文關(guān)系。
我通常會(huì)定義出:用戶類上下文、超級(jí)用戶類上下文(用于各種管理操作)、應(yīng)用類上下文(用于 UI 的狀態(tài))、以及其他類型。
注意:不要過于苛求***的上下文關(guān)系,這一步的目的只是為了簡(jiǎn)化任務(wù)創(chuàng)建的過程。
創(chuàng)建任務(wù)
您必須創(chuàng)建出具有明確范圍的任務(wù)。在此,您可以根據(jù)“拉取請(qǐng)求”的方式來考慮范圍。
雖然誰都不想一次性提交具有 5000 行代碼之多的變更,但是在 5000 行代碼中只提交 2 處修改顯然也是遠(yuǎn)遠(yuǎn)不夠的。
以我曾經(jīng)參與過的一個(gè)重構(gòu)項(xiàng)目為例,其目標(biāo)是從 Foundation 框架轉(zhuǎn)換到語義 UI 的 React,并且實(shí)現(xiàn) CSS 模塊。
請(qǐng)參考:https://foundation.zurb.com/
我最初創(chuàng)建了單一的任務(wù)來表示這一轉(zhuǎn)化,但是我立即意識(shí)到了它所牽扯到的巨大工作量。
在該應(yīng)用中,有著近 100 個(gè) React 組件需要被更新,而我并不想在 Trello 中創(chuàng)建 100 個(gè)任務(wù)來代表每一個(gè)單獨(dú)的組件。
在此情況下,我定義了一些簡(jiǎn)單的上下文關(guān)系:
- 首先,我在每個(gè)上下文中創(chuàng)建了不同的任務(wù),來重構(gòu)與 Redux 相連接的各個(gè)容器組件。
- 接下來,我查看了共享的 /components 目錄,并按照表單控件、圖表等類別對(duì)它們進(jìn)行分組。
- ***,我創(chuàng)建了單獨(dú)的任務(wù)來重構(gòu)每一組共享的組件。
下圖是我的看板界面截圖,上面包含了一些示例:
當(dāng)然,您也需要考慮到自己的變更對(duì)于應(yīng)用的影響。如果可能需要恢復(fù)到舊的版本,您一定不想在大段的變更代碼中費(fèi)力地深挖出造成某個(gè) Bug 的原因。
曾經(jīng)有一次,我為了某個(gè) Bug 而不得不撤銷了自己的絕大部分的更改。在此之后我試著用小塊的程序代碼去進(jìn)行重構(gòu)。
為任務(wù)排序
如果您愿意的話,可以考慮為任務(wù)制定排序或優(yōu)先級(jí)的一些標(biāo)準(zhǔn)。當(dāng)然,如果您已經(jīng)為每一項(xiàng)任務(wù)創(chuàng)建了明確的范圍,那么完成它們的順序也就一目了然了。
我傾向于將具有相似特定功能的任務(wù)進(jìn)行分組處理,例如根據(jù) Redux 的元素或 API 管理來區(qū)分。
當(dāng)然,如果您是新手的話,我會(huì)建議您先去“摘取那些低垂的果實(shí)”,即一些僅付出少量的努力就會(huì)效果明顯的任務(wù)。
持續(xù)更新您的計(jì)劃
隨著對(duì)于代碼的“深耕”,您可能會(huì)發(fā)現(xiàn)在前期重構(gòu)計(jì)劃里不準(zhǔn)確的地方,那么請(qǐng)不要猶豫,盡快修正以免覆水難收。
重構(gòu)代碼庫(kù)的好處往往是無形且難以衡量的。哪怕最終不得不終止重構(gòu)計(jì)劃,您的這份詳細(xì)的計(jì)劃也能為下一次重構(gòu)的“重啟”提供寶貴的資源和“追蹤”的線索。
測(cè)試的重要作用
如果連您都不喜歡測(cè)試自己的產(chǎn)品,那么很可能您的客戶也不會(huì)樂意試用它。
下面我將引導(dǎo)您來建立一個(gè)有效的測(cè)試架構(gòu)??紤]到方便搭建與運(yùn)行,我使用了Jest 和 Enzyme。
參考:https://facebook.github.io/jest/
http://airbnb.io/enzyme/
您可以根據(jù)如下鏈接的指引,將這兩個(gè)庫(kù)配置到 React/Redux 的應(yīng)用之中:
- 使用 Jest 和 Enzyme 實(shí)施基本組件測(cè)試
https://hackernoon.com/implementing-basic-component-tests-using-jest-and-enzyme-d1d8788d627a
- 如何用 Jest 測(cè)試 React 的各種組件
https://www.sitepoint.com/test-react-components-jest/
設(shè)置模擬數(shù)據(jù)
如果測(cè)試的是現(xiàn)有應(yīng)用,那么您應(yīng)該了解現(xiàn)有數(shù)據(jù)的形態(tài)和使用方式。在大多數(shù)情況下,隨著新功能的添加,API 會(huì)被微調(diào)與改進(jìn)。
對(duì)于我的重構(gòu)應(yīng)用項(xiàng)目,我創(chuàng)建了兩個(gè)帶有數(shù)據(jù)的文件:一個(gè)帶有API的各種響應(yīng),而另一個(gè)則帶有 Redux 的狀態(tài)。
您可能需要通過稍許的修改和大量的復(fù)制/粘貼來創(chuàng)建這兩個(gè)文件,不過這些工作都是一次性的。
產(chǎn)生這兩個(gè)數(shù)據(jù)文件的目的有兩個(gè):
- 首先(也是最明顯的),您需要測(cè)試的許多元素都會(huì)以某種方式來顯示或操縱數(shù)據(jù)。
- 其次,通過快速地參考數(shù)據(jù)的形態(tài),以便有效地確定如何撰寫測(cè)試程序,并分析代碼可能出現(xiàn)的問題。
如果所測(cè)數(shù)據(jù)過于敏感的話,有時(shí)候存儲(chǔ) API 的響應(yīng)與狀態(tài)并不太可行。在此情況下,您可以使用帶有 fakcer 的 json-schema-faker 庫(kù),或 chance 庫(kù)來生成隨機(jī)數(shù)據(jù)。
參考:https://www.npmjs.com/package/json-schema-faker
https://www.npmjs.com/package/chance
我建議您一次性生成數(shù)據(jù)并存儲(chǔ)到庫(kù)中,而不是每次在運(yùn)行測(cè)試時(shí)都使用其“種子”來生成新的數(shù)據(jù)。
我將自己的文件存放在__fixtures__文件夾中,目錄結(jié)構(gòu)如下所示:
- /src
- /components
- /constants
- /containers
- /redux
- /__fixtures__
- /state.js
- /responses.js
- /app
- /appActions.js
- /appReducer.js
- /appSelectors.js
- /...
獲取 Redux 整體狀態(tài)的最簡(jiǎn)便方法是使用 Redux DevTools 的擴(kuò)展。
參考:https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en
您可以在狀態(tài)視圖中選擇 Raw 選項(xiàng)卡,復(fù)制所有內(nèi)容,并將其粘貼到一個(gè)帶有 module.exports 聲明的 JavaScript 文件中。
我建議您只從狀態(tài)和 API 的響應(yīng)中獲取小部分的記錄,以減少 Jest 整體快照的大小。而具體保留多少條記錄則完全取決于您自己的判斷。
例如:某個(gè) API 的響應(yīng)返回了一個(gè)包含著 400 條記錄的數(shù)組,那么您肯定需要去除其中的絕大部分,以提高測(cè)試效率。
值得注意的是:使用有效的數(shù)據(jù)對(duì)于防止回歸錯(cuò)誤的產(chǎn)生是至關(guān)重要的。如果你用到的數(shù)據(jù)并不能代表應(yīng)用中真實(shí)環(huán)境所用到的內(nèi)容,那么測(cè)試的效果是無法保證重構(gòu)質(zhì)量的。
標(biāo)準(zhǔn)化您的測(cè)試
您在重構(gòu)項(xiàng)目中往往需要編寫大量的測(cè)試。在剛開始寫測(cè)試的時(shí)候,我經(jīng)常會(huì)出現(xiàn)命名不統(tǒng)一的情況。
例如:在測(cè)試兩個(gè)非常相似的組件時(shí),我所用到的 describe 時(shí)常不盡相同。同時(shí),標(biāo)準(zhǔn)化您的測(cè)試將有利于減少團(tuán)隊(duì)花費(fèi)在理解測(cè)試程序上的時(shí)間。
確定測(cè)試文件的位置
大部分人喜歡復(fù)制整個(gè) src/ 目錄,并將測(cè)試文件放在那里。無論您喜歡用 .spec.js 還是用 .test.js 作為測(cè)試文件的擴(kuò)展名,都要注意一致性。
Jest 的默認(rèn)配置會(huì)指定將測(cè)試文件放置在 __tests__ 目錄,并以 .test.js 作為擴(kuò)展名。
一旦您將此作為了標(biāo)準(zhǔn),請(qǐng)務(wù)必添加到自述文件(README)之中,以便將來使用該應(yīng)用的程序員能夠籍此遵守下去。
建立格式
您應(yīng)當(dāng)為每個(gè)正在測(cè)試的上下文(如:React 組件、Redux 的 selectors 等)建立起“格式/結(jié)構(gòu)”(format/structure)的關(guān)系。
例如,我創(chuàng)建的每個(gè) React 組件和容器的測(cè)試文件都會(huì)在其頂部有一個(gè)如下所示的 setup 函數(shù):
- const setup = (propOverrides, renderFn = shallow) => {
- const props = {
- propA: 'Some Value',
- propB: false,
- onClick: jest.fn(),
- ...propOverrides,
- };
- const wrapper = renderFn(<AppComponent {...props} />);
- return { props, wrapper };
- };
這會(huì)使得我在測(cè)試組件時(shí)非常輕松,而無需編寫大量額外的樣板引用。同時(shí),我還為 React 組件建立了一個(gè)特定的 describe 塊結(jié)構(gòu)。
- describe('Component A', () => {
- describe('Snapshot validation', () => {
- it('matches its snapshot with valid props', () => {
- const { wrapper } = setup();
- expect(wrapper).toMatchSnapshot();
- });
- });
- describe('Event validation', () => {
- it('fires props.onClick when button is clicked', () => {
- const { wrapper, props } = setup();
- wrapper.find('button').simulate('click');
- expect(props.onClick).toHaveBeenCalled();
- });
- });
- // Note: This is only for connected components.
- describe('Redux validation', () => {
- const store = {
- getState: () => state,
- dispatch: jest.fn(),
- subscribe: () => {},
- };
- it('renders when connected to Redux state', () => {
- const wrapper = shallow(<ComponentA store={store} />);
- expect(wrapper).toHaveLength(1);
- });
- });
- });
對(duì)于 Redux 的 actions、reducers 和 selectors,我進(jìn)行了同樣的操作。根據(jù)具體的測(cè)試環(huán)境,我還會(huì)使用 WebStorm 的文件模板功能,以快速地創(chuàng)建各種測(cè)試文件。
參考:https://www.jetbrains.com/help/webstorm/creating-and-editing-file-templates.html
如果您使用的程序編輯器能夠支持代碼片段或文件模板的話,我建議您事先創(chuàng)建好相應(yīng)的模板,以保持格式上的規(guī)范。同樣,請(qǐng)記得在自述文件中留下簡(jiǎn)要的概述說明。
編寫測(cè)試
假設(shè)您正在著手開始重構(gòu) Redux 的 actions、reducer 和 selectors 的 UI 狀態(tài)。由于您已經(jīng)掌握了相應(yīng)的狀態(tài)數(shù)據(jù),因此編寫測(cè)試相對(duì)來說會(huì)比較簡(jiǎn)單。
您只需要使用類似 redux-mock-store 的庫(kù)來模擬出用于測(cè)試 actions 的狀態(tài)即可。
參考:https://github.com/arnaudbenard/redux-mock-store
請(qǐng)務(wù)必在更改任何代碼之前編寫好了所有的測(cè)試。通過對(duì)重構(gòu)之后的代碼進(jìn)行測(cè)試,我們能夠發(fā)現(xiàn)一些人為的或無意犯下的錯(cuò)誤。
而事先保存好快照,則有助于我們發(fā)現(xiàn)那些字段或?qū)ο箨P(guān)鍵字上的拼寫錯(cuò)誤。
您應(yīng)當(dāng)對(duì)完成了重構(gòu)的代碼部分,和任何直接受到變更影響的代碼編寫測(cè)試程序。
雖說深挖程序間的依賴關(guān)系、并編寫出涉及到應(yīng)用各個(gè)方面的測(cè)試,會(huì)是一項(xiàng)比較繁瑣的工程,但是這會(huì)給您提供對(duì)于整個(gè)代碼庫(kù)的深入解析,并為代碼的重構(gòu)提供更多的整改機(jī)會(huì)。
應(yīng)該測(cè)試什么?確定測(cè)試內(nèi)容的最簡(jiǎn)單和可靠的方式是:代碼覆蓋率。
Jest 具有內(nèi)置的代碼覆蓋率檢查功能,您可以用來生成帶有覆蓋率百分比的 HTML 報(bào)告,以顯示代碼中的哪些部分目前未被測(cè)試所覆蓋到。
雖然行覆蓋(Line coverage,查看是否每一行都執(zhí)行了?)會(huì)讓您頗有成就感,但是分支覆蓋(branch coverage,查看是否每個(gè) if 代碼塊都執(zhí)行了?)才是您需要去關(guān)注的地方。
更多有關(guān)不同覆蓋類型的概念,請(qǐng)參考:http://jasonrudolph.com/blog/2008/06/10/a-brief-discussion-of-code-coverage-types/
如何知道自己已經(jīng)完成了?如前面所述,覆蓋率是評(píng)估代碼的某個(gè)部分是否通過了測(cè)試的***工具。
如果您的某個(gè)函數(shù)中包含一個(gè) if 的聲明,而它的 else 條件卻沒被測(cè)試所覆蓋到,那么就會(huì)被覆蓋率報(bào)告所指出。
我經(jīng)常會(huì)習(xí)慣性地多看幾次函數(shù)代碼,并理解其邏輯關(guān)系,然后編寫出能夠故意破壞它的測(cè)試用例,包括:如果 API 的響應(yīng)中缺少某個(gè)字段會(huì)怎么樣?如果響應(yīng)為空又會(huì)發(fā)生什么?
例如,假設(shè)有個(gè) selector 能夠加總某個(gè)特定區(qū)域里每個(gè)銷售員所分配到的預(yù)算。而銷售經(jīng)理則擁有該地區(qū)所有可用的預(yù)算總和。
顯然,所有可用預(yù)算的總和應(yīng)當(dāng)始終大于或等于分配出去的總預(yù)算。那么,如果小于的話,會(huì)發(fā)生什么?代碼中是否有 if 的聲明來涉及這方面呢?
通過閱讀代碼和編寫測(cè)試,您可考慮到更多的極端情況。由此可見,代碼覆蓋率(function coverage)可以反映出每個(gè)函數(shù)是否被測(cè)試所調(diào)用到的情況。
重寫代碼
通過上述部分,您應(yīng)該已經(jīng)制定出了計(jì)劃、選取了相應(yīng)任務(wù)、編寫出了測(cè)試,那么是否現(xiàn)在就可以開始打開某個(gè)文件、修正變量名稱并清理代碼了呢?
為了確保重構(gòu)的順利進(jìn)行,我建議您先熟悉一些基本的概念。下面,我將向您介紹一些在代碼重構(gòu)過程中的常見錯(cuò)誤和化繁為簡(jiǎn)的技巧。
識(shí)別自動(dòng)化的可能性
您很可能會(huì)碰到需要移動(dòng)并梳理到正確的位置的大量文件。
例如,我曾經(jīng)在重構(gòu)一個(gè)應(yīng)用時(shí)發(fā)現(xiàn)其 Redux 的 actions、reducers 和 selectors 都分屬于自己?jiǎn)为?dú)的文件夾,而我需要將它們按照模塊(例如 appActions.js、appReducer.js 和 appSelectors.js)進(jìn)行分類。
因此,我需要運(yùn)行一條 git mv 的命令,將 /actions/app.js 移動(dòng)到 /redux/app/appActions.js,并且對(duì)于 /reducers/app.js 和 /selectors/app.js 要執(zhí)行相同的操作。
由于該應(yīng)用項(xiàng)目中有 11 個(gè)模塊,因此我必須輸入 33 次 git mv 命令。另外,我還需要再運(yùn)行 150 次 git mv,以將 React 的容器和組件放置到正確的文件夾位置。
因此,面對(duì)如此“崩潰”的任務(wù),我并沒有手動(dòng)地逐條輸入命令,而是使用 JavaScript 和 Node.js 編寫了一個(gè)腳本來實(shí)現(xiàn):
- const fs = require('fs');
- const path = require('path');
- const chalk = require('chalk');
- const sh = require('shelljs');
- const _ = require('lodash');
- const sourcePath = path.resolve(process.cwd(), 'src');
- // This is the new /src/redux folder that gets created:
- const reduxPath = path.resolve(sourcePath, 'redux');
- // I used "entities" instead of "modules", but they represent the same thing:
- const entities = [
- 'app',
- 'projects',
- 'schedules',
- 'users',
- ];
- const createReduxFolders = () => {
- if (!fs.existsSync(reduxPath)) fs.mkdirSync(reduxPath);
- // Code to create entities folders in /src/redux...
- };
- // Executes a `git mv` command (I omitted some additional code that validates
- // if the file already exists for brevity).
- const gitMoveFile = (sourcePath, targetPath) => {
- console.log(chalk.cyan(`Moving ${sourcePath} to ${targetPath}`));
- const command = `git mv ${sourcePath} ${targetPath}`;
- sh.exec(command);
- console.log(chalk.green('Move successful.'));
- };
- const moveReduxFiles = () => {
- entities.forEach(entity => {
- ['actions', 'reducers', 'selectors'].forEach(reduxType => {
- // Get the file associated with the specified entity for the specified reduxType,
- // so the first file might be /src/actions/app.js:
- const sourceFile = path.resolve(sourcePath, reduxType, `${entity}.js`);
- if (fs.existsSync(sourceFile)) {
- // Capitalize the reduxType to append to the file name (e.g. appActions.js):
- const fileSuffix = _.capitalize(reduxType);
- // Build the path to the target file, so this would be /src/redux/app/appActions.js:
- const targetPath = `${reduxPath}/${entity}`;
- const targetFile = `${targetPath}/${entity}${fileSuffix}.js`;
- // Execute a `git mv` command for the file:
- gitMoveFile(sourceFile, targetFile);
- }
- });
- });
- };
- moveReduxFiles();
您既可以通過腳本來自動(dòng)遷移文件的路徑,也可以通過腳本來直接修改路徑的名稱。
當(dāng)然,您需要注意投入產(chǎn)出比,不要花費(fèi)了 20 小時(shí)去編寫一個(gè)腳本,卻只是節(jié)省了 1 個(gè)小時(shí)手動(dòng)工作量。
由于大多數(shù)代碼庫(kù)、及其結(jié)構(gòu)都相對(duì)獨(dú)特,因此一般您編寫腳本的復(fù)用性都不高。
持續(xù)提交
您所重構(gòu)的應(yīng)用程序越多、時(shí)間越長(zhǎng),就越難以記住和追蹤那些在不同文件里的細(xì)微修改。
而對(duì)于各種文件的累計(jì)且大量更改,勢(shì)必給您的應(yīng)用測(cè)試帶來失敗的風(fēng)險(xiǎn)。因此,無論代碼的修改量大或小、多或少,請(qǐng)記得予以持續(xù)提交。
以某次應(yīng)用重構(gòu)為例,我就進(jìn)行了 1747 次提交,涉及到 659 個(gè)文件中的 76080 行代碼,總體占用的存儲(chǔ)空間為 10MB。
另外,在多次且持續(xù)的提交過程中,您可以通過限制每一次更改的內(nèi)容和文件的數(shù)量,以便您能夠隨時(shí)按需“跳回”到某一個(gè)可靠的“保存點(diǎn)”。
抵御“分心”
請(qǐng)暫時(shí)避免清理那些手頭任務(wù)范圍之外的代碼,這也是代碼重構(gòu)過程中最困難的方面之一。
假設(shè)您遇到了一個(gè)使用 Object.assign() 的 selector,而您的后續(xù)任務(wù)之一是更新代碼,使用類似 spread syntax 新的 ESNext 功能。
參考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
那么請(qǐng)不要偏離當(dāng)前的任務(wù)走向,哪怕只是對(duì)該代碼進(jìn)行微不足道的改變,都不要放在現(xiàn)在進(jìn)行。
我的經(jīng)驗(yàn)是:在相應(yīng)的代碼處添加了一條 //REFACTOR: Fix this later 的注釋,然后繼續(xù)自己的當(dāng)前任務(wù)。
有關(guān)拉取請(qǐng)求的方法論
有時(shí)候,您花費(fèi)了大量的時(shí)間去清理一部分代碼庫(kù),而難以后退到過去的某個(gè)時(shí)間點(diǎn),或無法對(duì)修改后的代碼質(zhì)量進(jìn)行評(píng)估。
那么拉取請(qǐng)求往往就能夠幫助您和同事來評(píng)審這些修改,以確定是否有益于代碼的優(yōu)化:
- 當(dāng)您在提交拉取請(qǐng)求時(shí),請(qǐng)?jiān)谡幗o予盡可能詳細(xì)的描述。如果您使用的是 GitHub,那么新的拉取請(qǐng)求會(huì)伴隨著一個(gè)模板。您可以在其中填寫相應(yīng)的標(biāo)題、總體描述、重要章節(jié)的變更列表等各種審評(píng)人員所關(guān)注的信息。
- 您需要注意的一個(gè)指標(biāo)是:與拉取請(qǐng)求相關(guān)的代碼更改數(shù)量。請(qǐng)盡量限制更改的代碼行數(shù)在 500 行左右。由于 Jest 快照文件的添加會(huì)使得拉取請(qǐng)求動(dòng)輒添加數(shù)千行,因此請(qǐng)務(wù)必在摘要中包含與之相關(guān)的注釋。
- 如果您無法控制改變的數(shù)量,那么就請(qǐng)盡量降低其復(fù)雜性。
- 如果您只是對(duì)重要的聲明語句進(jìn)行重新排序的話,只要此類更改并不太復(fù)雜,超過 2000 行的代碼量還是可以接受的。
- 請(qǐng)將同類變更盡量限定在同一次拉取請(qǐng)求之中。
- 您可以大膽地在注釋中寫上“本次并未做邏輯上的修改”,以節(jié)約審閱者的理解時(shí)間。
寫在***
我們從代碼重構(gòu)項(xiàng)目的實(shí)施角度向您提供了:盡可能自動(dòng)化、持續(xù)提交、抵御“分心”,以及善用拉取請(qǐng)求等方面的建議。
【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文作者和出處為51CTO.com】






