導讀
親嘗百草,方知甘苦。套路,通常有助于提升代碼的可讀性、擴充性和效率。以下是作者工作中總結出來的一部分代碼套路,分享給大家。
武俠小說裡,往往有這樣的場景,一少年被仇家追殺至絕境誤入一山洞,偶然發現石壁上赫然刻着江湖失傳已久的絕世武功,少年有着過目不忘的本領,将石壁上刻的一招一式牢牢記在了心中。還沒等領悟其中奧妙,突然間洞穴開始晃動坍塌,山石崩裂,少年人僥幸逃出。後來隐姓埋名,勤學苦練,終于悟透了這絕世神功。滿月臨弓影,連星入劍端。十年一劍,破空而出,劍意萬裡,直沖霄漢。
在程式設計的世界裡,大概可能也是這樣的一個曆程吧。在代碼實踐之前,讀到教科書裡的設計原則、模式、規範,可能往往不知所雲。這時要做的,便是牢牢記住。紙上得來終覺淺,在之後數年的代碼實踐當中,每個人可能逐漸會形成自己的一個慣用套路,在解決實際問題時極為有效。而這些套路,實際就和那些理論融會貫通了。
親嘗百草,方知甘苦。套路,通常有助于提升代碼的可讀性、擴充性和效率。以下是我喜歡的一部分。
類型定義的套路
資料類型是值集和定義在這個值集上的一組操作的總稱。通常來講,資料類型确定了,代碼的邏輯結構也便随之确定,兩者互相成就,相輔相成。過往的編碼經驗表明,邏輯實作的困難程度和設計品質與資料類型之間存在很大相關性。選擇合适的資料類型,可以簡化邏輯,提升代碼的擴充性和可讀性。這種感覺非常像資料結構和算法之間的關系,雖然後者更傾向于影響計算機硬體的使用率和運作效率。
例子
例如下面這段代碼片段,裡面有多個魔法字元串出現,乍一看讓人頭大。我們知道,枚舉(Enum)是一些常量的集合。當代碼中需要用到一些固定的常量卻又晦澀難懂時,可以考慮使用枚舉定義,來提高代碼的可讀性。
...
if (group === 'customer_free_group') {
list.push({
id: 'customer_free_cid',
Type: 'EQ',
bizType: 'customer_free',
});
} else if (group === 'customer_biz_group') {
list.push({
id: 'customer_biz_cid',
Type: 'AND',
bizType: 'customer_biz'
});
} else if (group === 'contact_group') {
list.push({
id: 'contact_cid',
Type: 'IN',
bizType: 'contact',
});
}
...
使用枚舉類型來定義這裡的常量,類型定義一出來,仿佛代碼邏輯就容易懂了一些:
enum GroupTypes {
customerFreeGroup = 'customer_free_group', // 免費客戶組
customerBizGroup = 'customer_biz_group', // 收費客戶組
contactGroup = 'contact_group', // 聯系人組
}
enum LogicalOperationTypes {
EQ = 'EQ',
IN = 'IN',
AND = 'AND',
GT = 'GT',
LT = 'LT',
}
enum BizTypes {
customer_free = 'customer_free', // 免費客戶
customer_biz = 'customer_biz', // 收費客戶
contact = 'contact', // 聯系人
}
接着将原代碼的魔法字元串改掉,改後的代碼貌似可以透出一些幫助讀者了解的資訊了,可讀性上有些進展。而且因為有了明确的命名和集中管理的地方,也友善後續的擴充:
...
if (group === GroupTypes.customer_free_group) {
list.push({
id: IdTypes.customer_free_cid,
type: LogicalOperationTypes.EQ,
bizType: BizTypes.customer_free,
});
} else if (group === GroupTypes.customer_biz_group) {
list.push({
id: IdTypes.customer_biz_cid,
type: LogicalOperationTypes.AND,
bizType: BizTypes.customer_biz,
});
} else if (group === GroupTypes.contact_group) {
list.push({
id: IdTypes.contact_cid,
type: LogicalOperationTypes.IN,
bizType: BizTypes.contact,
});
}
...
接着,我們發現代碼裡出現了多次相似的代碼結構。相似代碼消除也有很多套路,使用合适的類型定義也是其中的一個。當我們在代碼中覺察到相似的結構時,可能可以動一動映射表(Map)的心思了。先嘗試将相似的代碼結構定義到一個映射表裡:
const groupConfigMap = {
customer_free_group: {
id: IdTypes.customer_free_cid,
type: LogicalOperationTypes.EQ,
bizType: BizTypes.customer_free,
},
customer_biz_group:{
id: IdTypes.customer_biz_cid,
type: LogicalOperationTypes.AND,
bizType: BizTypes.customer_biz,
},
contact_group: {
id: IdTypes.contact_cid,
type: LogicalOperationTypes.IN,
bizType: BizTypes.contact,
},
};
定義好之後,接下來,主代碼中的邏輯就會變成下面這樣:
...
if (group in groupConfigMap) {
list.push(groupConfigMap[group]);
}
...
當group在映射表中時,直接将對應的對象推入list數組。這使得代碼更簡潔且易于擴充。如果未來有新的group類型,隻需添加到映射表即可。主代碼邏輯部分從20行縮減到3行。雖然一目十行要求有些高,但一眼看過去三行應該是沒問題。變身後的代碼一目了然,簡單清晰,讀起來感覺真不錯。
小結
如果條件分支主要基于某個鍵的值,可以考慮使用 映射表來存儲對應的行為,這樣可以将條件判斷轉換為簡單的查找操作。通過使用枚舉和映射表等資料類型将資料重新定義,可以将複雜邏輯簡化,消減備援代碼,使代碼更加簡潔和易于維護。
函數提取的套路
在一個函數中可能出現多次分支和循環的邏輯結構,這可能導緻一個函數過長,進而影響閱讀體驗。如果不加控制,可能将變得難以了解和維護。這時我們可以考慮,将其中在多個地方重複或者負責特定任務的一段代碼封裝成一個新的函數。函數式程式設計強調使用純函數和不可變資料,最常見的是将業務邏輯和資料處理分開,資料處理部分很容易使用函數式程式設計。當然,新函數要符合單一職責原則,每個函數隻做一件事。
例子
這是一個函數中的部分代碼片段:
...
if (bizStatus === 'RUNNING') {
bizName = 'flow_in_approval';
} else if (bizStatus === 'COMPLETED') {
if (bizAction === 'modify') {
bizName = 'flow_modify';
} else if (bizAction === 'revoke') {
bizName = 'flow_revoke';
} else {
bizName = 'flow_completed';
}
} else {
bizName = 'sw_flow_forward';
}
...
這段代碼是通過BizStatus和BizAction的值來判斷設定對應的bizName,邏輯相對獨立,可以将其從幾百行的原函數中抽出來,封裝成一個新的函數getBizName。先用前面資料類型的套路整理一下,使用枚舉來存放常量,并應用到代碼中:
const BizStatus = {
RUNNING: 'RUNNING',
COMPLETED: 'COMPLETED',
};
const BizAction = {
MODIFY: 'modify',
REVOKE: 'revoke',
REFUSE: 'refuse'
};
const getBizName = (bizStatus:BizStatus, bizAction:BizAction)=>{
let bizName = '';
if (bizStatus === BizStatus.RUNNING) {
bizName = 'flow_in_approval';
} else if (bizStatus === BizStatus.COMPLETED) {
if (bizAction === BizAction.MODIFY) {
bizName = 'flow_modify';
} else if (bizAction === BizAction.REVOKE) {
bizName = 'flow_revoke';
} else {
bizName = 'flow_completed';
}
} else {
bizName = 'flow_forward';
}
return bizName;
}
之後,原函數的位置變為了順序結構。順序結構是讓人了解起來最輕松的一種結構了:
...
bizName = getBizName(bizStatus, bizAction);
...
小結
如果一個函數本身過大,其中出現了多次分支結構和循環結構,可以考慮将每個分支結構或循環結構封裝成新的函數。通過函數提取,可以逐漸分解大而複雜的函數,将原函數中主流程盡量保持簡單清晰的順序結構。通過新函數的合理命名,将極大的增強原函數的可讀性和維護性。
但是,看到getBizName這個函數,還會覺得哪裡怪怪的。那就要用到下面的分支邏輯處理的套路了,繼續往下。
分支處理的套路
分支結構是程式設計中的常見的邏輯結構,它可以使程式根據不同的條件執行不同的代碼。然而過度的分支可能導緻代碼難以了解和維護。優化的分支邏輯,可以提高代碼的可讀性、可維護性和性能。
衛語句(Guard Clause)是一種利用條件語句提前退出函數執行的程式設計風格,可以避免深層嵌套,使代碼更扁平并盡早傳回。它常用的兩個場景,一是在函數開頭用來進行參數驗證或條件檢查,不合條件立即退出,避免繼續執行剩餘的代碼。二是在函數的多個條件分支中,傳回對應的值。
例子
繼續使用上面未完的這段代碼,我們将深度嵌套的條件通過邏輯運算合并成一個條件,避免條件的多層嵌套。再使用衛語句,使函數盡早傳回,不需繼續執行後續代碼。這樣代碼更加簡潔,易于了解。擴充功能時,也不需要深入嵌套去了解邏輯,修改起來更容易。
const getBizName = (bizStatus, bizAction) => {
if (bizStatus === BizStatus.RUNNING) {
return 'flow_in_approval';
}
if (bizStatus === BizStatus.COMPLETED && bizAction === BizAction.MODIFY) {
return 'flow_modify';
}
if (bizStatus === BizStatus.COMPLETED && bizAction === BizAction.REVOKE) {
return 'flow_revoke';
}
if (bizStatus === BizStatus.COMPLETED) {
return 'flow_completed';
}
return 'flow_forward';
};
當然,這個例子稍微有點特别。你一定想它也可以使用類型定義的套路來處理試試。而這裡bizStatus為COMPLETED時,也是可以考慮再次函數提取。這取決于要處理的代碼的複雜度。
小結
簡化合并條件表達式、衛語句、政策模式以及前面提到的映射表的方式是時常可以應到到分支邏輯進行中,來提高代碼的可讀性和可維護性的工具。流水不腐,戶樞不蠹。代碼常更新,工具也要常用。
變更封裝的套路
在業務需求中,常常遇到一些需求經常發生調整的場景。例如新人引導步驟、菜單紅點位置、功能性彈窗内容上線後需要多次修改等等。那如何應對類似的需求變更呢?配置化是個很好用的工具。配置化是将程式的配置資訊存儲在外部檔案中的一種方法。通過設計,将配置和邏輯分離,在邏輯中實作配置驅動。這樣,在需求變更時可以輕松地通過修改配置來達成目的,避免繁瑣的代碼修改和線上變更。
例子
例如這樣一個新手任務的清單頁面需求,向符合條件的新手展示任務清單,任務點選後頁面會有多種行為:
考慮到後續任務項可能發生變更,使用配置化來實作。先經過通用化抽象,明确能力要求,例如這個需求可描述為:
1.任務清單的任務不同時期會發生數量和内容的變化。
2.彈窗的内容和按鈕點選後的行為會根據不同的任務發生變化。
3.任務清單頁的按鈕點選行為目前是這三種:打開彈窗、調用接口或JSAPI、打開新頁面。
需求描述清楚後,再使用合适的資料類型定義來描述視圖,将資料進行配置,定義例如:
interface ITaskModel {
key: string;
iconUrl: string;
title: string;
subTitle: IDescriptionModel[];
desc: IDescriptionModel[];
status: ITaskStatusEnum;
action: ITaskActionModel;
utParams: IUTParamsModel;
}
interface ITaskActionModel {
type: ITaskActionComponentType;
name: string;
targetConfig: IActionTargetModel;
}
interface IActionTargetModel {
type: ITaskActionType;
value: string;
targetConfig: IActionTargetModel;
}
最後就是資料驅動的邏輯實作了。
小結
配置化可以看做是更大範圍的類型定義套路的應用。它通常需要先将業務需求通過抽象轉化為通用化需求,再使用合适的資料類型來定義資料,将業務邏輯和資料分離,來實作在需求變更時通過修改配置快速響應的目的。
未完待續
篇幅關系更多的要留待下次更新,便借句話來結尾:輸時不悲,赢時不謙。手中握劍,心中有義。見海遼遠,就心生豪邁。見花盛開,不掩心中喜悅。前路有險,卻不知所畏。有友在旁,就想醉酒高歌。人間道理萬卷書,但求随心随性行。正值亂花漸欲迷人眼的好時節,不如約上三五好友出遊,切莫負了這好時光。
作者:單丹
來源:微信公衆号:阿裡雲開發者
出處:https://mp.weixin.qq.com/s/sl8CcJgJCY_xksmBVE4NhA