精益求精!記一次業(yè)務(wù)代碼的優(yōu)化探索
關(guān)鍵詞:需求實(shí)現(xiàn)、設(shè)計(jì)模式、策略模式、程序員成長(zhǎng)
本篇文章由淘系新人喜橙同學(xué)撰寫。
承啟:
本篇從業(yè)務(wù)場(chǎng)景出發(fā),介紹了面對(duì)一個(gè)復(fù)雜需求,拆解重難點(diǎn)、編碼實(shí)現(xiàn)需求、優(yōu)化代碼、思考個(gè)人成長(zhǎng)的過程。
會(huì)介紹一個(gè)運(yùn)用策略模式的實(shí)戰(zhàn)。 需求和編碼本身小于打怪升級(jí)成長(zhǎng)路徑。 文中代碼為偽代碼。
場(chǎng)景說明:
需求描述:手淘內(nèi)“充值中心”要投放在餓了么、淘寶極速版、UC瀏覽器等集團(tuán)二方APP。拿到需求之后,來梳理下“充值中心”在他端投放涉及到的核心功能點(diǎn):
通訊錄讀取 不同客戶端、操作系統(tǒng),JSbridge API實(shí)現(xiàn)略有不同。 支付 不同端支付JSbridge調(diào)用方式不同。 賬號(hào)體系:集團(tuán)內(nèi)不同端賬號(hào)體系可能不同,需要打通。 容器兼容 手淘內(nèi)采用PHA容器,淘寶極簡(jiǎn)版本投放H5,餓了么以手淘小程序的方式投放。環(huán)境變量、通信方式等需要兼容。 各端個(gè)性化訴求 極速版投放極簡(jiǎn)鏈路,只保留核心模塊等。
解決方案
需求明確了:充值相關(guān)核心模塊,需要兼容每個(gè)APP,本質(zhì)是提供一個(gè)多端投放的解決方案。那么這個(gè)場(chǎng)景如何編碼實(shí)現(xiàn)呢?
1、方案一
首先第一個(gè)想法??,在每個(gè)功能點(diǎn)模塊用if-else判斷客戶端環(huán)境,編寫此端邏輯。下面以獲取通訊錄列表功能為例,代碼如下:
// 業(yè)務(wù)代碼文件 index.js
/**
* 獲取通訊錄列表
* @param clientName 端名稱
*/
const getContactsList = (clientName) => {
if (clientName === 'eleme') {
getContactsListEleme()
} else if (clientName === 'taobao') {
getContactsListTaobao()
} else if (clientName === 'tianmao') {
getContactsListTianmao()
} else if (clientName === 'zhifubao') {
getContactsListZhifubao()
} else {
// 其他端
}
}
寫完之后,review一下代碼,思考一下這樣編碼的利弊。
利:邏輯清晰,可快速實(shí)現(xiàn)。
弊:代碼不美觀、可讀性略差,每兼容一個(gè)端都要在業(yè)務(wù)邏輯處改動(dòng),改一端測(cè)多端。
這時(shí),有的同學(xué)就說了:“把if-else改成switch-case的寫法,把獲取通訊錄模塊抽象成獨(dú)立的sdk封裝,用戶在業(yè)務(wù)層統(tǒng)一調(diào)用”,天才!動(dòng)手實(shí)現(xiàn)一下。
2、方案二
核心功能模塊,抽象成獨(dú)立的sdk,模塊內(nèi)部對(duì)不同的端進(jìn)行兼容,業(yè)務(wù)邏輯里統(tǒng)一方式調(diào)用。
/**
* 獲取通訊錄列表 sdk caontact.js
* @param clientName 端名稱
* @param successCallback 成功回調(diào)
* @param failCallback 失敗回調(diào)
*/
export default function (clientName, successCallback, failCallback) {
switch (clientName) {
case 'eleme':
getContactsListEleme()
break
case 'taobao':
getContactsListTaobao()
break
case 'zhifubao':
getContactsListTianmao()
break
case 'tianmao':
getContactsListZhifubao()
break
default:
// 省略
break
}
}
// 業(yè)務(wù)調(diào)用 index.js
<Contacts onIconClick={handleContactsClick} />
import getContactsList from 'Contacts'
import { clientName } from 'env'
const handleContactsClick = () => {
getContactsList(
clientName,
({ arr }) => {
this.setState({
contactsList: arr
})
},
() => {
alert('獲取通訊錄失敗')
}
)
}
慣例,review一下代碼:
利:模塊分工明確,業(yè)務(wù)層統(tǒng)一調(diào)用,代碼可讀性較高。
弊:多端沒有解藕,每次迭代,需要各個(gè)端回歸。
上面的實(shí)現(xiàn),看起來代碼可讀性提高了不少,是一個(gè)不錯(cuò)的設(shè)計(jì),可是這樣是最優(yōu)的設(shè)計(jì)嗎?
3、方案三
熟悉設(shè)計(jì)模式的同學(xué),這時(shí)候可能要說了,用策略模式啊,對(duì)了,這個(gè)場(chǎng)景可以用策略模式。這里簡(jiǎn)單解釋一下策略模式:策略模式,英文全稱是 Strategy Design Pattern。在 GoF 的《設(shè)計(jì)模式》一書中,它是這樣定義的:
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
翻譯成中文就是:定義一族算法類,將每個(gè)算法分別封裝起來,讓它們可以互相替換。策略模式可以使算法的變化獨(dú)立于使用它們的客戶端(這里的客戶端代指使用算法的代碼)。
難免有些晦澀,什么意思呢?我個(gè)人的理解為:策略模式用來解耦策略的定義、創(chuàng)建、使用。它典型的應(yīng)用場(chǎng)景就是:避免冗長(zhǎng)的if-else或switch分支判斷編碼。
下面看代碼實(shí)現(xiàn):
/**
* 策略定義
*/
const strategies = {
eleme: () => {
getContactsListEleme()
},
taobao: () => {
getContactsListTaobao()
},
tianmao: () => {
// 省略
}
}
/**
* 策略創(chuàng)建
*/
const getContactsStrategy = (clientName) => {
if (!clientName) {
throw new Error('clientName is empty.')
}
return strategies[clientName]
}
/**
* 策略使用
*/
import { clientName } from 'env'
getContactsStrategy(clientName)()
策略模式的運(yùn)用,把策略的定義、創(chuàng)建、使用解耦,符合設(shè)計(jì)原則中的迪米特法則(LOD),實(shí)現(xiàn)“高內(nèi)聚、松耦合”。當(dāng)需要新增一個(gè)適配端時(shí),我們只需要修改策略定義Map,其他代碼都不需要修改,這樣就將代碼改動(dòng)最小化、集中化了。
能做到這里,相信你已經(jīng)超越了一部分同學(xué)了,但是我們還要思考、精益求精,如何更優(yōu)呢?這個(gè)時(shí)候單從編碼層面思考已經(jīng)受阻塞了,可否從工程構(gòu)建角度、性能優(yōu)化角度、項(xiàng)目迭代流程角度、后期代碼維護(hù)角度思考一下,相信你會(huì)有更好的想法。
下面拋磚,聊聊我自己的思考:
4、方案四
從工程構(gòu)建和性能優(yōu)化角度出發(fā):如果每個(gè)端獨(dú)立一個(gè)文件,構(gòu)建的時(shí)候shake掉其他端chunk,這樣bundle可以變更小,網(wǎng)絡(luò)請(qǐng)求也變更快。
等等... Tree-Shaking是基于ES靜態(tài)分析,我們的策略判斷,基于運(yùn)行時(shí),好像可能沒什么用啊。
方案三使用策略模式來編碼,本質(zhì)是策略定義、創(chuàng)建和使用解藕,那可否使用剛才的想法,把每端各個(gè)功能模塊兼容方法聚合成獨(dú)立module,從更高維度,將多端業(yè)務(wù)策略定義、創(chuàng)建和使用解藕?
思考一下這樣做的收益是什么?
因?yàn)槊總€(gè)端的適配,聚合在一個(gè)module,將多端業(yè)務(wù)策略解藕,某個(gè)端策略變更,只需要修改此端module,代碼改動(dòng)較小,且后續(xù)測(cè)試鏈路,不需要重復(fù)回歸其他端。符合“高內(nèi)聚、松耦合”。
代碼實(shí)現(xiàn):
/**
* 餓了么端策略定義module
*/
export const elmcStrategies = {
contacts: () => {
getContactsListEleme()
},
pay: () => {
payEleme()
},
// 其他功能略
}
/**
* 手淘端策略定義module
*/
export const tbStrategies = {
contacts: () => {
getContactsListTaobao()
},
pay: () => {
payTaobao()
},
// 其他功能略
};
// ...... (其他端略)
/**
* 策略創(chuàng)建 index.js
*/
import tbStrategies from './tbStrategies'
import elmcStrategies from './elmcStrategies'
export const getClientStrategy = (clientName) => {
const strategies = {
elmc: elmcStrategies,
tb: tbStrategies
// ...
}
if (!clientName) {
throw new Error('clientName is empty.')
}
return strategies[clientName]
};
/**
* 策略使用 pay
*/
import { clientName } from 'env'
getClientStrategy(clientName).pay()
代碼目錄如下圖所示:

index.js是多端策略的入口,其他文件為各端策略實(shí)現(xiàn)。
從方案四的推導(dǎo)來看,有時(shí)候,判斷不一定是對(duì)的,但是從多個(gè)維度去思考,會(huì)打開思路,這時(shí),更優(yōu)方案往往就找上門來了~
5、方案五
既要解決眼前痛點(diǎn),也要長(zhǎng)遠(yuǎn)謀劃,基于以上四種方案,再深入思考一步,如果業(yè)務(wù)有投放在第三方(非集團(tuán)APP)的需求,比如投放在商家APP,且商家APP獲取通訊錄、支付邏輯等復(fù)雜多變,這個(gè)時(shí)候如何設(shè)計(jì)編碼呢?例如:拉起別端的喚端策略,受多方因素影響,涉及到產(chǎn)品壁壘,策略攻防,怎樣控制代碼改動(dòng)次數(shù),及時(shí)提高喚端率呢?在這里簡(jiǎn)單拋磚,可以借助近幾年很火的serverless,搭建喚端策略的faas函數(shù),動(dòng)態(tài)獲取最優(yōu)喚端策略,是不是一個(gè)好的方案呢?
沉淀&思考
以上針對(duì)多端兼容的問題,我們學(xué)習(xí)并運(yùn)用了設(shè)計(jì)模式——策略模式。那么我們?cè)賮砜纯床呗阅J降脑O(shè)計(jì)思想是什么:一提到策略模式,有人就覺得,它的作用是避免 if-else 分支判斷邏輯。實(shí)際上,這種認(rèn)識(shí)是很片面的。策略模式主要的作用還是解耦策略的定義、創(chuàng)建和使用,控制代碼的復(fù)雜度,讓每個(gè)部分都不至于過于復(fù)雜、代碼量過多。除此之外,對(duì)于復(fù)雜代碼來說,策略模式還能讓其滿足開閉原則,添加新策略的時(shí)候,最小化、集中化代碼改動(dòng),減少引入 bug 的風(fēng)險(xiǎn)。實(shí)際上,設(shè)計(jì)原則和思想比設(shè)計(jì)模式更加普適和重要。掌握了代碼的設(shè)計(jì)原則和思想,我們能更清楚的了解,為什么要用某種設(shè)計(jì)模式,就能更恰到好處地應(yīng)用設(shè)計(jì)模式。還有一點(diǎn)需要注意,在代碼設(shè)計(jì)時(shí),應(yīng)該了解他的業(yè)務(wù)價(jià)值和復(fù)雜度,避免過度設(shè)計(jì),如果一個(gè)if-else可以解決的問題,何必大費(fèi)周折,闊談設(shè)計(jì)模式呢?
總結(jié)
理一下全文的核心路徑,也是我此篇文章想要主要傳達(dá)的打怪升級(jí)成長(zhǎng)路徑。
接到一個(gè)復(fù)雜的需求--> 理清需求 --> 拆解技術(shù)難點(diǎn) --> 編碼實(shí)現(xiàn) --> 代碼優(yōu)化 --> 設(shè)計(jì)模式和設(shè)計(jì)原則學(xué)習(xí) --> 舉一反三 --> 記錄沉淀。
當(dāng)下,前端工程師在工作中,難免會(huì)陷入業(yè)務(wù)漩渦中,被業(yè)務(wù)推著走。面對(duì)這種風(fēng)險(xiǎn),我們要思考如何在保障完成業(yè)務(wù)迭代的基礎(chǔ)上,運(yùn)用適合的技術(shù)架構(gòu),抽象出通用解決方案,沉淀落地。這樣,既能幫助業(yè)務(wù)更快更穩(wěn)定增長(zhǎng),又能在這個(gè)過程中收獲個(gè)人成長(zhǎng)。
