實戰總結|記一次迭代需求中的微型代碼重構

大家好,我是釘釘業務平臺前端技術的單丹。以往,歷經考勤、日誌、審批、開放平臺、工作臺等多個釘釘重點業務,分享更多的是偏技術架構或業務思考,這次,僅記錄下程序員一次普通的日常需求中的微型重構過程。

需求背景

一個頁面如下左圖,需求是將紅框內的部分移動到紅線箭頭所指的位置去,達成右圖效果。

需求評估

沒看到原頁面代碼之前可能認爲這是個很小的需求。頁面上組件移動一下位置不就好了嘛。需求很快完成,業務方滿意,我們都有美好的明天。看了代碼頓覺自己天真。並沒想象的這麼簡單,而且很容易出錯。爲描述清楚原因,下面使用了字母縮寫,大家可不用理解其中的業務含義。

要完成這個需求,要做的應該是這三件事:

1. 找到目標位置

2. 找到目標組件

3. 移動

先分析下現在的頁面結構:頁面Page包含A和B兩個組件,組件A有a1和a2兩個子組件,目標位置就在a1和a2之間,目標位置很清晰。問題在B裏。要移動的b1部分是在組件B內,但b1不是一個組件,而是一堆在B內部的代碼。b1邏輯中出現了2個組件AR和HR,不同條件下可能展示其中之一,或者不展示。且b1依賴了B上下文的10個屬性。如果目標b1不是組件,移動複雜度UPUP。

b1部分的代碼示意:

.../** b1部分的代碼 */let hRecord = null;const hasSupplySuite = hasSupplySuite();if (type === TypeENUM.REPAIR_CHECK && data && !hasSupplySuite) {hRecord = (<ARecordid={originatorInfo.workNo}pId={pId}/>);} else if (hasSupplySuite || type > 1) {hRecord = (<HRecordpId={pId}id={originatorInfo.workNo}count={currentCount}name={schema.title}username={originatorInfo.name}isLeave={type === TypeEnum.LEAVE}code={formData.code.value}/>);}if (!isUserInList() || location.href.indexOf('list') !== -1) {hRecord = null;}...

如果將b1這一堆邏輯從B組件中移植到A組件中去,需要關注這10個參數。就好像要從B房間通過窗口(組件的API)搬運10個形狀不一的箱子到A房間,沒有運輸工具,只能一個一個搬。A的窗口和B的窗口不一樣大小和形狀,有些可能送不進去,需要A擴展窗口。拔出蘿蔔帶出泥,這個過程很難乾乾淨淨不出錯。

A和B兩個組件的API已經三十多個了,估計是經年累月一個需求接一個需求疊加上去的,如果爲本次需求按照慣例要疊加更多API上去,並且需要注意避免遺漏和衝突。

這種方式很不美好,對未來擴展沒有任何助益,反而坑越挖越深。重構之心蠢蠢欲動。

當然,面對是否選擇重構,是會有不少想法的,例如:

1.重構帶來的好處通常是在未來兌現,現在一定要做嗎?

2.重構可要花我很多時間,但增加代碼的可讀性只是減少別人採坑,對我而言可能並沒有實際收益,我要做嗎?

3.重構還可能引起bug,帶來不必要的麻煩,我要承擔這個風險嗎?

這些可能也是老的應用代碼年久失修的部分原因。但是:

1.如果不重構,代碼中bad smell 越累越多,開發者就像進入到一個隨處都是垃圾的房間;

2.如果不重構,以後別人來改這部分代碼需要再重新去理解一遍,對團隊來講增加整體研發成本;

3.如果不重構,內心會很自責;

重構目的

爲時間成本可控,最小化重構範圍,需要圍繞需求明確本次的目標。

目標:讓完成本次需求變得更容易。

這需要做三件事:

1.將b1部分獨立成一個對移動友好的組件,降低移動的成本。

2.提升所到之處代碼的可讀性,降低理解代碼的成本。

3.不改變原有功能。

重構過程

老代碼重構,要格外小心。設計好重構步驟,每步一個獨立的小改動,做到每一步可測,每一步代碼可用,降低bug出現概率。

分析b1部分的代碼邏輯,其中引用了兩個組件,ARecord和HRecord,簡稱AR和HR。HR是個獨立的組件。AR是寫在組件B中。既然b1要封裝成組件,首先要將AR獨立封裝。

1. 使AR組件獨立,而不是附屬於B內

先將AR的代碼從B中遷出,稍加改造,將API進行封裝,補上類型定義。使用context表示引用組件宿主的上下文信息,例如可以將埋點頁面來源信息放到此對象中。componentProps表示組件數據。這一步相對比較簡單。

原代碼在B中:

class ARecord extends React.Component {openARecord = () => {const openUrl = `${getBaseUrl()}#/list`;openLink(openUrl);}render() {return (<div className="content" onClick={this.openARecord}>      ...</div>);}}

新代碼:

// 從AFlow中將ARecord遷移出來稍加改造// 組件接口定義interface IARecordProps {  className?: string;  style?: object;context: IContextModel;// 本組件的數據componentProps: {id: string;pId: string;  }}
export default function ARecord(props: IARecordProps) {const {    className,    style = {},    context = {},componentProps,  } = props;
const {    utSpace,  } = context;
const {    id,    pId,  } = componentProps;
const openARecord = (id: string, pId: string) => {const openUrl = `${getBaseUrl()}#/arecordlist`;openLink(openUrl);};
return <div className="content"     onClick={() => openARecord(id, pId)}  >  ...  </div>;}

2. 頁面功能測試

在Page中給B組件增加context屬性,在B組件內的原位置,引用新的AR組件,按照AR的API傳遞數據,測試頁面邏輯。測試通過。

3. AR和HR都具備了可移動的條件,開始封裝b1組件

這步有點棘手,主要原因是b1代碼和B組件耦合太深。依賴的有屬性,也有方法。先從大的方面着手,原來用了哪些屬性先不動。先定義新組件的對外API,原封不動和原組件對應。因爲原代碼裏沒有類型定義,確定原屬性是什麼類型還是花了一些時間。

順手補上類型定義。

組件封裝時,API設計很重要。我更習慣將容易變化的部分封裝,例如將組件的API設計爲兩個對象,context和componentProps,分別代表宿主上下文和組件數據。宿主使用起來簡單清晰,後續如有添加新屬性,只需增加數據處理的邏輯即可,不用改模板部分的代碼,代碼看起來簡潔,修改成本也更低。

// b1組件數據模型定義interface IHRecordSummaryData {hasSupplySuite: ()=>boolean;id: string;pId: string;type: TypeEnum;data: {};count: number;name: string;username: string;code: string;isUserInList: ()=>boolean;}// b1組件數據API定義interface IHRecordSummaryProps {  className?: string;  style?: object;context: IContextModel;// 本組件的數據componentProps: IHRecordSummaryData;}

順手,將原邏輯中if elseif 的語句,改成衛語句,直截了當更好理解。其他內部邏輯暫不動它。

export default function HRecordSummary(props: IHRecordSummaryProps) {  const {    context,    componentProps,  } = props;
  const {    id,    pId,  } = componentProps || {};
  const getComponentNode = (componentProps: IHRecordSummaryData) => {    const {      hasSupplySuite,      id,      pId,      type,      data,      count,      name,      username,      code,      isUserInList,    } = componentProps || {};
    const aRecordData = {      id,      pId,    }
    if (type === TypeENUM.REPAIR_CHECK && data && !hasSupplySuite) {      return  <ARecord        context={context}        componentProps={aRecordData}      />;    }         if (hasSupplySuite || type > 1) {      return  <HRecord        pId={pId}        id={id}        count={count}        name={name}        username={username}        isLeave={type === TypeEnum.LEAVE}        code={code}      />    }      if (!isUserInList || location.href.indexOf('list') !== -1) {      return null;    }  };
  return <div>    {getComponentNode(componentProps)}  </div>;}

4. 頁面功能測試

在B的原位置引用b1組件,按照組件數據類型定義組裝componentProps數據並測試。測試通過。

B封裝傳給b1的數據:

...    const context = {      utSpace: context?.utSpace,    };    const hRecordSummaryData = {      id: originatorInfo?.workNo,      pId: pId,      hasSupplySuite: hasSupplySuite(),      type: type,      data: data,      count: currentCount,      name: schema?.title,      username: originatorInfo?.name,      code: formData?.code?.value,      isUserInList: isUserInList(),    };...
return (  ...    <HRecordSummary      context={context}      componentProps={hRecordSummaryData}    >    </HRecordSummary>  ...)

5. b1組件糾結一下API設計

搬運的原依賴的屬性共10個,由於原10個屬性通過10個API從Page傳給B,再從B傳給b1,封裝成一個對象會更方便,未來擴展也只需要增加擴展字段的邏輯,而不需要修改模板部分的代碼。是否適合封裝成對象,還要看這10個在其他位置是否有使用。通過在B的代碼中搜索,10個屬性中只有1個是其他邏輯在使用,其餘的只在b1中使用。可以先封裝。

首先,將B的10個API合併成1個API,類型是對象,共10個屬性,在B中取到這1個對象傳給b1。

然後,在Page中組裝這個對象,傳給B,同時刪除B原來多餘的9個API,記得保留1個其他邏輯在使用的。對B來說,新增1個API,刪除了9個,B的API從37個減少到29個,代碼也清爽了一些。

6. 頁面功能測試

將從Page傳到B再傳到b1的10個屬性改成1個對象並測試。測試通過。

7. b1組件糾結一下類型定義

b1的原屬性中傳遞了2個方法,原樣搬過來的。查閱代碼,這兩個方法並有沒和當前上下文有關係,可以改成只傳方法的結果的boolean值,傳個方法不必要。

8. 頁面功能測試

將從Page傳到B再傳到b1的2個方法改成boolean值並測試。測試通過。

9. b1組件類型定義中data

只給組件設計必要的API,不傳冗餘的數據。

查閱代碼,同樣的,其中一個屬性data僅在條件判斷中用了一下,僅使用其是否存在作爲條件,但data本身數據量可不小。實際可以改成boolean值。

10. 頁面功能測試

將從Page傳到B再傳到b1的data改成boolean值並測試。測試通過。

11. 目前,具備了組件快速移動的條件

至此,代碼已經具備了,將組件快速移動位置的條件。後面操作起來就容易了。將ApproveHead添加同樣的組件數據類型定義,將ApprovePage傳給ApproveFlow的數據傳給ApproveHead,將組件引用從ApproveFlow改成ApproveHead。需求便完成。

12. 頁面功能測試

頁面功能迴歸測試,測試通過。

總結

好的代碼總讓人賞心悅目,讀者思路容易和作者達成一致。每次需求都是一次改善老代碼的機會。堅持寫讓人容易理解、讓人容易修改的代碼,將重構落入到每次需求迭代中,避免代碼日久年深而腐壞。

但需求迭代有周期,不能每次搞得翻天覆地。確定好每次重構的目的,適可而止,既可以尊重業務節奏,又可以進行優化。銖積寸累,日就月將,也會有所改變。

"任何一個傻瓜都能寫出計算機可以理解的程序,只有寫出人類容易理解的程序纔是優秀的程序員。" - Martin Flower

點擊立即免費試用雲產品 開啓雲上實踐之旅!

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章