65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 坏实践:通过坏代码示例学习如何写出好代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (492投票s)

2016年3月6日

CPOL

14分钟阅读

viewsIcon

795297

本文旨在通过一个糟糕的类示例,展示如何编写干净、可扩展且可维护的代码。

引言

我叫 Radoslaw Sadowski,是一名微软认证软件开发人员。自职业生涯开始以来,我一直从事微软技术相关工作。

经过几年的经验积累,我见过太多糟糕的代码,足以写一本书来展示所有这些“污秽”的例子。
这些经历让我变成了一个“干净代码”的狂热者。

要理解本文,您至少需要具备以下基础知识:

  • C# 语言
  • 依赖注入、工厂方法和策略设计模式

本文描述的示例是一个具体、真实的业务场景——我不会展示像“使用装饰器模式构建披萨”或“使用策略模式实现计算器”这样的理论性示例。:)

尽管这些理论性示例在解释概念时非常好用,但我发现它们在实际生产应用中极其难以使用。

我们经常听到“不要使用这个,而要使用那个”。但为什么呢?我将尝试解释原因,并证明所有好的实践和设计模式确实都在拯救我们的生命!

注意

  • 我不会解释 C# 语言特性和设计模式(这会让文章太长)。网络上有很多很好的理论性示例。我将专注于展示如何在日常工作中应用它们。

  • 这个示例被极大简化,仅为了突出所描述的问题——当我通过包含大量代码的示例学习时,我发现很难理解文章的整体思路。

  • 我并不是说我为下面描述的问题提供的解决方案是唯一的,但可以肯定的是,它们确实有效,并且能为您的代码带来高质量的解决方案。

  • 在下面的代码中,我不关心错误处理、日志记录等。代码仅用于展示常见编程问题的解决方案。

让我们谈谈具体内容...

糟糕的类...

我们的真实世界示例将是下面的类

public class Class1
{
  public decimal Calculate(decimal amount, int type, int years)
  {
    decimal result = 0;
    decimal disc = (years > 5) ? (decimal)5/100 : (decimal)years/100; 
    if (type == 1)
    {
      result = amount;
    }
    else if (type == 2)
    {
      result = (amount - (0.1m * amount)) - disc * (amount - (0.1m * amount));
    }
    else if (type == 3)
    {
      result = (0.7m * amount) - disc * (0.7m * amount);
    }
    else if (type == 4)
    {
      result = (amount - (0.5m * amount)) - disc * (amount - (0.5m * amount));
    }
    return result;
  }
}

这真的是糟糕的代码。我们能想象上面这个类的作用吗?它在做一些奇怪的计算?目前我们只能说这么多……

现在想象一下,这是一个 `DiscountManager` 类,负责在在线商店计算客户购买产品时的折扣。

  • 怎么会?真的吗?
  • 很不幸,是的!

它完全难以阅读、维护、扩展,并且使用了许多糟糕的实践和反模式。

这里究竟有什么问题?

  1. 命名——我们只能猜测这个方法计算什么,以及计算的输入究竟是什么。很难从这个类中提取计算算法。
    风险
    在这种情况下,最重要的事情是——浪费时间

    如果我们收到业务部门关于展示算法细节的查询,或者我们需要修改这段代码,我们需要花费很长时间才能理解我们 `Calculate` 方法的逻辑。如果我们不写文档或重构代码,下次我们/其他开发人员将花费同样的时间来弄清楚那里到底发生了什么。我们也很容易在修改时出错。

  2. 魔法数字

    在我们的例子中,`type` 变量代表客户账户的状态。你能猜到吗?`if`-`else if` 语句决定了折扣后产品的价格如何计算。
    现在我们不知道 1、2、3 或 4 是哪种账户类型。现在想象一下,您需要更改 `ValuableCustomer` 账户的折扣算法。您可以尝试从剩余的代码中推断——这需要很长时间,但即使如此,我们也可能很容易犯错,并修改 `BasicCustomer` 账户的算法——数字 2 或 3 并不具有描述性。在我们的错误之后,客户会很高兴,因为他们会获得尊贵客户的折扣。:)

  3. 不明显的错误

    因为我们的代码非常混乱且难以阅读,我们很容易错过非常重要的事情。想象一下,我们的系统中添加了一个新的客户账户状态——`GoldenCustomer`。现在,我们的方法将返回 `0` 作为这种新类型账户购买的每件产品的最终价格。为什么?因为如果我们的任何 `if`-`else if` 条件都不满足(存在未处理的账户状态),方法将始终返回 `0`。我们的老板不高兴——他在有人意识到不对劲之前,卖了很多免费产品。

  4. 难以阅读

    我们都必须承认,我们的代码极其难以阅读。
    难以阅读 = 更多理解代码的时间 + 增加犯错的风险。

  5. 魔法数字——再次

    我们知道 0.1、0.7、0.5 这样的数字是什么意思吗?我们不知道,但如果我们拥有代码,我们应该知道。

    让我们想象一下,您需要更改这一行

    result = (amount - (0.5m * amount)) - disc * (amount - (0.5m * amount));

    由于方法完全难以阅读,您只将第一个 0.5 改为 0.4,而将第二个 0.5 保留原样。这可能是一个 bug,但也可能是一个完全正确的修改。这是因为 0.5 并没有告诉我们任何信息。
    将 `years` 变量转换为 `disc` 变量时,故事也一样

    decimal disc = (years > 5) ? (decimal)5/100 : (decimal)years/100;

    它以百分比的形式计算客户在我们系统中的服务年限折扣。好吧,但 5 是什么鬼?这是客户因忠诚度而获得的最大折扣百分比。你能猜到吗?

  6. DRY – 不要重复自己

    第一眼看不出来,但我们的方法中有许多地方的代码是重复的。
    例如

    disc * (amount - (0.1m * amount));

    与……逻辑相同

    disc * (amount - (0.5m * amount))

    只有一个 `static` 变量在起作用——我们可以轻松地参数化这个变量。
    如果我们不消除重复的代码,我们会遇到这样的情况:我们只完成任务的一部分,因为我们没有看到需要在代码的 5 个地方以相同的方式进行更改。上面的逻辑是计算客户在我们系统中的服务年限折扣。因此,如果我们只在 2 个地方更改此逻辑,而不是 3 个地方,我们的系统就会变得不一致。

  7. 每个类有多个职责

    我们的方法至少有三个职责

    1. 选择计算算法
    2. 计算账户状态的折扣
    3. 计算客户服务年限的折扣

这违反了单一职责原则。这会带来什么风险?如果我们想修改这 3 个功能中的一个,它会影响到其他两个。这意味着它可能会破坏我们不想触及的功能。因此,我们将不得不重新测试所有类——浪费时间

重构...

在接下来的九个步骤中,我将向您展示如何避免上述所有风险和糟糕的实践,从而实现干净、可维护、可单元测试的代码,读起来就像一本书一样。

第一步——命名、命名、命名

在我看来,这是优秀代码最重要的方面之一。我们只更改了方法、参数和变量的名称,现在我们确切地知道下面的类负责什么。

public class DiscountManager
{
  public decimal ApplyDiscount
    (decimal price, int accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? 
            (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100; 
    if (accountStatus == 1)
    {
      priceAfterDiscount = price;
    }
    else if (accountStatus == 2)
    {
      priceAfterDiscount = (price - (0.1m * price)) - 
           (discountForLoyaltyInPercentage * (price - (0.1m * price)));
    }
    else if (accountStatus == 3)
    {
      priceAfterDiscount = (0.7m * price) - 
           (discountForLoyaltyInPercentage * (0.7m * price));
    }
    else if (accountStatus == 4)
    {
      priceAfterDiscount = (price - (0.5m * price)) - 
           (discountForLoyaltyInPercentage * (price - (0.5m * price)));
    }
 
    return priceAfterDiscount;
  }
}

但是,我们仍然不知道 1、2、3、4 的意思,让我们来处理一下!

第二步——魔法数字

在 C# 中避免魔法数字的一种技术是用 `enum` 替换它们。我准备了一个 `AccountStatus enum` 来替换 `if`-`else if` 语句中的魔法数字。

public enum AccountStatus
{
  NotRegistered = 1,
  SimpleCustomer = 2,
  ValuableCustomer = 3,
  MostValuableCustomer = 4
}

现在看看我们重构后的类,我们可以轻松地说出为哪种账户状态使用了哪种折扣计算算法。混淆账户状态的风险迅速降低。

public class DiscountManager
{
  public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, 
                               int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? 
            (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100;
 
    if (accountStatus == AccountStatus.NotRegistered)
    {
      priceAfterDiscount = price;
    }
    else if (accountStatus == AccountStatus.SimpleCustomer)
    {
      priceAfterDiscount = (price - (0.1m * price)) - 
           (discountForLoyaltyInPercentage * (price - (0.1m * price)));
    }
    else if (accountStatus == AccountStatus.ValuableCustomer)
    {
      priceAfterDiscount = (0.7m * price) - 
           (discountForLoyaltyInPercentage * (0.7m * price));
    }
    else if (accountStatus == AccountStatus.MostValuableCustomer)
    {
      priceAfterDiscount = (price - (0.5m * price)) - 
           (discountForLoyaltyInPercentage * (price - (0.5m * price)));
    }
    return priceAfterDiscount;
  }
}

第三步——更具可读性

在这一步,我们将通过将 `if`-`else if` 语句替换为 `switch`-`case` 语句来提高类的可读性。

我还将长单行算法分成了两行。现在我们将“计算账户状态折扣”与“计算客户账户服务年限折扣”分开了。

例如,这行

priceAfterDiscount = (price - (0.5m * price)) - 
       (discountForLoyaltyInPercentage * (price - (0.5m * price)));

被替换为

priceAfterDiscount = (price - (0.5m * price));
priceAfterDiscount = 
    priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);

下面的代码展示了所描述的更改。

public class DiscountManager
{
  public decimal ApplyDiscount
    (decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? 
                       (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        priceAfterDiscount = price;
        break;
      case AccountStatus.SimpleCustomer:
        priceAfterDiscount = (price - (0.1m * price));
        priceAfterDiscount = priceAfterDiscount - 
                             (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.ValuableCustomer:
        priceAfterDiscount = (0.7m * price);
        priceAfterDiscount = priceAfterDiscount - 
                             (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.MostValuableCustomer:
        priceAfterDiscount = (price - (0.5m * price));
        priceAfterDiscount = priceAfterDiscount - 
                             (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
    }
    return priceAfterDiscount;
  }
}

第四步——不明显的错误

我们终于找到了隐藏的 bug!
如前所述,我们的 `ApplyDiscount` 方法将返回 `0` 作为从新类型账户购买的每件产品的最终价格。不幸但真实。

我们该如何修复?通过抛出 `NotImplementedException`!

您可能会想——这不是异常驱动开发吗?不是!

当我们的方法接收到一个我们未支持的 `AccountStatus` 值作为参数时,我们希望立即注意到这一点,并停止程序流程,以免在我们的系统中产生任何不可预测的操作。

这种情况永远不应该发生,因此如果发生,我们必须抛出异常。

下面的代码已修改为在没有条件满足时抛出 `NotImplementedException`——这是 `switch`-`case` 语句的default 部分。

public class DiscountManager
{
  public decimal ApplyDiscount
    (decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? 
            (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        priceAfterDiscount = price;
        break;
      case AccountStatus.SimpleCustomer:
        priceAfterDiscount = (price - (0.1m * price));
        priceAfterDiscount = priceAfterDiscount - 
             (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.ValuableCustomer:
        priceAfterDiscount = (0.7m * price);
        priceAfterDiscount = priceAfterDiscount - 
             (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.MostValuableCustomer:
        priceAfterDiscount = (price - (0.5m * price));
        priceAfterDiscount = priceAfterDiscount - 
             (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      default:
        throw new NotImplementedException();
    }
    return priceAfterDiscount;
  }
}

第五步——让我们分析计算

在我们的示例中,客户的折扣有两个标准:

  1. 账户状态
  2. 客户在我们系统中的账户年限。

所有计算客户服务年限折扣的算法看起来都相似

(discountForLoyaltyInPercentage * priceAfterDiscount)

但有一个例外,即计算账户状态的固定折扣:`0.7m * price`。

因此,让我们将其更改为与其他情况相同的格式:`price - (0.3m * price)`。

public class DiscountManager
{
  public decimal ApplyDiscount
    (decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? 
           (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        priceAfterDiscount = price;
        break;
      case AccountStatus.SimpleCustomer:
        priceAfterDiscount = (price - (0.1m * price));
        priceAfterDiscount = priceAfterDiscount - 
             (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.ValuableCustomer:
        priceAfterDiscount = (price - (0.3m * price));
        priceAfterDiscount = priceAfterDiscount - 
             (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.MostValuableCustomer:
        priceAfterDiscount = (price - (0.5m * price));
        priceAfterDiscount = priceAfterDiscount - 
             (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      default:
        throw new NotImplementedException();
    }
    return priceAfterDiscount;
  }
}

现在,我们有了所有根据账户状态计算折扣的规则,并且格式统一。

price - ((static_discount_in_percentages/100) * price)

第六步——消除魔法数字——另一种技术

让我们看看 `static` 变量,它是账户状态折扣算法的一部分:`(static_discount_in_percentages/100)`。

以及它的具体实例
0.1m
0.3m
0.5m

这些数字也相当“魔法”,它们并没有告诉我们任何关于它们的信息。

在计算“客户服务年限”的折扣时,我们遇到了同样的情况,“忠诚度折扣”。

decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? 
        (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100;

数字 5 使我们的代码非常神秘。

我们必须对它做些什么,让它更具描述性!

我将使用另一种避免魔法字符串的技术——即常量(C# 中的 `const` 关键字)。我强烈建议为常量创建一个 `static` 类,以便在应用程序中集中管理。

对于我们的示例,我创建了下面的类。

public static class Constants
{
  public const int MAXIMUM_DISCOUNT_FOR_LOYALTY = 5;
  public const decimal DISCOUNT_FOR_SIMPLE_CUSTOMERS = 0.1m;
  public const decimal DISCOUNT_FOR_VALUABLE_CUSTOMERS = 0.3m;
  public const decimal DISCOUNT_FOR_MOST_VALUABLE_CUSTOMERS = 0.5m;
}

修改后,我们的 `DiscountManager` 类将如下所示。

public class DiscountManager
{
  public decimal ApplyDiscount
    (decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    decimal discountForLoyaltyInPercentage = 
          (timeOfHavingAccountInYears > Constants.MAXIMUM_DISCOUNT_FOR_LOYALTY) ? 
          (decimal)Constants.MAXIMUM_DISCOUNT_FOR_LOYALTY/100 : 
                             (decimal)timeOfHavingAccountInYears/100;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        priceAfterDiscount = price;
        break;
      case AccountStatus.SimpleCustomer:
        priceAfterDiscount = (price - (Constants.DISCOUNT_FOR_SIMPLE_CUSTOMERS * price));
        priceAfterDiscount = priceAfterDiscount - 
                             (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.ValuableCustomer:
        priceAfterDiscount = (price - (Constants.DISCOUNT_FOR_VALUABLE_CUSTOMERS * price));
        priceAfterDiscount = priceAfterDiscount - 
                             (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.MostValuableCustomer:
        priceAfterDiscount = (price - 
                             (Constants.DISCOUNT_FOR_MOST_VALUABLE_CUSTOMERS * price));
        priceAfterDiscount = priceAfterDiscount - 
                             (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      default:
        throw new NotImplementedException();
    }
    return priceAfterDiscount;
  }
}

我希望您同意,现在我们的方法更具自我解释性了。:)

第七步——不要重复自己!

为了避免代码重复,我们将部分算法移到单独的方法中。

我们将使用扩展方法来做到这一点。

首先,我们必须创建两个扩展方法。

public static class PriceExtensions
{
  public static decimal ApplyDiscountForAccountStatus
                (this decimal price, decimal discountSize)
  {
    return price - (discountSize * price);
  }
 
  public static decimal ApplyDiscountForTimeOfHavingAccount
                (this decimal price, int timeOfHavingAccountInYears)
  {
     decimal discountForLoyaltyInPercentage = 
             (timeOfHavingAccountInYears > Constants.MAXIMUM_DISCOUNT_FOR_LOYALTY) ? 
             (decimal)Constants.MAXIMUM_DISCOUNT_FOR_LOYALTY/100 : 
             (decimal)timeOfHavingAccountInYears/100;
    return price - (discountForLoyaltyInPercentage * price);
  }
}

由于我们的方法名称非常具有描述性,我无需解释它们负责什么。现在,让我们在示例中使用新代码。

public class DiscountManager
{
  public decimal ApplyDiscount
    (decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        priceAfterDiscount = price;
        break;
      case AccountStatus.SimpleCustomer:
        priceAfterDiscount = price.ApplyDiscountForAccountStatus
                             (Constants.DISCOUNT_FOR_SIMPLE_CUSTOMERS)
          .ApplyDiscountForTimeOfHavingAccount(timeOfHavingAccountInYears);
        break;
      case AccountStatus.ValuableCustomer:
        priceAfterDiscount = price.ApplyDiscountForAccountStatus
                             (Constants.DISCOUNT_FOR_VALUABLE_CUSTOMERS)
          .ApplyDiscountForTimeOfHavingAccount(timeOfHavingAccountInYears);
        break;
      case AccountStatus.MostValuableCustomer:
        priceAfterDiscount = price.ApplyDiscountForAccountStatus
                             (Constants.DISCOUNT_FOR_MOST_VALUABLE_CUSTOMERS)
          .ApplyDiscountForTimeOfHavingAccount(timeOfHavingAccountInYears);
        break;
      default:
        throw new NotImplementedException();
    }
    return priceAfterDiscount;
  }
}

扩展方法非常好,可以使代码更简洁,但归根结底它们仍然是 `static` 类,并且可能使单元测试非常困难,甚至不可能。因此,在最后一步中我们将摆脱它们。我只是用它们来向您展示它们如何让我们的生活更轻松,但我不是它们的忠实粉丝。

无论如何,您是否同意我们的代码现在看起来好多了?

那么,让我们进入下一步!

第八步——删除一些不必要的行...

我们应该编写尽可能简短和简单的代码。代码越短 = bug 越少,理解业务逻辑的时间越短。
让我们进一步简化我们的示例。

我们可以轻松地注意到,我们对三种类型的客户账户调用了相同的方法。

.ApplyDiscountForTimeOfHavingAccount(timeOfHavingAccountInYears);

不能只做一次吗?不行,因为对于 `NotRegistered` 用户存在例外,因为服务年限折扣对未注册客户没有意义。没错,但是未注册用户的账户年限是多少?

- 0 年

在这种情况下,折扣将始终为 `0`,因此我们可以安全地为未注册用户添加此折扣,让我们这样做!

public class DiscountManager
{
  public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, 
                               int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        priceAfterDiscount = price;
        break;
      case AccountStatus.SimpleCustomer:
        priceAfterDiscount = price.ApplyDiscountForAccountStatus
                             (Constants.DISCOUNT_FOR_SIMPLE_CUSTOMERS);
        break;
      case AccountStatus.ValuableCustomer:
        priceAfterDiscount = price.ApplyDiscountForAccountStatus
                             (Constants.DISCOUNT_FOR_VALUABLE_CUSTOMERS);
        break;
      case AccountStatus.MostValuableCustomer:
        priceAfterDiscount = price.ApplyDiscountForAccountStatus
                             (Constants.DISCOUNT_FOR_MOST_VALUABLE_CUSTOMERS);
        break;
      default:
        throw new NotImplementedException();
    }
    priceAfterDiscount = priceAfterDiscount.ApplyDiscountForTimeOfHavingAccount
                         (timeOfHavingAccountInYears);
    return priceAfterDiscount;
  }
}

我们将此行移出了 `switch`-`case` 语句。好处——代码更少!

第九步——进阶——最终获得干净的代码

好的!现在我们可以像阅读一本书一样阅读我们的类了,但这还不够!我们想要超级干净的代码!

好的,那么让我们做一些改变来最终实现这个目标。我们将使用依赖注入策略以及工厂方法设计模式!

这是我们代码最终的样子。

public class DiscountManager
{
  private readonly IAccountDiscountCalculatorFactory _factory;
  private readonly ILoyaltyDiscountCalculator _loyaltyDiscountCalculator;
 
  public DiscountManager(IAccountDiscountCalculatorFactory factory, 
                         ILoyaltyDiscountCalculator loyaltyDiscountCalculator)
  {
    _factory = factory;
    _loyaltyDiscountCalculator = loyaltyDiscountCalculator;
  }
 
  public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, 
                               int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    priceAfterDiscount = _factory.GetAccountDiscountCalculator
                         (accountStatus).ApplyDiscount(price);
    priceAfterDiscount = _loyaltyDiscountCalculator.ApplyDiscount
                         (priceAfterDiscount, timeOfHavingAccountInYears);
    return priceAfterDiscount;
  }
}
public interface ILoyaltyDiscountCalculator
{
  decimal ApplyDiscount(decimal price, int timeOfHavingAccountInYears);
}
 
public class DefaultLoyaltyDiscountCalculator : ILoyaltyDiscountCalculator
{
  public decimal ApplyDiscount(decimal price, int timeOfHavingAccountInYears)
  {
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 
            Constants.MAXIMUM_DISCOUNT_FOR_LOYALTY) ? 
            (decimal)Constants.MAXIMUM_DISCOUNT_FOR_LOYALTY/100 : 
            (decimal)timeOfHavingAccountInYears/100;
    return price - (discountForLoyaltyInPercentage * price);
  }
}
public interface IAccountDiscountCalculatorFactory
{
  IAccountDiscountCalculator GetAccountDiscountCalculator(AccountStatus accountStatus);
}
 
public class DefaultAccountDiscountCalculatorFactory : IAccountDiscountCalculatorFactory
{
  public IAccountDiscountCalculator GetAccountDiscountCalculator(AccountStatus accountStatus)
  {
    IAccountDiscountCalculator calculator;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        calculator = new NotRegisteredDiscountCalculator();
        break;
      case AccountStatus.SimpleCustomer:
        calculator = new SimpleCustomerDiscountCalculator();
        break;
      case AccountStatus.ValuableCustomer:
        calculator = new ValuableCustomerDiscountCalculator();
        break;
      case AccountStatus.MostValuableCustomer:
        calculator = new MostValuableCustomerDiscountCalculator();
        break;
      default:
        throw new NotImplementedException();
    }
 
    return calculator;
  }
}
public interface IAccountDiscountCalculator
{
  decimal ApplyDiscount(decimal price);
}
 
public class NotRegisteredDiscountCalculator : IAccountDiscountCalculator
{
  public decimal ApplyDiscount(decimal price)
  {
    return price;
  }
}
 
public class SimpleCustomerDiscountCalculator : IAccountDiscountCalculator
{
  public decimal ApplyDiscount(decimal price)
  {
    return price - (Constants.DISCOUNT_FOR_SIMPLE_CUSTOMERS * price);
  }
}
 
public class ValuableCustomerDiscountCalculator : IAccountDiscountCalculator
{
  public decimal ApplyDiscount(decimal price)
  {
    return price - (Constants.DISCOUNT_FOR_VALUABLE_CUSTOMERS * price);
  }
}
 
public class MostValuableCustomerDiscountCalculator : IAccountDiscountCalculator
{
  public decimal ApplyDiscount(decimal price)
  {
    return price - (Constants.DISCOUNT_FOR_MOST_VALUABLE_CUSTOMERS * price);
  }
}

首先,我们摆脱了扩展方法(意味着 `static` 类),因为使用它们会使调用者类(`DiscountManager`)与扩展方法中的折扣算法紧密耦合。如果我们想单元测试我们的 `ApplyDiscount` 方法,这是不可能的,因为我们也会测试 `PriceExtensions` 类。

为了避免这种情况,我创建了一个 `DefaultLoyaltyDiscountCalculator` 类,它包含 `ApplyDiscountForTimeOfHavingAccount` 扩展方法的逻辑,并通过抽象(即接口)`ILoyaltyDiscountCalculator` 隐藏了其实现。现在,当我们想测试 `DiscountManager` 类时,我们将能够通过构造函数将实现了 `ILoyaltyDiscountCalculator` 的 mock/fake 对象注入到我们的 `DiscountManager` 类中,从而只测试 `DiscountManager` 的实现。这里,我们使用了依赖注入设计模式

通过这样做,我们将忠诚度折扣的计算责任移到了另一个类,因此如果我们想修改此逻辑,我们只需要更改 `DefaultLoyaltyDiscountCalculator` 类,而所有其他代码将保持不变——降低破坏的风险,减少测试时间。

下面是 `DiscountManager` 类中代码按类划分的使用情况。

priceAfterDiscount = _loyaltyDiscountCalculator.ApplyDiscount
                    (priceAfterDiscount, timeOfHavingAccountInYears);

在账户状态折扣计算逻辑方面,我不得不创建一些更复杂的东西。我们有两个责任想要从 `DiscountManager` 中移出:

  1. 根据账户状态使用哪种算法
  2. 特定算法计算的详细信息

为了移出第一个责任,我创建了一个工厂类(`DefaultAccountDiscountCalculatorFactory`),它是工厂方法设计模式的实现,并将其隐藏在抽象——`IAccountDiscountCalculatorFactory`——之后。

我们的工厂将决定选择哪种折扣算法。最后,我们通过构造函数使用依赖注入设计模式将工厂注入到 `DiscountManager` 类中。

下面是 `DiscountManager` 类中使用工厂的情况。

priceAfterDiscount = _factory.GetAccountDiscountCalculator(accountStatus).ApplyDiscount(price);

上面的行将返回适用于特定账户状态的正确策略,并调用其上的 `ApplyDiscount` 方法。

第一个责任已经划分,所以让我们谈谈第二个。

让我们来谈谈策略...

由于每个账户状态的折扣算法可能不同,我们将不得不使用不同的策略来实现它。这是使用策略设计模式的绝佳机会!

在我们的示例中,我们现在有三个策略:

  1. NotRegisteredDiscountCalculator
  2. SimpleCustomerDiscountCalculator
  3. MostValuableCustomerDiscountCalculator

它们包含了特定折扣算法的实现,并隐藏在抽象
IAccountDiscountCalculator.

之后。这将允许我们的 `DiscountManager` 类在不了解其实现的情况下使用正确的策略。`DiscountManager` 只知道返回的对象实现了 `IAccountDiscountCalculator` 接口,该接口包含 `ApplyDiscount` 方法。

`NotRegisteredDiscountCalculator`、`SimpleCustomerDiscountCalculator`、`MostValuableCustomerDiscountCalculator` 类包含了根据账户状态的正确算法实现。由于我们的三个策略看起来很相似,我们能做的更多的是为所有三个算法创建一个方法,并从每个策略类中用不同的参数调用它。由于这会使我们的示例过大,我没有决定这样做。

好了,现在总结一下,我们有了干净易读的代码,并且我们所有的类都只有一个职责——只有一个改变的理由

  1. `DiscountManager` – 管理代码流程
  2. `DefaultLoyaltyDiscountCalculator` – 计算忠诚度折扣
  3. `DefaultAccountDiscountCalculatorFactory` – 决定选择哪种账户状态折扣计算策略
  4. `NotRegisteredDiscountCalculator`, `SimpleCustomerDiscountCalculator`, `MostValuableCustomerDiscountCalculator` – 计算账户状态折扣

现在比较一下最初的方法

public class Class1
{
    public decimal Calculate(decimal amount, int type, int years)
    {
        decimal result = 0;
        decimal disc = (years > 5) ? (decimal)5 / 100 : (decimal)years / 100;
        if (type == 1)
        {
            result = amount;
        }
        else if (type == 2)
        {
            result = (amount - (0.1m * amount)) - disc * (amount - (0.1m * amount));
        }
        else if (type == 3)
        {
            result = (0.7m * amount) - disc * (0.7m * amount);
        }
        else if (type == 4)
        {
            result = (amount - (0.5m * amount)) - disc * (amount - (0.5m * amount));
        }
        return result;
    }
}

与我们新的、重构后的代码。

public decimal ApplyDiscount
  (decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
{
  decimal priceAfterDiscount = 0;
  priceAfterDiscount = 
    _factory.GetAccountDiscountCalculator(accountStatus).ApplyDiscount(price);
  priceAfterDiscount = 
  _loyaltyDiscountCalculator.ApplyDiscount(priceAfterDiscount, timeOfHavingAccountInYears);
  return priceAfterDiscount;
}

结论

本文介绍的代码经过极大简化,以便更容易解释所使用的技术和模式。它展示了常见的编程问题如何以糟糕的方式解决,以及使用良好实践和设计模式以正确、干净的方式解决这些问题的好处。

在我工作经验中,我曾多次看到本文中强调的糟糕实践。它们显然存在于应用程序的许多地方,而不是像我的示例那样只存在于一个类中,这使得查找它们更加困难,因为它们隐藏在正常的代码之间。编写这类代码的人总是争辩说他们遵循了“保持简单,愚蠢”(Keep It Simple Stupid)的原则。不幸的是,系统几乎总是在增长并变得非常复杂。那时,对这种简单、不可扩展的代码进行的任何修改都非常困难,并带来破坏某些东西的巨大风险。

请记住,您的代码将在生产环境中运行很长时间,并且会在每次业务需求变更时进行修改。因此,编写过于简单、不可扩展的代码很快就会带来严重后果。最后,这对维护您代码的开发人员来说会很好!

如果您对本文有任何疑问,请随时与我联系!

历史

  • 2016 年 3 月 6 日:初始版本
© . All rights reserved.