一天一點代碼壞味道(4)

作爲一個後端工程師,想必在職業生涯中都寫過一些不好維護的代碼。本文是我學習《代碼之醜》的學習筆記,今天最後一天,一起品品濫用控制語句的味道,再看看策略模式的使用。

上一篇:一天一點代碼壞味道(3)

1 濫用控制語句

這是一個我們經常都在製造,卻又毫無感知的壞味道。它可能是我們熟悉的if/else,又可能是for循環等等。

多層嵌套

網上有一張多層嵌套的代碼示例圖,你能很快看懂我就服你。很多時候,代碼的複雜度都是來源於多層嵌套,在軟件開發中有一個常用的複雜度的標準,稱之爲 圈複雜度。即圈複雜度越高,代碼也就越複雜,理解和維護的成本也就隨之越高。而在圈複雜度的判定規則中,循環語句和選擇語句就是一個重要的指標。

圖片來源:互聯網

下面來一個實際中可能會出現的短小一點的壞味道代碼:

public void DistributeEpubs(long bookId)
{
    List<Epub> epubs = GetEpubsByBookId(bookId);
    foreach (var epub in epubs)
    {
        if (epub.IsValid())
        {
            bool registered = RegisterIsbn(epub);
            if (registered)
            {
                SendEpub(epub);
            }
        }
    }
}

 這是一個“平鋪直敘寫代碼”的示範,代碼沒錯,只是不容易理解和維護,特別是for循環遍歷中的內容。

因此,不妨將其提取出來,讓這個方法只處理一個元素:

public void DistributeEpubs(long bookId)
{
    List<Epub> epubs = GetEpubsByBookId(bookId);
    foreach (var epub in epubs)
    {
        DistributeEpub(epub);
    }
}

private void DistributeEpub(Epub epub)
{
    if (epub.IsValid())
    {
        bool registered = RegisterIsbn(epub);
        if (registered)
        {
            SendEpub(epub);
        }
    }
}

提取之後,DistributeEpubs方法就只有一層縮進了。而私有方法DistributeEpub還有多層縮進,可以考慮適度繼續處理一下。

if / else

在剛剛的DistributeEpub方法中,造成縮進的原因是if語句。那麼,我們可以採用一種典型的重構手法:衛語句(guard clause),如下所示:

private void DistributeEpub(Epub epub)
{
    if (!epub.IsValid())
    {
        return;
    }

    bool registered = RegisterIsbn(epub);
    if (!registered)
    {
        return;
    }

    SendEpub(epub);
}

使用衛語句重構後,沒有了嵌套,也就沒有了多層縮進。

當代碼裏只有一層縮進的時候,代碼的可讀性是最高的。

《ThoughtWorks文集》中曾經提出,“不要使用else關鍵字”,它認爲 else 也是一種代碼壞味道。那麼,如何不寫else呢?

先看一段代碼,典型的if/else整體:

public double GetEpubPrice(bool highQuality, int chapterSequence)
{
    double price = 0.0;
    if (highQuality && chapterSequence > START_CHARGING_SEQUENCE)
    {
        price = 4.99;
    }
    else if (SequenceNumber > START_CHARGING_SEQUENCE
        && SequenceNumber <= FURTHER_CHARGING_SEQUENCE)
    {
        price = 1.99;
    }
    else if (SequenceNumber > FURTHER_CHARGING_SEQUENCE)
    {
        price = 2.99;
    }
    else
    {
        price = 0.99;
    }

    return price;
}

相信你跟我一樣,第一眼看上去應該是比較煩躁的,那麼如果我們想辦法移除else呢?嗯,仿照衛語句的思路,來優化一下:

public double GetEpubPrice(bool highQuality, int chapterSequence)
{
    if (highQuality && chapterSequence > START_CHARGING_SEQUENCE)
    {
        return 4.99;
    }

    if (SequenceNumber > START_CHARGING_SEQUENCE
        && SequenceNumber <= FURTHER_CHARGING_SEQUENCE)
    {
        return 1.99;
    }

    if (SequenceNumber > FURTHER_CHARGING_SEQUENCE)
    {
        return 2.99;
    }

    return 0.99;
}

優化之後,是不是感覺看上去至少沒有那麼煩躁了。對於這種比較簡單的邏輯,可以這樣改造。但是,稍微複雜些的邏輯,可能就需要引入多態來改進了。

重複的switch

對,重複的switch也是一種壞味道,而之所以會重複出現,根據鄭曄老師的話來說,都是缺少了一個模型。

壞味道代碼:

public double GetBookPrice(User user, Book book)
{
    double price = book.Price;
    switch (user.Level)
    {
        case UserLevel.SILVER:
            price = price * 0.95;
            break;
        case UserLevel.GOLD:
            price = price * 0.85;
            break;
        case UserLevel.PLATINUM:
            price = price * 0.8;
            break;
        default:
            break;
    }

    return price;
}

public double GetEpubPrice(User user, Epub book)
{
    double price = book.Price;
    switch (user.Level)
    {
        case UserLevel.SILVER:
            price = price * 0.95;
            break;
        case UserLevel.GOLD:
            price = price * 0.85;
            break;
        case UserLevel.PLATINUM:
            price = price * 0.8;
            break;
        default:
            break;
    }

    return price;
}

應對類似於這種重複的switch味道,可以看到,不管是Book還是Epub都是其實都是根據用戶的等級來判斷的,而其餘的各種需要根據用戶等級來區分的場景可能都會有相同的代碼,那麼這時候可能就是缺少了一個模型,一個針對用戶等級的模型,我們需要引入多態來取代這個條件表達式。

引入一個UserLevel模型(接口):

public interface IUserLevel
{
    double GetBookPrice(Book book);

    double GetEpubPrice(Epub epub);
}

public class RegularUserLevel : IUserLevel
{
    public double GetBookPrice(Book book)
    {
        return book.Price;
    }

    public double GetEpubPrice(Epub epub)
    {
        return epub.Price;
    }
}

public class GoldUserLevel : IUserLevel
{
    public double GetBookPrice(Book book)
    {
        return book.Price * 0.8;
    }

    public double GetEpubPrice(Epub epub)
    {
        return epub.Price * 0.85;
    }
}

public class SilverUserLevel : IUserLevel
{
    public double GetBookPrice(Book book)
    {
        return book.Price * 0.9;
    }

    public double GetEpubPrice(Epub epub)
    {
        return epub.Price * 0.85;
    }
}

public class PlatinumUserLevel : IUserLevel
{
    public double GetBookPrice(Book book)
    {
        return book.Price * 0.75;
    }

    public double GetEpubPrice(Epub epub)
    {
        return epub.Price * 0.8;
    }
}

修改入口方法代碼:

public double GetBookPrice(User user, Book book)
{
    IUserLevel level = user.GetLevel();
    return level.GetBookPrice(book);
}

public double GetEpubPrice(User user, Epub book)
{
    IUserLevel level = user.GetLevel();
    return level.GetEpubPrice(book);
}

這裏的User類可以如下定義,通過依賴注入將具體的UserLevel類傳遞進去:

public class User
{
    private readonly IUserLevel _level;

    public User(IUserLevel level)
    {
        _level = level;
    }

    public IUserLevel GetLevel()
    {
        return _level;
    }
}

綜述,循環和選擇語句都會增加圈複雜度,可能都是壞味道

2 策略模式

在類似於計算價格的業務場景中,我們經常會使用到策略模式,它是一個典型的開放封閉原則的最佳實踐,也避免了多重的if/else選擇語句,有利於系統的維護。

策略模式定義

策略模式的主要目的主要是將算法的定義和使用分開,也就是將算法的行爲和環境分開,將算法的定義放在專門的策略類中,每一個策略類封裝一個實現算法。而使用算法的環境中針對抽象策略編程,而不是針對實現編程,符合依賴倒置原則。

策略模式包含以下3個角色:

(1)Context(環境類):負責使用算法策略,其中維持了一個抽象策略類的引用實例。

(2)Strategy(抽象策略類):所有策略類的父類,爲所支持的策略算法聲明瞭抽象方法。=> 既可以是抽象類也可以是接口

(3)ConcreteStrategy(具體策略類):實現了在抽象策略類中聲明的方法。

策略模式案例

X公司爲某電影院開發了一套影院售票系統,在該系統中需要爲不同類型的用戶提供不同的電影票打折方式,具體打折方案如下:

(1)學生憑學生證可享受票價8折優惠;

(2)年齡在10週歲以及以下的兒童可以享受每張票減免10元的優惠;

(3)影院VIP用戶除享受票價八折優惠外還可以進行積分,積分累計到一定額度可以換取電影院贈送的獎品;

該系統在將來還可能會根據需求引入更多的打折方案(潛在的可擴展性的需求)。

如果不假思索的設計,我們可能就會寫一串重複的if/else或者switch代碼出來,通過上面的分析,我們知道,這個代碼的圈複雜度會很高,不利於維護和理解。

因此,我們在OCP原則(面向修改封閉面相擴展開放)下,參考策略模式來實現。

具體的類圖設計如下:

 

具體的代碼實現如下:

(1)Context 環境類:MovieTicket

public class MovieTicket
{
    private double _price;
    private IDiscount _discount;

    public double Price
    {
        get
        {
            return _discount.Calculate(_price);
        }
        set
        {
            _price = value;
        }
    }

    public IDiscount Discount
    {
        set
        {
            _discount = value;
        }
    }
}

(2)Strategy 抽象策略類:IDiscount

public interface IDiscount
{
    double Calculate(double price);
}

 (3)ConcreteStrategy 具體策略類:StudentDiscount, VIPDiscount 和 ChildrenDiscount

public class StudentDiscount : IDiscount
{
    public double Calculate(double price)
    {
        Console.WriteLine("學生票:");
        return price * 0.8;
    }
}

public class VIPDiscount : IDiscount
{
    public double Calculate(double price)
    {
        Console.WriteLine("VIP票:");
        Console.WriteLine("增加積分!");
        return price * 0.5;
    }
}

public class ChildrenDiscount : IDiscount
{
    public double Calculate(double price)
    {
        Console.WriteLine("兒童票:");
        return price - 10;
    }
}

最後在客戶端調用時,通過給MovieTicket傳遞不同的打折策略即可實現正確計算票價。而以後有其他打折方案的需求時,只需要擴展一個新的打折策略即可,不用修改原有代碼。

策略模式的本質就是OCP原則的一個具體應用,將變化的算法與不變的環境區分開來,可以在類似電商業務商品複雜的計算價格等場景中使用。

3 小結

本文總結了濫用控制語句如循環和選擇語句造成的高複雜度代碼的應對方法,還介紹了策略模式的定義、類圖以及案例,希望能對你的代碼精進之路有用。

最後,感謝鄭曄老師的這門《代碼之醜》課程,讓我受益匪淺!我也誠心把它推薦給關注Edison的各位童鞋!

參考資料

鄭曄,《代碼之醜》(推薦訂閱學習)

Martin Flower著,熊傑譯,《重構:改善既有代碼的設計》(推薦至少學習第三章)

👇掃碼訂閱《代碼之醜》

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章