我是如何將業(yè)務(wù)代碼寫優(yōu)雅的
本文作者:螞蟻保險(xiǎn)-體驗(yàn)技術(shù)組-祎遠(yuǎn)
https://juejin.im/user/5cc7d4d26fb9a0320f7df265
0x00 前言
我是一名來自螞蟻金服-保險(xiǎn)事業(yè)群的前端工程師,在一線大廠的業(yè)務(wù)部門寫代碼,非常辛苦但也非常充實(shí)。業(yè)務(wù)代碼不同于框架代碼、個(gè)人項(xiàng)目或者開源項(xiàng)目,它的特點(diǎn)在于邏輯復(fù)雜、前后依賴多、可復(fù)用性差、迭代周期短,今天辛辛苦苦寫的代碼,上線運(yùn)行一周可能就下線了。能熟練書寫框架代碼、構(gòu)建底層基礎(chǔ)設(shè)施的工程師不一定能寫好業(yè)務(wù)代碼。
有人說,業(yè)務(wù)代碼無非就是按部就班,優(yōu)不優(yōu)雅?who care?。但實(shí)際業(yè)務(wù)規(guī)則復(fù)雜得多,不是依葫蘆畫瓢就能輕松解決的,寫一段糟糕的代碼,可能要用雙倍的時(shí)間去發(fā)現(xiàn)和解決問題,麻煩了自己、也難受了和你并肩作戰(zhàn)的戰(zhàn)友。
有時(shí)候?yàn)榱?strong>減少重復(fù)開發(fā)的成本,反復(fù)提煉和沉淀有復(fù)用價(jià)值的功能,就需要我們對(duì)業(yè)務(wù)代碼進(jìn)行合理抽象、甚至精雕細(xì)琢,要把業(yè)務(wù)代碼寫得優(yōu)雅并非易事。我一直認(rèn)為,程序設(shè)計(jì)和搬磚的最大區(qū)別在于設(shè)計(jì)二字,寫代碼也是一門藝術(shù)活。今天就借此機(jī)會(huì),站在前端工程師的視角,給大家分享關(guān)于書寫業(yè)務(wù)代碼的最佳實(shí)踐。
0x01 漸進(jìn)式重構(gòu)
漸進(jìn)式重構(gòu)是不斷地對(duì)既有代碼進(jìn)行抽象、分離和組合。做代碼重構(gòu)之前需要回答兩個(gè)問題:
1、什么樣的代碼需要重構(gòu)?
2、何時(shí)進(jìn)行重構(gòu)?
復(fù)制代碼
設(shè)計(jì)不是一蹴而就的,有時(shí)候?qū)懼鴮懼虐l(fā)現(xiàn)某些代碼可以抽離出來單獨(dú)使用,需要重構(gòu)的代碼需要滿足幾個(gè)條件:
1、代碼后期可復(fù)用
2、代碼無副作用
3、代碼邏輯單一
復(fù)制代碼
過早重構(gòu)可能會(huì)因需求變化太快白白浪費(fèi)許多時(shí)間;過晚重構(gòu)會(huì)因?yàn)榇a邏輯復(fù)雜、相似代碼積壓過多導(dǎo)致變更風(fēng)險(xiǎn)太高,難以維護(hù)。漸進(jìn)式重構(gòu)如下圖所示(紅色部分為增加的代碼):

首先我們?cè)谕粋€(gè)源文件中新增功能,發(fā)現(xiàn)部分代碼無副作用且可分離,因此在同一個(gè)文件中進(jìn)行代碼分割,形成許多功能單一的模塊。如此往復(fù)后發(fā)現(xiàn)單文件的體積越來越大,此時(shí)就可以將功能相關(guān)聯(lián)的模塊抽出來放到單獨(dú)的文件中統(tǒng)一管理,如 helpers、components、constants 等等。
0x02 高內(nèi)聚低耦合
高內(nèi)聚低耦合一直是軟件設(shè)計(jì)領(lǐng)域里亙古不變的話題,重構(gòu)的目標(biāo)是提高代碼的內(nèi)聚性,降低各功能間的耦合程度,降低后期維護(hù)成本,特別是寫業(yè)務(wù)代碼,這一點(diǎn)相當(dāng)重要。
舉個(gè)栗子,比如新需求希望在現(xiàn)有的產(chǎn)品頁面上增加發(fā)紅包功能,以吸引用戶開通某個(gè)功能,按照正常邏輯,我需要:
1、在當(dāng)前頁面中引入相關(guān)依賴
2、初始化,查詢紅包相關(guān)信息
3、用戶點(diǎn)擊時(shí),觸發(fā)紅包發(fā)送
復(fù)制代碼
白色部分表示上個(gè)版本的代碼,紅色部分表示完成這個(gè)需求需要變更的代碼:

這樣一來,這個(gè)發(fā)紅包功能就和以前的代碼嚴(yán)重耦合,如果這是個(gè)只需要上線一周的臨時(shí)需求,下線代碼的時(shí)候就是一個(gè)高風(fēng)險(xiǎn)的動(dòng)作;如果上線運(yùn)行期間還需要對(duì)產(chǎn)品頁面進(jìn)行迭代,越往后就越搞不清楚誰是誰了。合理的設(shè)計(jì)應(yīng)該是下面這個(gè)樣子的:

將和產(chǎn)品代碼無關(guān)的功能性代碼拆分出來,放到另一個(gè)文件中內(nèi)部維護(hù)好整個(gè)生命周期狀態(tài),對(duì)外只暴露少量的接口或是方法,這樣一來對(duì)產(chǎn)品頁面的改造只需要:
1、引入紅包組件
2、用戶點(diǎn)擊時(shí),調(diào)用紅包組件的發(fā)獎(jiǎng)方法
復(fù)制代碼
這樣的變更是極小的、明確的、可控的。換句話說,整個(gè)紅包功能是高內(nèi)聚的,與產(chǎn)品代碼是低耦合的。這樣實(shí)踐也帶來另一個(gè)好處:我得到了一個(gè)可復(fù)用的紅包組件!
0x03 合理冗余
業(yè)務(wù)需求是多變的,寫出來的代碼也是如此,頻繁地抽象很可能導(dǎo)致過度設(shè)計(jì),一個(gè)抽象很可能隨著迭代次數(shù)的增多變得十分復(fù)雜。在存在多個(gè)變量的分支業(yè)務(wù)場(chǎng)景,比如同時(shí)包含活動(dòng)是否過期、是否已參加活動(dòng)、是否完成一次任務(wù)這樣的情況,會(huì)存在多個(gè)嵌套 if-else 結(jié)構(gòu),這時(shí)將代碼冗余設(shè)計(jì)是個(gè)不錯(cuò)的選擇。下面舉一個(gè)例子來說明什么是合理冗余:
e.g. 有這樣一個(gè)需求,一開始很簡(jiǎn)單,需要設(shè)計(jì)兩個(gè)運(yùn)營(yíng)展位:

那么抽象一個(gè)組件:
const?Item?=?({?title,?content?})?=>?(
??
????{title}
????{content}
??
);
復(fù)制代碼
現(xiàn)在需求要求在第一個(gè)展位的標(biāo)題上增加熱文標(biāo)記:

也很容易:
const?Item?=?({?title,?content?},?index)?=>?(
??
????{title}{index?===?0?&&?hot}
????{content}
??
);
復(fù)制代碼
需求又變了,要求:在第一個(gè)展位去掉內(nèi)容,并且在下方加個(gè)按鈕;第二個(gè)展位的標(biāo)題右邊增加一個(gè)超鏈接以及增加一個(gè)副標(biāo)題:

這下有點(diǎn)惡心了:
const?Item?=?({?title,?content?},?index)?=>?(
??
????
??????{title}
??????{index?===?0?&&?hot}
??????{index?===?1?&&?"xxx">去看看}
????
????{index?===?1?&&?副標(biāo)題
}
????
??????{index?!==?0?&&?content}
??????{index?===?0?&&?
??
);
復(fù)制代碼
可以看到,之前抽象的好好的,現(xiàn)在需求一變,代碼就面目全非了,中間混雜著兩個(gè)狀態(tài)(第一個(gè)、第二個(gè))的判斷邏輯。實(shí)際情況很可能比這個(gè)更復(fù)雜,在多狀態(tài)交織邏輯難以通過一套代碼表達(dá)清楚時(shí),進(jìn)行合理冗余就是個(gè)不錯(cuò)的選擇,將上面的例子用兩個(gè) if 重寫如下:
//?第一個(gè)展位
if?(index?===?0)?{
??return?(
????
??????標(biāo)題一hot
??????領(lǐng)福利
????
??);
}
//?第二個(gè)展位
if?(index?===?1)?{
??return?(
????
??);
}
復(fù)制代碼
合理冗余其實(shí)也是一種重構(gòu),根據(jù)業(yè)務(wù)邏輯和代碼規(guī)模,做相似抽象還是代碼冗余,這其實(shí)也是漸進(jìn)式重構(gòu)的一種體現(xiàn)。無論采用何種方式,只要能把業(yè)務(wù)邏輯表達(dá)清楚,讓代碼始終保持良好的可讀性和可維護(hù)性,就OK。
下面介紹一個(gè)過度抽象的例子。
0x04 拒絕過度抽象
在 JavaScript 代碼中進(jìn)行深度抽象有時(shí)并非好事,有 OOP(面向?qū)ο缶幊蹋┍尘暗耐瑢W(xué)很容易先入為主設(shè)計(jì):所有數(shù)據(jù)結(jié)構(gòu)都想封裝成一個(gè)類 (Class) 。實(shí)際上 Class 在 JavaScript 中是個(gè)不好的設(shè)計(jì),它并非真正的類。幾年前,我曾看到一位 Java 轉(zhuǎn)前端的同學(xué)寫出了類似這樣的代碼:
class?DataItem?{
??constructor(id,?name,?value)?{
????this.id?=?id;
????this.name?=?name;
????this.value?=?value;
??}
}
class?DataCollection?{
??constructor()?{
????this.items?=?new?Array();
??}
??insert(item)?{
????this.items.push(item);
??}
}
const?item1?=?new?DataItem(1,?'name1',?100);
const?item2?=?new?DataItem(2,?'name2',?200);
const?list?=?new?DataCollection();
list.insert(item1);
list.insert(item2);
...
復(fù)制代碼
一股濃濃的 Java 味道撲面而來。上面的代碼并沒有發(fā)揮出 JavaScript 的語言優(yōu)勢(shì),也增加了不少理解成本,如果用面向?qū)ο缶幊痰乃悸啡懬岸舜a,特別是業(yè)務(wù)代碼,可真是一場(chǎng)噩夢(mèng)。正確的寫法如下:
const?list?=?[{
??id:?1,
??name:?'name1',
??value:?100
},?{
??id:?2,
??name:?'name2',
??value:?200
}];
復(fù)制代碼
由于 JS 屬于弱類型語言,弱類型語言就要發(fā)揮弱類型的優(yōu)勢(shì),無需過多類型定義和 Class 抽象,用最原始的 object 和 function 足以勝任從簡(jiǎn)單到復(fù)雜的業(yè)務(wù)場(chǎng)景。這里特別想提及前端所熟知的 Redux 狀態(tài)管理器,Redux 中,state 就是普通的 object,reducer 就是普通的 function,action 也是普通的 object,不加任何類型約束。因?yàn)楹?jiǎn)單,所以強(qiáng)大。
0x05 眼觀六路
用弱類型語言編程意味著無需編譯,無需編譯的語言天生存在一個(gè)問題是在運(yùn)行前缺少必要的類型檢查,將問題暴露在運(yùn)行時(shí)往往會(huì)導(dǎo)致非常嚴(yán)重的故障。這就要求開發(fā)者能在寫代碼的階段嚴(yán)格保證代碼質(zhì)量,特別是寫業(yè)務(wù)代碼。
集成開發(fā)環(huán)境(IDE)對(duì) JavaScript 代碼的智能提示能力有限,很多時(shí)候不能通過 IDE 查找某個(gè)變量或者函數(shù)的所有引用,這時(shí)就要善用 Ctrl + F 進(jìn)行全局查找來保證自己的單點(diǎn)變更不會(huì)影響到其他地方。如果使用 TypeScript,在類型檢查、引用查找上的幫助會(huì)更好。
0x06 總結(jié)
今天給大家分享了關(guān)于書寫業(yè)務(wù)代碼的一些實(shí)踐經(jīng)驗(yàn):對(duì)代碼進(jìn)行漸進(jìn)式重構(gòu)是提升代碼健壯性的有力武器;設(shè)計(jì)高內(nèi)聚低耦合的代碼可以讓你在做需求的過程中沉淀出一套通用解決方案;合理冗余可以簡(jiǎn)化復(fù)雜的場(chǎng)景,讓開發(fā)變得高效、測(cè)試變得容易;拒絕過度抽象,擁抱簡(jiǎn)單,靈活變化。保持 眼觀六路 的好習(xí)慣能讓代碼質(zhì)量提升一個(gè)臺(tái)階。
最后,希望大家能在實(shí)際開發(fā)過程中去體會(huì)和學(xué)習(xí),不斷思考和總結(jié),將業(yè)務(wù)代碼寫優(yōu)雅,是個(gè)很大的挑戰(zhàn)
