[淺析]特定場景下取代if-else和switch的方案

守候i發表於2018-07-16

世界那麼大,景點那麼多。有些時候,換個方式,換個角度,換個陪同,都會有不一樣感覺與收穫。寫程式碼也亦如此。

1.前言

相信很多人有這樣的經歷,在專案比較忙的時候,都是先考慮實現,用當時以為最好的方式先實現方案,在專案不忙的時候,再看下以前程式碼,想下有什麼更好的實現方案,或者優化方案。筆者也不例外,下面就和讀者們分享一下自己最近在特定場合下,代替if-else,switch的解決方案。如果大家有什麼想法,歡迎在評論區內留言,大家多多交流。

2.look-up表代替if-else

比如大家可能會遇到類似下面的需求:比如某平臺的信用分數評級,超過700-950,就是信用極好,650-700信用優秀,600-650信用良好,550-600信用中等,350-550信用較差。

實現很簡單

function showGrace(grace) {
    let _level='';
    if(grace>=700){
        _level='信用極好'
    }
    else if(grace>=650){
        _level='信用優秀'
    }
    else if(grace>=600){
        _level='信用良好'
    }
    else if(grace>=550){
        _level='信用中等'
    }
    else{
        _level='信用較差'
    }
    return _level;
}
複製程式碼

[淺析]特定場景下取代if-else和switch的方案

執行也沒問題,但是問題也是有

1.萬一以後需求,改了比如650-750是信用優秀,750-950是信用極好。這樣就整個方法要改。

2.方法存在各種神仙數字:700,650,600,550。日後的維護可能存在問題。

3.if-else太多,看著有點強迫症

所以下面用look-up表,把配資料置和業務邏輯分離的方式實現下

function showGrace(grace) {
    let graceForLevel=[700,650,600,550];
    let levelText=['信用極好','信用優秀','信用良好','信用中等','信用較差'];
    for(let i=0;i<graceForLevel.length;i++){
        if(grace>=graceForLevel[i]){
            return levelText[i];
        }
    }
    //如果不存在,那麼就是分數很低,返回最後一個
    return levelText[levelText.length-1];
}
複製程式碼

這樣的修改,優點就是如果有需求修改,只需要修改graceForLevel,levelText。業務邏輯不需要改。

為什麼這裡推薦配資料置和業務邏輯分離

1.修改配置資料比業務邏輯修改成本更小,風險更低

2.配置資料來源和修改都可以很靈活

3.薦配置和業務邏輯分離,可以更快的找到需要修改的程式碼

如果還想靈活一些,可以封裝一個稍微通用一點的look-up函式。

function showGrace(grace,level,levelForGrace) {
    for(let i=0;i<level.length;i++){
        if(grace>=level[i]){
            return levelForGrace[i];
        }
    }
    //如果不存在,那麼就是分數很低,返回最後一個
    return levelForGrace[levelForGrace.length-1];
}
let graceForLevel=[700,650,600,550];
let levelText=['信用極好','信用優秀','信用良好','信用中等','信用較差'];
複製程式碼

[淺析]特定場景下取代if-else和switch的方案

使用推薦配置資料和業務邏輯分離形式開發,還有一個好處,在上面例子沒體現出來,下面簡單說下。比如輸入一個景點,給出景點所在的城市。

function getCityForScenic(scenic) {
    let _city=''
    if(scenic==='廣州塔'){
        _city='廣州'
    }
    else if(scenic==='西湖'){
        _city='杭州'
    }
    return _city;
}
複製程式碼

輸入廣州塔,就返回廣州。輸入西湖就返回杭州。但是一個城市不止一個景點,那麼有人習慣這樣寫。

if(scenic==='廣州塔'||scenic==='花城廣場'||scenic==='白雲山'){
    _city='廣州'
}
複製程式碼

如果景點很多,資料很長,看著難受,有些人喜歡這樣寫

let scenicOfHangZhou=['西湖','湘湖','砂之船生活廣場','京杭大運河','南宋御街']
if(~scenicOfHangZhou.indexOf(scenic)){
    _city='杭州'
}
複製程式碼

[淺析]特定場景下取代if-else和switch的方案

這樣執行沒錯,但是寫出來的程式碼可能像下面這樣,風格不統一

function getCityForScenic(scenic) {
    let _city='';
    let scenicOfHangZhou=['西湖','湘湖','砂之船生活廣場','京杭大運河','南宋御街'];
    if(scenic==='廣州塔'||scenic==='花城廣場'||scenic==='白雲山'){
        _city='廣州'
    }
    else if(~scenicOfHangZhou.indexOf(scenic)){
        _city='杭州'
    }
    return _city;
}
複製程式碼

即使用switch,也有可能出現這樣的情況

function getCityForScenic(scenic) {
    let _city='';
    let scenicOfHangZhou=['西湖','湘湖','砂之船生活廣場','京杭大運河','南宋御街'];
	switch(true){
		case (scenic==='廣州塔'||scenic==='花城廣場'||scenic==='白雲山'):_city='廣州';break;
        case (!!~scenicOfHangZhou.indexOf(scenic)):return '杭州';   
	}
	return 	_city;
}
複製程式碼

雖然上面的程式碼出現的概率很小,但畢竟會出現。這樣的程式碼可能會造成日後維看得眼花繚亂。如果使用了配置資料和業務邏輯分離,那就可以避免這個問題。

function getCityForScenic(scenic) {
    let cityConfig={
        '廣州塔':'廣州',
        '花城廣場':'廣州',
        '白雲山':'廣州',
        '西湖':'杭州',
        '湘湖':'杭州',
        '京杭大運河':'杭州',
        '砂之船生活廣場':'杭州',
        '南宋御街':'杭州',
    }
    
    return cityConfig[scenic];
}
複製程式碼

有些人不習慣物件的 key 名是中文。也可以靈活處理

function getCityForScenic(scenic) {
    let cityConfig=[
        {
            scenic:'廣州塔',
            city:'廣州'
        },
        {
            scenic:'花城廣場',
            city:'廣州'
        },
        {
            scenic:'白雲山',
            city:'廣州'
        },
        {
            scenic:'西湖',
            city:'杭州'
        },
        {
            scenic:'湘湖',
            city:'杭州'
        },
        {
            scenic:'京杭大運河',
            city:'杭州'
        },
        {
            scenic:'砂之船生活廣場',
            city:'杭州'
        }
    ]
    for(let i=0;i<cityConfig.length;i++){
        if(cityConfig[i].scenic===scenic){
            return cityConfig[i].city
        }
    }
}
複製程式碼

這樣一來,如果以後要加什麼景點,對應什麼城市,只能修改上面的cityConfig,業務邏輯不需要改,也不能改。程式碼風格上面就做到了統一。

這裡簡單總結下,使用配置資料和業務邏輯分離的形式,好處

  1. 修改配置資料比業務邏輯修改成本更小,風險更低
  2. 配置資料來源和修改都可以很靈活
  3. 配置和業務邏輯分離,可以更快的找到需要修改的程式碼
  4. 配置資料和業務邏輯可以讓程式碼風格統一

但是並不是所有的if-else都建議這樣改造,有些需求不建議使用look-up改造。比如if-else不是很多,if判斷的邏輯不統一的使用,還是建議使用if-else方式實現。但是神仙數字,要清除。

比如下面這個根絕傳入時間戳,顯示評論時間顯示的需求,

釋出1小時以內的評論:x分鐘前

釋出1小時~24小時的評論:x小時前

釋出24小時~30天的評論:x天前

釋出30天以上的評論:月/日

去年釋出並且超過30天的評論:年/月/日

實現不難,幾個if-else就行了

function formatDate(timeStr){
    //獲取當前時間戳
    let _now=+new Date();
    //求與當前的時間差
    let se=_now-timeStr;
    let _text='';
    //去年
    if(new Date(timeStr).getFullYear()!==new Date().getFullYear()&&se>2592000000){
      _text=new Date(timeStr).getFullYear()+'年'+(new Date(timeStr).getMonth()+1)+'月'+new Date(timeStr).getDate()+'日';
    }
    //30天以上
    else if(se>2592000000){
      _text=(new Date(timeStr).getMonth()+1)+'月'+new Date(timeStr).getDate()+'日';
    }
    //一天以上
    else if(se>86400000){
      _text=Math.floor(se/86400000)+'天前';
    }
    //一個小時以上
    else if(se>3600000){
      _text=Math.floor(se/3600000)+'小時前';
    }
    //一個小時以內
    else{
      //如果小於1分鐘,就顯示1分鐘前
      if(se<60000){se=60000}
      _text=Math.floor(se/60000)+'分鐘前';
    }
    return _text;
}

複製程式碼

[淺析]特定場景下取代if-else和switch的方案

執行結果沒問題,但是也存在一個問題,就是這個需求有神仙數字:2592000000,86400000,3600000,60000。對於後面維護而言,一開始可能並不知道這個數字是什麼東西。

所以下面就消滅神仙數字,常量化

function formatDate(timeStr){
    //獲取當前時間戳
    let _now=+new Date();
    //求與當前的時間差
    let se=_now-timeStr;
    const DATE_LEVEL={
      month:2592000000,
      day:86400000,
      hour:3600000,
      minter:60000,
    }
    let _text='';
    //去年
    if(new Date(timeStr).getFullYear()!==new Date().getFullYear()&&se>DATE_LEVEL.month){
      _text=new Date(timeStr).getFullYear()+'年'+(new Date(timeStr).getMonth()+1)+'月'+new Date(timeStr).getDate()+'日';
    }
    //一個月以上
    else if(se>DATE_LEVEL.month){
      _text=(new Date(timeStr).getMonth()+1)+'月'+new Date(timeStr).getDate()+'日';
    }
    //一天以上
    else if(se>DATE_LEVEL.day){
      _text=Math.floor(se/DATE_LEVEL.day)+'天前';
    }
    //一個小時以上
    else if(se>DATE_LEVEL.hour){
      _text=Math.floor(se/DATE_LEVEL.hour)+'小時前';
    }
    //一個小時以內
    else{
      //如果小於1分鐘,就顯示1分鐘前
      if(se<DATE_LEVEL.minter){se=DATE_LEVEL.minter}
      _text=Math.floor(se/DATE_LEVEL.minter)+'分鐘前';
    }
    return _text;
}
複製程式碼

執行結果也是正確的,程式碼多了,但是神仙數字沒了。可讀性也不差。

這裡也順便提一下,如果硬要把上面的需求改成look-up的方式,程式碼就是下面這樣。這樣程式碼的修改的擴充套件性會強一些,成本會小一些,但是可讀性不如上面。取捨關係,實際情況,實際分析。

function formatDate(timeStr){
    //獲取當前時間戳
    let _now=+new Date();
    //求與當前的時間差
    let se=_now-timeStr;
    let _text='';
	//求上一年最後一秒的時間戳
	let lastYearTime=new Date(new Date().getFullYear()+'-01-01 00:00:00')-1;
	//把時間差新增進去(當前時間戳與上一年最後一秒的時間戳的差)新增進去,如果時間差(se)超過這個值,則代表了這個時間是上一年的時間。
	//DATE_LEVEL.unshift(_now-lastYearTime);
	const DATE_LEVEL={
      month:2592000000,
      day:86400000,
      hour:3600000,
      minter:60000,
    }
	let handleFn=[
        {
			time:DATE_LEVEL.month,
            fn:function(timeStr){
                return (new Date(timeStr).getMonth()+1)+'月'+new Date(timeStr).getDate()+'日';
            }
		},
        {
			time:DATE_LEVEL.day,
            fn:function(timeStr){
                return Math.floor(se/DATE_LEVEL.day)+'天前';
            }
		},
		{
			time:DATE_LEVEL.hour,
            fn:function(timeStr){
                return Math.floor(se/DATE_LEVEL.hour)+'小時前';
            }
		},
        {
			time:DATE_LEVEL.minter,
            fn:function(timeStr){
                return Math.ceil(se/DATE_LEVEL.minter)+'分鐘前';
            }
		} 
    ];
    //求上一年最後一秒的時間戳
	let lastYearTime=new Date(new Date().getFullYear()+'-01-01 00:00:00')-1;
	//把時間差(當前時間戳與上一年最後一秒的時間戳的差)和操作函式新增進去,如果時間差(se)超過這個值,則代表了這個時間是上一年的時間。
	handleFn.unshift({
		time:_now-lastYearTime,
		fn:function(timeStr){
		    if(se>DATE_LEVEL.month){
		        return new Date(timeStr).getFullYear()+'年'+(new Date(timeStr).getMonth()+1)+'月'+new Date(timeStr).getDate()+'日';
		        
		    }
		},
	});
    let result='';
    for(let i=0;i<handleFn.length;i++){
        if(se>=handleFn[i].time){
            result=handleFn[i].fn(timeStr);
            if(result){
                return result;
            }
        }
    }
	//如果釋出時間小於1分鐘,之際返回1分鐘
	return result='1分鐘前'
}
複製程式碼

3.配置物件代替switch

比如有一個需求:傳入cash,check,draft,zfb,wx_pay,對應輸出:現金,支票,匯票,支付寶,微信支付。

需求也很簡單,就一個switch就搞定了

function getPayChanne(tag){
    switch(tag){
        case 'cash':return '現金';
        case 'check':return '支票';
        case 'draft':return '匯票';
        case 'zfb':return '支付寶';
        case 'wx_pay':return '微信支付';
    }
}
複製程式碼

[淺析]特定場景下取代if-else和switch的方案

但是這個的硬傷還是和上面一樣,萬一下次又要多加一個如:bank_trans對應輸出銀行轉賬呢,程式碼又要改。類似的問題,同樣的解決方案,配置資料和業務邏輯分離。程式碼如下。

function getPayChanne(tag){
    let payChanneForChinese = {
        'cash': '現金',
        'check': '支票',
        'draft': '匯票',
        'zfb': '支付寶',
        'wx_pay': '微信支付',
    };
    return payChanneForChinese[tag];
}
複製程式碼

同理,如果想封裝一個通用的,也可以的

let payChanneForChinese = {
    'cash': '現金',
    'check': '支票',
    'draft': '匯票',
    'zfb': '支付寶',
    'wx_pay': '微信支付',
};
function getPayChanne(tag,chineseConfig){
    return chineseConfig[tag];
}
getPayChanne('cash',payChanneForChinese);
複製程式碼

這裡使用物件代替 switch 好處就在於

  1. 使用物件不需要 switch 逐個 case 遍歷判斷。
  2. 使用物件,編寫業務邏輯可能更靈活
  3. 使用物件可以使得配置資料和業務邏輯分離。好處參考上一部分內容。

4.小結

最近在特定場合下,代替if-else和switch的解決方案就是這麼多了。if-else,switch本身沒錯,主要是想著怎麼優化程式碼,讓程式碼更加具有可讀性,擴充套件性。如果大家還有什麼優化的方案或者對方面的方案有更好的實現方案。歡迎在評論區留言。

-------------------------華麗的分割線--------------------

想了解更多,關注關注我的微信公眾號:守候書閣

[淺析]特定場景下取代if-else和switch的方案

相關文章