iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 7
2

if判斷式是每個人第一次學寫程式時會學到的語法,主要用於根據條件來控制流程。雖然if判斷式每個人都會寫,但是如何使用if判斷式,讓主要流程與次要流程在閱讀時有脈絡可循,就是一個值得思考的方向。

DeadAnts的例子中,Count(string ants)檢查了輸入是否符合預期,確認符合預期後計算dead ants數量,不符合的話就回傳0。

public class DeadAnts
{
    public int Count(string ants)
    {
        if (!string.IsNullOrEmpty(ants))
        {
            return CalculateDeadAntCount(FindDeadAnts(ants));
        }
        else
        {
            return 0;
        }
    }

    private string FindDeadAnts(string ants)
    {
        return new Regex("ant|[^ant]").Replace(ants, "");
    }

    private int CalculateDeadAntCount(string deadAntsCollections)
    {
        if (!string.IsNullOrEmpty(deadAntsCollections))
        {
            return deadAntsCollections.GroupBy(x => x).Max(x => x.Count());
        }
        else
        {
            return 0;
        }
    }
}

我們從方法名稱可以知道,計算dead ants才是這個方法的主要目的。所以把if判斷是稍微調整一下,讓參數不符合預期時可以在一開始就回傳,這種if判斷是稱為guard if。這樣的好處是可以主要流程在縮排的第一層,比較能凸顯重要的流程。

public int Count(string ants)
{
    if (string.IsNullOrEmpty(ants))
    {
        return 0;
    }

    return CalculateDeadAntCount(FindDeadAnts(ants));
}

guard if常用在提前結束非正常流程,像是檢查參數是否符合需求就十分適合使用guard if提前結束流程。假設有一個新的需求,dead ants長度小於10時回傳1,此時就可以透過guard if來判斷,讓主要流程維持在第一層縮排。

public int Count(string ants)
{
    if (string.IsNullOrEmpty(ants))
    {
        return 0;
    }

    var deadAnts = FindDeadAnts(ants);
    if (deadAnts.Length < 10)
    {
        return 1;
    }
    
    return CalculateDeadAntCount(FindDeadAnts(deadAnts));
}

Potter的例子中,可以發現在計算折扣時,使用了一長串的if判斷式。因為每個一個折扣的計算方式都是等同重要的,所以不適合使用guard if來簡化。

public decimal BuyHarryPotter(List<int> numbersOfVolumes)
{
    var maxCombination = numbersOfVolumes.Count(numberOfEachVolume => numberOfEachVolume > 0); 
    return GetCombinationPrice(maxCombination) + BuyHarryPotter(GetRemainingBooks(numbersOfVolumes));
}

private decimal GetCombinationPrice(int maxCombination)
{
    return maxCombination * 100 * GetDiscount(maxCombination);
}

private List<int> GetRemainingBooks(List<int> eachBookAmount)
{
    return eachBookAmount.Select(x => x > 0 ? x - 1 : x).ToList();
}

private decimal GetDiscount(int maxCombination)
{
    if (maxCombination == 2)
    {
        return 0.95m;
    }
    else if (maxCombination == 3)
    {
        return 0.9m;
    } 
    else if (maxCombination == 4)
    {
        return 0.8m;
    }
    else if (maxCombination == 5)
    {
        return 0.75m;
    }
    else
    {
        return 1;
    }
}

這種情況下可以把計算折扣的邏輯放到Dictionary中,提供狀態給Dictionary,Dictionary就能幫我們選擇符合條件的折扣。

private readonly Dictionary<int, decimal> _discountDict = new Dictionary<int, decimal>()
{
    {2, 0.95m},
    {3, 0.9m},
    {4, 0.8m},
    {5, 0.75m}
};

public decimal BuyHarryPotter(List<int> eachBookAmount)
{
		var maxCombination = eachBookAmount.Count(x => x > 0);
    return GetCombinationPrice(maxCombination) + BuyHarryPotter(GetRemainingBooks(eachBookAmount));
}

private decimal GetCombinationPrice(int maxCombination)
{
    return (_discountDict.ContainsKey(maxCombination)
        ? maxCombination * 100 * _discountDict[maxCombination]
        : maxCombination * 100);
}

private List<int> GetRemainingBooks(List<int> eachBookAmount)
{
    return eachBookAmount.Select(x => x > 0 ? x - 1 : x).ToList();
}

Dictionary的Value可以放各式各樣的物件,在前天提到的Reverse polish notation calculator的例子裡,也是透過Dictionary來選擇不同的運算邏輯。

var operatorDict = new Dictionary<string, Func<double, double, double>>()
{
    {"+", (operator2, operator1) => operator1 + operator2 },
    {"-", (operator2, operator1) => operator1 - operator2 },
    {"*", (operator2, operator1) => operator1 * operator2 },
    {"/", (operator2, operator1) => operator1 / operator2 },
};

Dictionary比較適合邏輯比較簡單的情境,假設折扣還需要考慮不同使用者的身份或是否促銷期等相依的話,Dictionary的Value可能就會變得很複雜,在這種情況下,則會比較建議實作Strategy Pattern,關於Strategy Pattern之後若有機會則會在介紹。

不管是guard if或是Dictionary都是為了讓代碼和流程更為簡潔,減輕閱讀代碼的負擔。


上一篇
Day 6 善用LINQ操作集合
下一篇
Day 8 消除常見迴圈
系列文
在Kata中尋找Clean Code是否搞錯了什麼30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言