C#坏习惯:通过不好的例子学习如何制作好的代码——第1部分

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mzl87/article/details/86556290

目录

介绍

类写得那么糟糕......

1、命名

2、神奇数字      ​

3、不明显的错误

4、不可读

5、神奇数字——又一次

6、DRY——不要重复自己

7、每类多重责任

重构...

I STEP——命名,命名,命名

II STEP——神奇数字

III STEP——更具可读性

IV STEP——不明显的错误

V STEP——让我们分析计算结果

VI STEP——摆脱神奇数字——另一种技巧

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

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

STEP IX——高级——最后得到干净的代码

结论


介绍

本文的目的是通过展示写得不好的类的示例来展示如何编写干净,可扩展和可维护的代码。我将解释它可能带来的麻烦,并提出一种如何通过更好的解决方案取代它的方法——使用良好的实践和设计模式。

第一部分是每个熟悉C#语言基础知识的开发人员——它将展示一些基本的错误和技巧,如何让代码像书一样可读。高级部分适用于至少对设计模式有基本了解的开发人员——它将显示完全干净,可单元测试的代码。

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

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

本文中描述的示例是一个具体的,真实世界的功能——我不会显示使用装饰模式构建披萨或使用策略模式实现计算器的示例:)

           https://openclipart.org/image/800px/svg_to_png/170891/Calculator.png           https://openclipart.org/image/800px/svg_to_png/189439/Pizza-Pepperoni.png      

由于这些理论示例非常适合解释,我发现在实际的生产应用程序中使用它非常困难。

我们听到很多次不要使用this ,并使用that 代替。但为什么?我将尝试解释它并证明所有良好实践和设计模式真正拯救了我们的生命!


注意:

  • 我不会解释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、命名

我们只能猜测这种方法的计算方法以及这些计算的输入究竟是什么。从这个类中提取计算算法真的很难。
风险:
在这种情况下最重要的是——浪费时间
                                                                      https://openclipart.org/image/800px/svg_to_png/4077/ernes-clessidra-hourglass.png
,如果我们将从业务部门获得询问以呈现算法细节,或者我们需要修改这段代码,我们需要花费很多时间来理解我们的Calculate方法的逻辑。如果我们不注释它或重构代码,下次我们/其他开发人员将花费同样的时间来弄清楚那里到底发生了什么。我们也可以在修改它时轻易搞错。

2、神奇数字
      https://openclipart.org/image/800px/svg_to_png/27057/silvia2k1-nr.png

在我们的示例中,类型变量表示——客户帐户的状态。你能猜到吗?If-else if语句可以选择折扣后如何计算产品价格。

现在我们不知道什么样的帐户是1,2,34。我们现在想象你必须改变为ValuableCustomer帐户提供折扣的算法你可以尝试从其余的代码中找出它——什么会花费你很长时间,但即使我们很容易犯错并修改BasicCustomer帐户的算法——像23这样的数字不是很具描述性。在我们的错误之后,客户将非常高兴,因为他们将获得有价值客户的折扣:)

3、不明显的错误

因为我们的代码非常脏且不可读,我们很容易错过非常重要的事情。想象一下,我们的系统中添加了一个新的客户帐户状态——GoldenCustomer。现在,我们的方法将返回0作为每种产品的最终价格,这些产品将从新类型的帐户中购买。为什么?因为如果我们的if-else if条件都不满足(将有未处理的帐户状态)方法总是会返回0.我们的老板不高兴——在有人意识到出了问题之前他卖了很多产品。

                                                                           https://openclipart.org/image/800px/svg_to_png/26625/johnny-automatic-angry-guy.png

4、不可读

我们都必须同意我们的代码非常难以理解。
难以理解=更多时间理解代码+增加错误风险。

5、神奇数字——又一次

我们知道像0.1,0.7,0.5那样的数字是什么意思?不,我们不知道,但如果我们是代码的所有者,我们就应该知道。
让我们假设您必须更改此行:
result =amount - 0.5m * amount)) - disc *amount - 0.5m * amount))

由于该方法完全不可读,因此您只需先将0.5更改为0.4,然后将第二个0.5保持原样。它可能是一个错误,但它也可以是一个完全正确的修改。这是因为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))

只有静态变量才有所不同——我们可以轻松地对这个变量进行参数化。

如果我们不会删除重复的代码,我们将遇到只执行任务的一部分的情况,因为我们不会看到我们必须以相同的方式进行更改,例如代码中的5个地方(重复)。以上逻辑正在计算我们系统中作为客户多年的折扣。因此,如果我们将在3个位置中的2个位置更改此逻辑,则我们的系统将变得不一致。

7、每个类都是多重责任

我们的方法至少有3个职责:
1.选择计算算法,

2.计算账户状态的折扣,

3.计算作为客户的年份的折扣。

它违反了单一责任原则。它带来了什么风险?如果我们需要修改这3个功能中的一个,它也会影响其他2个功能。这意味着它可能会破坏我们不想触及的功能。所以我们必须再次测试所有类——浪费时间 

 

重构...

在下面的9个步骤中,我将向您展示如何避免上述所有风险和不良做法,以实现干净、可维护和可单元测试的代码,这些代码可以像书一样阅读。 

I STEP——命名,命名,命名

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

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是什么意思,让我们用它做点什么吧!

II STEP——神奇数字

C#中避免神奇数字的技巧之一是用枚举替换它。我准备了AccountStatus  枚举来替换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;
  }
}

III STEP——更具可读性

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

我还将长单行算法划分为两个单独的行。现在把帐户状态的折扣的计算有顾客帐户的年度的折扣的计算分开了。

例如,行:
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;
  }
}

IV STEP——不明显的错误

我们终于得到了隐藏的bug

正如我之前提到的,我们的方法ApplyDiscount将返回0作为将从新类型帐户购买的每个产品的最终价格。伤心但真实......

我们该如何解决?通过抛出NotImplementedException

                   https://openclipart.org/image/800px/svg_to_png/133123/cybergedeon-AL-throwing-a-stone.png

你会想——这不是驱动开发的异常吗?不,不是!

当我们的方法将作为我们不支持的AccountStatus参数值获取时,我们希望立即注意到这个事实,并停止程序流程,不要在我们的系统中进行任何不可预测的操作。

这种情况绝不应该发生,所以如果发生这种情况,我们必须抛出异常。

如果没有条件满足,则下面的代码被修改为抛出NotImplementedException——switch-case语句的默认部分:

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;
  }
}

V STEP——让我们分析计算结果

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

  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)

VI STEP——摆脱神奇数字——另一种技巧

让我们看一下静态变量,它是帐户状态折扣算法的一部分:(static_discount_in_percentages/100)

具体实例:
0.1m 
0.3m 
0.5m

这些数字也很神奇——他们没有告诉我们任何关于他们自己的事情。

我们也有同样的情况,如果将“拥有账户的时间”转换为“忠诚的折扣”:
decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100;

数字5让我们的代码变得非常神秘。

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

我将使用另一种避免神奇字符串的技术——就是常量C#中的const关键字)。我强烈建议为常量创建一个静态类,以便将它放在我们应用程序的一个位置。

对于我们的示例,我创建了以下类:

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;
  }
}

我希望你会同意我们的方法现在更加自解释:)

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

        https://www.codeproject.com/KB/cs/1083348/extracted-png-image1.png

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

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

首先,我们必须创建2个扩展方法:

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;
  }
}

扩展方法非常好,可以使您的代码更简单,但在一天结束时仍然是静态类,可以使您的单元测试非常困难甚至不可能。因此,我们将在最后一步中摆脱它。我用它只是为了告诉你如何让我们的生活更轻松,但我不是他们的忠实粉丝。

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

那么让我们跳到下一步吧!

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

我们应该尽可能地编写简短的代码。更短的代码=更少的可能错误,更短的理解业务逻辑的时间。
让我们简化一下我们的例子吧。

我们可以很容易地注意到,我们对3种客户帐户有相同的方法调用:
.ApplyDiscountForTimeOfHavingAccount(timeOfHavingAccountInYears);

我们不能一次这样做吗?不,我们对未注册用户有例外,因为多年来作为注册客户的折扣对未注册的客户没有任何意义。是的,但是帐户有未注册用户的时间是什么时候?

——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语句之外移动这一行。好处——更少的代码!

STEP IX——高级——最后得到干净的代码

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

                                                           https://openclipart.org/image/800px/svg_to_png/177506/cleaner.png

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

这就是我们的代码在一天结束时的样子:

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);
  }
}

 首先我们摆脱了扩展方法(read:静态类),因为使用它们使得调用者类(DiscountManager)与扩展方法中的折扣算法紧密结合。如果我们想要对我们的ApplyDiscount方法进行单元测试,那将是不可能的,因为我们也会测试PriceExtensions类。 

为了避免它,我创建了DefaultLoyaltyDiscountCalculator类,其中包含ApplyDiscountForTimeOfHavingAccount扩展方法的逻辑,并隐藏它在抽象(read:接口)ILoyaltyDiscountCalculator背后的实现。现在,当我们想要测试我们的DiscountManager类时,我们将能够通过构造函数将实现ILoyaltyDiscountCalculator mock / fake对象注入到我们的DiscountManager类中,仅仅用于测试DiscountManager实现。我们在这里使用依赖注入设计模式
     https://openclipart.org/image/800px/svg_to_png/23811/pitr-Syringe-icon.png

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

下面在我们的DiscountManager类中使用划分为单独的类逻辑:
priceAfterDiscount = _loyaltyDiscountCalculator.ApplyDiscount(priceAfterDiscount, timeOfHavingAccountInYears);

为了计算帐户状态逻辑的折扣,我必须创建更复杂的内容。我们有两个想从DiscountManager移出的责任:

  1. 根据帐户状态使用哪种算法
  2. 特定算法计算的细节

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

    https://openclipart.org/image/800px/svg_to_png/23962/Anonymous-Factory.png

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

下面在我们的DiscountManager类中使用工厂:
priceAfterDiscount = _factory.GetAccountDiscountCalculator(accountStatus).ApplyDiscount(price);

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

第一个责任分开,让我们谈谈第二个。

我们来谈谈策略.....

     https://openclipart.org/image/800px/svg_to_png/200651/primary-package-games-strategy.png

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

在我们的示例中,我们现在有3个策略:
NotRegisteredDiscountCalculator 
SimpleCustomerDiscountCalculator 
MostValuableCustomerDiscountCalculator

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

它将允许我们的DiscountManager类在不知道其实现的情况下使用适当的策略。DiscountManager只知道返回的对象实现了包含方法ApplyDiscountIAccountDiscountCalculator接口。

NotRegisteredDiscountCalculatorSimpleCustomerDiscountCalculatorMostValuableCustomerDiscountCalculator类根据帐户状态包含正确算法的实现。由于我们的3个策略看起来相似,我们唯一可以做的就是为所有3个算法创建一个方法,并使用不同的参数从每个策略类调用它。因为它会使我们的示例变得更大,所以我没有决定那样做。 

好吧,总结一下,现在我们有一个干净可读的代码和我们所有的类都只有一个责任——只有一个理由去改变
1DiscountManager——管理代码流
2. DefaultLoyaltyDiscountCalculator ——忠诚折扣的计算
3. DefaultAccountDiscountCalculatorFactory ——决定计算帐户状态折扣的策略选择
4. NotRegisteredDiscountCalculator SimpleCustomerDiscountCalculatorMostValuableCustomerDiscountCalculator——计算帐户状态的折扣 

现在比较开始的方法:

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;
}

结论

本文提供的代码非常简化,可以更轻松地解释使用过的技术和模式。它显示了如何以肮脏的方式解决常见的编程问题,以及使用良好实践和设计模式以适当、干净的方式解决它的好处。

在我的工作经历中,我在这篇文章中看到过很多次被强调的坏习惯。它们显然存在于许多应用程序中,而不是像我的例子那样存在于一个类中,这使得查找它更加困难,因为它们隐藏在正确的代码之间。编写这种代码的人总是认为他们遵循保持简单和直接的规则。不幸的是,几乎总是系统正在成长并变得非常复杂。然后,这个简单的、不可扩展的代码中的每一个修改都是非常重要的,并且带来了巨大的风险。

请记住,您的代码将在生产环境中存在很长时间,并且会在每次业务需求更改时进行修改。因此,编写过于简单,无法使用的代码将很快产生严重后果。最后对开发人员很好,他们会自己维护你的代码:)

下一篇:C#坏习惯:通过不好的例子学习如何制作好的代码——第2部分 

 

原文地址:https://www.codeproject.com/Articles/1083348/Csharp-BAD-PRACTICES-Learn-how-to-make-a-good-code

猜你喜欢

转载自blog.csdn.net/mzl87/article/details/86556290