一天一点代码坏味道(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著,熊杰译,《重构:改善既有代码的设计》(推荐至少学习第三章)

👇扫码订阅《代码之丑》

 

posted @ 2021-02-22 22:51  EdisonZhou  阅读(151)  评论(0编辑  收藏  举报