工作中新開始的一個項目里,寫代碼前我總會先思考怎麼寫更優雅,邏輯更清晰,然後再去碼,而且寫完後還會審查一遍,把能優化的都盡量優化了。但是後來隨著項目需求越來越多,代碼量蹭蹭地漲,而且很多東西我發現拆成組件寫會更麻煩,即使他們結構類似,於是我就粘貼複製,然後一個個去改。另外一點就是發現 if else越來越多,前期我還不敢用太多,後面就徹底放飛自我了,實在改不動了,改一處就要改多處。

我前端工作經驗一年多,想知道該怎麼避免這種情況。是否是能力差太多,該怎麼彌補?


首先要隨喜下,因為你這種想改變的衝動,將為你帶來一段時間的水平快速提升。

初學一年左右,個人很容易面臨寫程序的能力與對程序結構把控能力不匹配的矛盾:感覺代碼都會寫,但寫多了就感覺到處是問題:不好復用,不好維護。這種對結構不踏實的感覺中,最有代表性,也是最容易被發覺的,便是 if-else的邏輯分支帶來的混亂。

此時建議靜下心來,研究下設計模式,javascript版本的我推薦《JavaScript設計模式(張容銘)》。

另外,平時要保持思考,感覺下代碼的問題在哪,看看能不能想出好辦法應對。這既可以鍛煉思維水平,也有助於加深對一些問題的認知和理解。在這個前提上,去讀別人優秀的代碼和設計模式,才能更加深刻的理解別人「精妙」的一些解決思路。

這將是一個持續時間較長,且充滿樂趣的過程:看著自己的東西一點點變得整潔。


更新:單就簡化if-else而言,個人有一些體會,只說個要點,具體自行查資料:

1 充分利用js中的bool隱性轉換和特殊的與或,如

array array.length dosomthing() ; &<==&> if(array!=null array!=undefined array.length &>0 ){ dosomthing()}

!array || !array.length || dosomething();

array? a() : b();

let i = array.length; while(i--){ dosomething(); } // i減至0時,隱性等於false, 循環中止。

2 用字典代替switch-case(條件比較多,但不太嵌套的情況),如

var map = {1: funcA, 2: funcB, 3:funcC ...};

map[code](args);

3 借鑒簡化的狀態機思路,比較好的分散邏輯分支,讓代碼清晰;

4 鏈式組裝邏輯,這個可以適用於嵌套深步驟多的了(這個是我自己曾經寫的,不是標準設計模式)。曾經幫小夥伴重構過一個邏輯分支圖的代碼,超過20多個環節,原代碼(C#的)是無數的if-else的嵌套,代碼的維護和測試都非常困難。後來我封裝了一個結構,大致如下(用TS語法描述):

interface ILogicUnit{ // 邏輯單元
invoke(args:any): any; // 根據參數進行判斷,觸發下面true和false分支的方法
trueBranch: ILogicUnit; // 如果invoke中判定為true, 則調用 trueBranch.invoke(上一步invoke的結果)
falseBranch: ILogicUnit;
context:any ; // 邏輯嵌套深之後,傳參會很麻煩,可以通過context來解決這個問題
}

interface ILogicLinker{ // 邏輯單元的組裝器
context:any ; //用來在多個邏輯單元中共享變數;
entryUnit: ILogicUnit; // 入口邏輯單元
}

解釋:

a 我們將每一個if-else分支的判斷邏輯,封裝在一個邏輯單元裡面。邏輯單元的命名與邏輯圖上一致(可讀性增強)。

b 每個邏輯單元有兩個出口。構建完所有的邏輯單元後,將所有的邏輯單元根據邏輯圖上的規則,拼接好兩個下游出口端。拼接的過程中賦值好context,這一步在ILoigcLinker的實現中進行,LogicLinker的類名對應邏輯圖的名字。

c 入口邏輯單元.invoke(入口參數)後,接著 (trueBranch or falseBranch).invoke(上一步的返回結果) ... 依次進行鏈式反應,直至程序結束。(末端的邏輯單元沒有分支邏輯,invoke完會就停止了)

說明:

a 這樣做用代碼的調用避免了if-else的嵌套深度;

b 代碼清晰外,主要是好測試。之前所有邏輯分支在一個大if-else中,製造測試條件極其麻煩。一個出問題全部重測。修改分支順序更是要命。現在只要對每個邏輯分支單獨測試就行了,邏輯順序調整也只要改變裝配順序就行了。

5 根據邏輯的複雜度合理應對,現實中經常出來殺雞用牛刀的情況。過度解決問題也是一種浪費。


更新:由於有些人不太能跟得上第4部分的思路,我做一個簡化版的示例,圖和代碼如下:

// 邏輯單元介面
interface ILogicUnit {
// 根據參數進行判斷,觸發下面true和false分支的方法
invoke(args: any): any;

// 如果invoke中判定為true, 則調用 trueBranch.invoke(上一步invoke的結果)
trueBranch: ILogicUnit;

falseBranch: ILogicUnit;

// 邏輯嵌套深之後,傳參會很麻煩,可以通過context來解決這個問題
context: any;
}

// 邏輯單元的組裝器介面
interface ILogicLinker {
//用來在多個邏輯單元中共享變數;
context: any;

// 入口邏輯單元
entryUnit: ILogicUnit;
}

abstract class LogicUnit implements ILogicUnit{
trueBranch: ILogicUnit;
falseBranch: ILogicUnit;
context: any;

invoke(args: any) {
this.check(args) ?
this.trueBranch this.trueBranch.invoke(this.process(true,args))
: this.falseBranch this.falseBranch.invoke(this.process(false,args))
}

//判斷邏輯條件是否為true
check(args:any):boolean{return true;}

//為下一步判斷提供參數輸入
process(checkResult:boolean, args:any):any{return null;}

setBraches(trueBranch:ILogicUnit, falseBranch:ILogicUnit){
this.trueBranch = trueBranch;
this.falseBranch = falseBranch;

this.trueBrach.context = this.context;
this.falseBranch.context = this.context;
}
}

//是否是會員
class IsMemberLogic extends LogicUnit{
check(args: any): boolean {
// 調用後台介面,判斷當前用戶是否為會員。
return true;
}

process(checkResult: boolean, args: any) {
return checkResult this.getMemberInfo();
}

getMemberInfo(){
//todo
}
}

// 導航到註冊頁面
class GotoMemberRegisterPage extends LogicUnit{
invoke(args:any){
// 導航到註冊頁面的代碼
}
}

// 賬號是否凍結的邏輯
class IsMemberLockedLogic extends LogicUnit{

}

// A Logic
class ALogic extends LogicUnit{

}

// B Logic
class BLogic extends LogicUnit{

}

// 會員購物邏輯
class MemberShoppingLogics implements ILogicLinker{
isMemeberLogic: ILogicUnit = new IsMemberLogic();
gotoMemberRegisterPage : ILogicUnit = new GotoMemberRegisterPage ();
isMemberLockedLogic: ILogicUnit= new IsMemberLockedLogic();
aLogic: ILogicUnit = new ALogic();
bLogic: ILogicUnit = new BLogic();

context: any;
entryUnit = this.isMemeberLogic;

constructor(context:any) {
//拼裝邏輯單元
this.isMemeberLogic.setBraches(this.isMemberLockedLogic, this.gotoMemberRegisterPage );
this.isMemberLockedLogic.setBraches(this.aLogic,this.bLogic);

this.context
= this.entryUnit.context
= context; // eg: {user: GlobalServices.userService.getUser()}
}
}

//調用
new MemeberShoppingLogics({...}).entryUnit.invoke();

說明:

  • 之所以用typescript,是為了更好的展示程序的結構性特點(面向對象),對於使用強類型語言的,很容易翻譯成對應的。如果翻譯成js,把介面和類型約束去掉就可以了;
  • 在前端,有些判斷處理邏輯是非同步的,實戰可能要支持非同步寫法,使用await等。
  • 請在腦中感受和模擬下數據的流動和轉化的方向性。如果isMemberLogic判定是true, 就會觸發它的true邏輯分支,也就是isMemberLockedLogic。該邏輯觸發後,又會進一步觸發a或b邏輯……不論邏輯分支圖如何長,用這種小鏈條一樣的單元拼接起來後,就會直接執行到底。
  • 在每個logic的invoke中,用控制台記下"邏輯名","判定結果", "傳到下一步的參數", 你很容易追蹤邏輯圖中的執行情況和數據流向,很方便複雜邏輯嵌套時的調試。
  • 如果讓你寫,會寫成怎樣呢?是不是有2重以上的if-else。。。如果邏輯分支再深一些呢?


拆成組件寫會更麻煩,說明拆分的時候,只考慮了代碼本身書寫的分散,沒有考慮邏輯的低耦合

保持代碼足夠simple最大的難點其實是作出正確的抽象。而程序員的成長總是伴隨著不斷地發現和修正錯誤的抽象

這才是題主下一步思考的應該去往的方向。

而作出正確的抽象又需要兩大能力:

1.對事物進行定性分析的能力

2.對業務流程/工作流的熟悉程度

這兩者都需要大量的經驗來提升。

如何找到正確的抽象是程序員要思考一輩子的問題。

圍繞這個問題其實產生了很多基本的編程原則,比如著名的OOP六大編程原則。

當然它主要是根據基於類繼承的OOP總結出來的。

但是裡面也不乏在其它任何需要「對象」的編程範式中也都適用的原則,比如

迪米特法則(Law Of Demeter),又稱 最少知識原則(Least Knowledge Principle)

單一職責原則(Single Responsibility Principle, SRP)

以及一些在實踐中總結出來的原則,如:

通過SST(Single Source of Truth)原則,避免冗餘/衝突的數據來源,實現DRY(Dont repeat yourself)可以避免對copy/past的代碼進行重構時漏掉一些功能。

再比如人們在對一些編程範式的思考中也得到了一些經驗:

聲明式 的可讀性和可維護性大於 命令式,而命令式則在性能和硬體資源的節約上佔優。

純函數和副作用相互隔離,形成明確的分界,更便於測試(redux,redux-saga)。

等等。

總的來說。。

學習大佬的經驗,再根據這些原則和大佬的經驗去優化代碼積累經驗,就可以變厲害啦!~


什麼組件化,模塊化,架構啊,模式啊。我只想說qtmlgb的吧。分分鐘被產品經理手持需求文檔指著鼻子說「不是針對誰,在座的都是垃圾」。

想解決代碼臃腫先解決需求臃腫。但這在現實世界是不可能的。

解決代碼的內部臃腫只有一個方式:刪。

當然能不能愉快的刪取決於代碼是否disposable。寫disposable code跟什麼組件模塊架構無關甚至相悖。

那怎麼寫disposable code?不好意思我也不知道,只能提供個思路:寫下任何一個功能之前,以第三者能否愉快的刪掉它作為質量評價標準之一。

試想:你馬上離職,對接手的同事說,某某文件夾下面東西想要就要不想要直接刪0副作用。不管你留下幾萬還是幾千萬行代碼,誰敢說你臃腫?身材除外。


感覺題主出發點是好的,思考前會思考如何寫更優化,更清晰,維護性更高,這其實已經很好了,如果我有這樣的同事的話,那真得是太幸運了。

言歸正傳,但是光有這樣好的出發點是不夠的,根據描述,你到最後還是陷入了 cv 程序員的怪圈。據我臆測,我覺的你在寫代碼時認為「優雅」、「清晰」的代碼,可能僅僅是停留在代碼層面了,而不是架構層面。因為我也經歷過程序員小白的階段,當時也會一味追求代碼層面所謂的「優雅」和「簡約」(一些奇奇怪怪的語法),到頭來坑還是給自己填。

所以,寫代碼,一定要多投入精力放到設計這個事情上來,先設計,再寫。尤其是前端開發現在比較流行的組件化開發模式,用組件來抽象頁面,最考驗對於業務狀態的抽象和設計能力。所謂架構啊,代碼擴展性,靈活性,都是在設計這個時間段決定的。我認為新手常見的誤區是這樣的,就是一個需求拿來就寫,之後有了變更就改,然後會在中間做一些所謂的「優化」,然後重複這個過程,直到有一天來知乎提出了類似題主所問的這個問題。

對於前端開發中,如何對組件進行設計,要談這個話題我覺得一時半會根本講不完,所以就不說了。針對你反饋的問題,我倒是可以給你一些建議和心得,僅供參考:

  • 代碼量暴增最可能的原因是冗餘代碼或者模板代碼造成的,嘗試使用經典的設計模式來提升代碼的復用性和內聚性,降低耦合性,設計模式這個東西對於前端來講,經常能用到的其實就那麼幾個,比如發布訂閱、代理、策略,嘗試用這幾個模式去思考一些當前代碼是不是能套用一些?
  • if...else 暴增的原因一般都是由於複雜的業務變更造成的,這個大家都懂,我當前項目的解決方案,是通過函數式的 composable 特性來解決的,具體你可以搜集資料了解一下,大體思路就是簡單邏輯組成複雜邏輯,複雜邏輯組成超級複雜的邏輯,就和玩樂高一樣。使用函數式來抽象業務有以下幾個好處:
    • 封裝的邏輯大部分都是純函數,能夠比較容易的針對業務代碼編寫單元測試
    • 由於 if...else 更少,循環更少,中間變數更少,代碼可讀性因此會更高
    • 純函數可以很方便的進行復用,而不必在意外在依賴
  • 切實地實行測試驅動開發,很多人都對編寫單元測試不屑一顧,有的會說沒時間,有的會說沒必要,有的嫌麻煩,反正各有個的理由,但我覺的這個真的是需要落實的,尤其做業務開發,如果沒有單元測試,改 bug 到後期基本都在改 regression 了
  • 嘗試遵守 SOLID 原則,這個東西吧,比較玄學,一般不太可能生搬硬套,只能融會貫通,我的感受是,這些原則每過一段時間,重新思考一遍,都會有不同的看法和心得,對於前端開發,我覺的比較重要的要算依賴倒置、單一職責和開閉這三個原則,對於另外兩個,由於現在前端開發還是比較青睞使用 composable 的函數式思想來進行業務抽象,因此可能不太重要

我之前曾經翻譯過 SOLID 開發原則的相關文章,有興趣可以看一看,我覺的會有一定的幫助的。這些模式對於前端開發,可能不能」照搬「,但是可以借鑒其中的所蘊含的精髓。

目錄如下:

讀懂 SOLID 的「單一職責」原則?

segmentfault.com

讀懂 SOLID 的「開閉」原則?

segmentfault.com

讀懂 SOLID 的「里氏替換」原則?

segmentfault.com

讀懂 SOLID 的「介面隔離」原則?

segmentfault.com

讀懂 SOLID 的「依賴倒置」原則?

segmentfault.com

祝工作順利。


if/else 很多就是典型的多態問題,設計上缺乏多態就會導致你被迫用代碼自行實現多態邏輯,體現在代碼中就是大量的 if/else。


推薦閱讀:
相关文章