首页 > Python基础教程 >
-
C#教程之C# BAD PRACTICES: Learn how to make a good code by
自己的前言说明:
本文原作者:Radoslaw Sadowski,原文链接为:C# BAD PRACTICES: Learn how to make a good code by bad example。
本系列还有其他文章,后续将慢慢翻译。
引言:
我的名字叫Radoslaw Sadowski,我现在是一个微软技术开发人员。我从开始工作时就一直接触的微软技术.
在工作一年后,我看到的质量很差的代码的数量基本上都可以写成一整本书了。
这些经历让我变成了一个想要清洁代码的强迫症患者。
写这篇文章的目的是为了通过展示质量很差的类的例子来说明如何书写出干净的、可延伸的和可维护的代码。我会通过好的书写方式和设计模式来解释坏的代码带来的问题,以及替换他的好的解决方法。
第一部分是针对那些拥有C#基础知识的开发人员——我会展示一些常见的错误,然后再展示一些让代码变得可读性的方法与技巧。高级部分主要针对那些至少拥有设计模式概念的开发人员——我将会展示完全干净的、单元可测试的代码。
为了能够理解这篇文章你需要至少掌握以下两个部分的基本知识:
- C#语言
- 依赖注入、工厂设计模式、策略设计模式
本文中所涉及的例子都将会是现实中实实在在的具体的特性,而不是用装饰模式来做披萨或者用策略模式来做计算器这样的示例。
(ps解释:看过设计模式相关的书籍的人应该会知道很多这方面的书籍都是用这种例子,只是为了帮助读者理解设计模式)
因为我发现这种类型的产品不好用来解释,相反这些理论性的例子却是非常适合用来在本文中做解释的。
我们常常会听到说不要用这个,要用那个,但是却不知道这种替换的理由。今天我将会努力解释和证明那些好的书写习惯以及设计模式是真的是在拯救我们的开发生活!
提示:
- 在本文中我不会花时间来讲解C#的特性和涉及模式之类(我也解释不完),网上有很多关于这方面的好的理论的例子。我将集中讲述如何在我们日常工作中使用这些东西。
- 例子是一种比较容易的突出我们要说明的问题的方法,但是仅限于描述的问题——因为我发现当我在学习哪些包含着主要代码的例子时,我发现在理解文章的总体思想方面会有困难。
- 我不是说我文中说的方法是惟一的解决方式,我只是能保证这些方法将会是让你的代码变得更高质量的途径。
- 我并不关心下面这些代码的什么错误处理,日志记录等等。我要表述的只是用来解决日常编码一些问题的方法。
那就开始吧….
那些糟糕透了的类...
下面的例子是我们现实中的类:
1 public class Class1 2 { 3 public decimal Calculate(decimal amount, int type, int years) 4 { 5 decimal result = 0; 6 decimal disc = (years > 5) ? (decimal)5/100 : (decimal)years/100; 7 if (type == 1) 8 { 9 result = amount; 10 } 11 else if (type == 2) 12 { 13 result = (amount - (0.1m * amount)) - disc * (amount - (0.1m * amount)); 14 } 15 else if (type == 3) 16 { 17 result = (0.7m * amount) - disc * (0.7m * amount); 18 } 19 else if (type == 4) 20 { 21 result = (amount - (0.5m * amount)) - disc * (amount - (0.5m * amount)); 22 } 23 return result; 24 } 25 }
上面这个例子真的是一种非常差的书写方式。你能知道这个类是用来干嘛的么?这个东西是用来做一些奇怪的运算的么?我们文章就从他开始入手来讲解吧…
现在我来告诉你,刚刚那个类是用来当顾客在网上买东西的时候为他们计算对应折扣的折扣计算和管理的类。
-难以置信吧!
-可是这是真的!
这种写法真的是将难以阅读、难以维护和难以扩展这三种集合在一起了,而且拥有着太差的书写习惯和错误的模式。
除此之外还有其他什么问题么?
1.命名方式-从源代码中我们可以连蒙带猜估计出来这个计算方法和输出结果是什么。而且我们想要从这个类中提取计算算法将会是一件非常困难的事情。
这样带来的危害是:
最严重的问题是:浪费时间,
如果我们需要满足客户的商业咨询,要像他们展示算法细节,或者我们需要修改这段代码,这将花费我们很长的时间去理解我们的计算方法的逻辑。即使我们不记录他或重构代码,下次我们/其他开发人员再看这段代码的时候,还是需要花费同等的时间来研究这些代码是干嘛的。而且在修改的同时还容易出错,导致原来的计算全部出错。
2.魔法数字
在这个例子中type是变量,你能猜到它代表着客户账户的等级么?If-else if语句是用来实现如何选择计算出产品价格折扣的方法。
现在我们不知道什么样的账户是1,2,3或4。现在想象一下,当你不得不为了那些有价值的VIP客户改变他们的折扣计算方式的时候,你试着从那些代码中找出修改的方法---这个过程可能会花费你很长的时间不说,还很有可能犯错以至于修改那些基础的一般的客户的账户,毕竟像2或者3这些词语毫无描述性的。但是在我们犯错以后,那些一般的客户却很高兴,因为他们得到了VIP客户的折扣。:)
3.没有明显的bug
因为我们的代码质量很差,而且可读性非常差,所以我们可能轻易就忽略掉很多非常重要的事情。想象一下,现在突然在系统中增加一种新的客户类型-金卡用户,而在我们的系统中任何一种新的账户类型最后获得的价格将是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.简洁-不要反复做无用功
虽然第一眼看的时候不容易看出来,但是仔细研究一下就会发现:我们的代码里有很多重复的地方。例如:disc * (amount - (0.1m * amount));
而与之有同样效果的还有(只是变了一个参数而已):disc * (amount - (0.5m * amount))
在这两个算术中,唯一的区别就只是一个静态参数,而我们完全可以用一个可变的参数来替代。
如果我们不试着在写代码的时候从一直ctri+c,ctrl+v中摆脱出来,那我们将遇到的问题就是我们只能修改代码中的部分功能,因为我们不知道有多少地方需要修改。上面的逻辑是计算出在我们系统中每个客户对应年限获得的折扣,所以如果我们只是贸然修改两到三处,很容易造成其他地方的前后不一致。
7.每个类有着太多的复杂的责任区域
我们写的类至少背负了三个责任:
- 选择计算的运算法则
- 为每个不同状态的账户计算折扣率
- 根据每个客人的年限计算出对应的折扣率
这个违背了单一责任原则。那么这会带来什么危害呢?如果我们想要改变上诉3个特性中的两个,那就意味着可能会碰触到一些其他的我们并不想修改的特性。所以在修改的时候我们不得不重新测试所有的类,那么这就造成了很重的时间的浪费。
那就开始重构吧…
在接下来的9个步骤中我将向你展示我们如何避免上诉问题来构建一个干净的易维护,同时又方便单元测试的看起来一目了然的代码。
I:命名,命名,命名
恕我直言,这是代码中最重要的一步。我们只是修改方法/参数/变量这些的名字,而现在我们可以直观的了解到下面这个类代表什么意思。
1 public class DiscountManager 2 { 3 public decimal ApplyDiscount(decimal price, int accountStatus, int timeOfHavingAccountInYears) 4 { 5 decimal priceAfterDiscount = 0; 6 decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100; 7 if (accountStatus == 1) 8 { 9 priceAfterDiscount = price; 10 } 11 else if (accountStatus == 2) 12 { 13 priceAfterDiscount = (price - (0.1m * price)) - (discountForLoyaltyInPercentage * (price - (0.1m * price))); 14 } 15 else if (accountStatus == 3) 16 { 17 priceAfterDiscount = (0.7m * price) - (discountForLoyaltyInPercentage * (0.7m * price)); 18 } 19 else if (accountStatus == 4) 20 { 21 priceAfterDiscount = (price - (0.5m * price)) - (discountForLoyaltyInPercentage * (price - (0.5m * price))); 22 } 23 24 return priceAfterDiscount; 25 } 26 }
虽然如此,我们还是不理解1,2,3,4代表着什么,那就继续往下吧!
II:魔法数
在C#中避免出现不理解的魔法数的方法是通过枚举来替代。我通过枚举方法来替代在if-else if 语句中出现的代替账户状态的魔法数。
1 public enum AccountStatus 2 { 3 NotRegistered = 1, 4 SimpleCustomer = 2, 5 ValuableCustomer = 3, 6 MostValuableCustomer = 4 7 }
现在在看我们重构了的类,我们可以很容易的说出那个计算法则是用来根据不用状态来计算折扣率的。将账户状态弄混的几率就大幅度减少了。
1 public class DiscountManager 2 { 3 public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears) 4 { 5 decimal priceAfterDiscount = 0; 6 decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100; 7 8 if (accountStatus == AccountStatus.NotRegistered) 9 { 10 priceAfterDiscount = price; 11 } 12 else if (accountStatus == AccountStatus.SimpleCustomer) 13 { 14 priceAfterDiscount = (price - (0.1m * price)) - (discountForLoyaltyInPercentage * (price - (0.1m * price))); 15 } 16 else if (accountStatus == AccountStatus.ValuableCustomer) 17 { 18 priceAfterDiscount = (0.7m * price) - (discountForLoyaltyInPercentage * (0.7m * price)); 19 } 20 else if (accountStatus == AccountStatus.MostValuableCustomer) 21 { 22 priceAfterDiscount = (price - (0.5m * price)) - (discountForLoyaltyInPercentage * (price - (0.5m * price))); 23 } 24 return priceAfterDiscount; 25 } 26 }
III:更多的可读性
在这一步中我们将通过将if-else if 语句改为switch-case 语句,来增加文章的可读性。
同时,我也将一个很长的计算方法拆分为两句话来写。现在我们将“ 通过账户状态来计算折扣率”与“通过账户年限来计算折扣率”这两者分开来计算。
例如:priceAfterDiscount = (price - (0.5m * price)) - (discountForLoyaltyInPercentage * (price - (0.5m * price)));
我们将它重构为:priceAfterDiscount = (price - (0.5m * price));
priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
这就是修改后的代码:
1 public class DiscountManager 2 { 3 public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears) 4 { 5 decimal priceAfterDiscount = 0; 6 decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100; 7 switch (accountStatus) 8 { 9 case AccountStatus.NotRegistered: 10 priceAfterDiscount = price; 11 break; 12 case AccountStatus.SimpleCustomer: 13 priceAfterDiscount = (price - (0.1m * price)); 14 priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount); 15 break; 16 case AccountStatus.ValuableCustomer: 17 priceAfterDiscount = (0.7m * price); 18 priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount); 19 break; 20 case AccountStatus.MostValuableCustomer: 21 priceAfterDiscount = (price - (0.5m * price)); 22 priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount); 23 break; 24 } 25 return priceAfterDiscount; 26 } 27 }
IV:没有明显的bug
我们终于找到我们隐藏的bug了!
因为我刚刚提到的我们的方法中对于不适合的账户状态会在造成对于所有商品最后都返回0。虽然很不幸,但却是真的。
那我们该如何修复这个问题呢?那就只有通过没有错误提示了。
你是不是会想,这个会不会是开发的例外,应该不会被提交到错误提示中去?不,他会的!
当我们的方法通过获取账户状态作为参数的时候,我们并不想程序让我们不可预知的方向发展,造成不可预计的失误。
这种情况是绝对不允许出现的,所以我们必须通过抛出异常来防止这种情况。
下面的代码就是通过抛出异常后修改的以防止出现不满足条件的情况-修改方式是将抛出异常防止 switch-case语句中的default 句中。
1 public class DiscountManager 2 { 3 public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears) 4 { 5 decimal priceAfterDiscount = 0; 6 decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100; 7 switch (accountStatus) 8 { 9 case AccountStatus.NotRegistered: 10 priceAfterDiscount = price; 11 break; 12 case AccountStatus.SimpleCustomer: 13 priceAfterDiscount = (price - (0.1m * price)); 14 priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount); 15 break; 16 case AccountStatus.ValuableCustomer: 17 priceAfterDiscount = (0.7m * price); 18 priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount); 19 break; 20 case AccountStatus.MostValuableCustomer: 21 priceAfterDiscount = (price - (0.5m * price)); 22 priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount); 23 break; 24 default: 25 throw new NotImplementedException(); 26 } 27 return priceAfterDiscount; 28 } 29 }
V:分析计算方法
在我们的例子中我们有两个定义给客户的折扣率的标准:
- 账户状态;
- 账户在我们系统中存在的年限
对于年限的计算折扣率的方法,所有的计算方法都有点类似:
(discountForLoyaltyInPercentage * priceAfterDiscount)
当然,也还是存在例外的:0.7m * price
所以我们把这个改成这样:price - (0.3m * price)
1 public class DiscountManager 2 { 3 public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears) 4 { 5 decimal priceAfterDiscount = 0; 6 decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100; 7 switch (accountStatus) 8 { 9 case AccountStatus.NotRegistered: 10 priceAfterDiscount = price; 11 break; 12 case AccountStatus.SimpleCustomer: 13 priceAfterDiscount = (price - (0.1m * price)); 14 priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount); 15 break; 16 case AccountStatus.ValuableCustomer: 17 priceAfterDiscount = (price - (0.3m * price)); 18 priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount); 19 break; 20 case AccountStatus.MostValuableCustomer: 21 priceAfterDiscount = (price - (0.5m * price)); 22 priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount); 23 break; 24 default: 25 throw new NotImplementedException(); 26 } 27 return priceAfterDiscount; 28 } 29 }
现在我们将整理所有通过账户状态的计算方法改为同一种格式:price - ((static_discount_in_percentages/100) * price)
VI:通过其他方式再摆脱魔法数
接下来让我们的目光放在通过账户状态计算折扣率的计算方法中的静态变量:(static_discount_in_percentages/100)
然后带入下面数字距离试试:0.1m,0.3m,0.5m
这些数字其实也是一种类型的魔法数-他们也没有直接告诉我们他们代表着什么。
我们也有同样的情况,比如将“有账户的时间”折价为“忠诚折扣”。