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






4.82/5 (492投票s)
本文旨在通过一个糟糕的类示例,展示如何编写干净、可扩展且可维护的代码。
引言
我叫 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` 类,负责在在线商店计算客户购买产品时的折扣。
- 怎么会?真的吗?
- 很不幸,是的!
它完全难以阅读、维护、扩展,并且使用了许多糟糕的实践和反模式。
这里究竟有什么问题?
-
命名——我们只能猜测这个方法计算什么,以及计算的输入究竟是什么。很难从这个类中提取计算算法。
风险
在这种情况下,最重要的事情是——浪费时间。如果我们收到业务部门关于展示算法细节的查询,或者我们需要修改这段代码,我们需要花费很长时间才能理解我们 `Calculate` 方法的逻辑。如果我们不写文档或重构代码,下次我们/其他开发人员将花费同样的时间来弄清楚那里到底发生了什么。我们也很容易在修改时出错。
-
魔法数字
在我们的例子中,`type` 变量代表客户账户的状态。你能猜到吗?`if`-`else if` 语句决定了折扣后产品的价格如何计算。
现在我们不知道 1、2、3 或 4 是哪种账户类型。现在想象一下,您需要更改 `ValuableCustomer` 账户的折扣算法。您可以尝试从剩余的代码中推断——这需要很长时间,但即使如此,我们也可能很容易犯错,并修改 `BasicCustomer` 账户的算法——数字 2 或 3 并不具有描述性。在我们的错误之后,客户会很高兴,因为他们会获得尊贵客户的折扣。:) -
不明显的错误
因为我们的代码非常混乱且难以阅读,我们很容易错过非常重要的事情。想象一下,我们的系统中添加了一个新的客户账户状态——`GoldenCustomer`。现在,我们的方法将返回 `0` 作为这种新类型账户购买的每件产品的最终价格。为什么?因为如果我们的任何 `if`-`else if` 条件都不满足(存在未处理的账户状态),方法将始终返回 `0`。我们的老板不高兴——他在有人意识到不对劲之前,卖了很多免费产品。
-
难以阅读
我们都必须承认,我们的代码极其难以阅读。
难以阅读 = 更多理解代码的时间 + 增加犯错的风险。 -
魔法数字——再次
我们知道 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 是什么鬼?这是客户因忠诚度而获得的最大折扣百分比。你能猜到吗?
-
DRY – 不要重复自己
第一眼看不出来,但我们的方法中有许多地方的代码是重复的。
例如disc * (amount - (0.1m * amount));
与……逻辑相同
disc * (amount - (0.5m * amount))
只有一个 `static` 变量在起作用——我们可以轻松地参数化这个变量。
如果我们不消除重复的代码,我们会遇到这样的情况:我们只完成任务的一部分,因为我们没有看到需要在代码的 5 个地方以相同的方式进行更改。上面的逻辑是计算客户在我们系统中的服务年限折扣。因此,如果我们只在 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;
}
}
第五步——让我们分析计算
在我们的示例中,客户的折扣有两个标准:
- 账户状态
- 客户在我们系统中的账户年限。
所有计算客户服务年限折扣的算法看起来都相似
(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` 中移出:
- 根据账户状态使用哪种算法
- 特定算法计算的详细信息
为了移出第一个责任,我创建了一个工厂类(`DefaultAccountDiscountCalculatorFactory`),它是工厂方法设计模式的实现,并将其隐藏在抽象——`IAccountDiscountCalculatorFactory`——之后。
我们的工厂将决定选择哪种折扣算法。最后,我们通过构造函数使用依赖注入设计模式将工厂注入到 `DiscountManager` 类中。
下面是 `DiscountManager` 类中使用工厂的情况。
priceAfterDiscount = _factory.GetAccountDiscountCalculator(accountStatus).ApplyDiscount(price);
上面的行将返回适用于特定账户状态的正确策略,并调用其上的 `ApplyDiscount` 方法。
第一个责任已经划分,所以让我们谈谈第二个。
让我们来谈谈策略...
由于每个账户状态的折扣算法可能不同,我们将不得不使用不同的策略来实现它。这是使用策略设计模式的绝佳机会!
在我们的示例中,我们现在有三个策略:
NotRegisteredDiscountCalculator
SimpleCustomerDiscountCalculator
MostValuableCustomerDiscountCalculator
它们包含了特定折扣算法的实现,并隐藏在抽象
IAccountDiscountCalculator
.
之后。这将允许我们的 `DiscountManager` 类在不了解其实现的情况下使用正确的策略。`DiscountManager` 只知道返回的对象实现了 `IAccountDiscountCalculator` 接口,该接口包含 `ApplyDiscount` 方法。
`NotRegisteredDiscountCalculator`、`SimpleCustomerDiscountCalculator`、`MostValuableCustomerDiscountCalculator` 类包含了根据账户状态的正确算法实现。由于我们的三个策略看起来很相似,我们能做的更多的是为所有三个算法创建一个方法,并从每个策略类中用不同的参数调用它。由于这会使我们的示例过大,我没有决定这样做。
好了,现在总结一下,我们有了干净易读的代码,并且我们所有的类都只有一个职责——只有一个改变的理由。
- `DiscountManager` – 管理代码流程
- `DefaultLoyaltyDiscountCalculator` – 计算忠诚度折扣
- `DefaultAccountDiscountCalculatorFactory` – 决定选择哪种账户状态折扣计算策略
- `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 日:初始版本