首先,良好的代码和劣质的代码都是可以编译的代码。
其次,不论良好的代码还是劣质的代码都有其成因。
良好的代码 | 劣质的代码 |
合理的缩进 | 混乱的缩进 |
有意义的注释 | 解释显而易见的代码 |
API文档注释 | 解释低质量的代码,将代码注释掉 |
使用命名空间合理组织代码 | 命名空间组织混乱 |
合理的命名规则 | 混乱的命名规则 |
一个类执行一种任务 | 一个类执行多种任务 |
一个方法做一件事情 | 一个方法做多件事情 |
方法的代码少于10行,通常小于4行 | 方法的代码大于10行 |
方法的参数不多于两个 | 方法的参数大于两个 |
合理使用异常 | 使用异常控制程序的执行流程 |
代码可读性强 | 代码可读性弱 |
代码耦合程度低 | 代码耦合紧密 |
高内聚的代码 | 低内聚的代码 |
对象会被恰当销毁 | 遗留对象 |
避免使用Finalize()方法 | 使用Finalize()方法 |
合理地进行抽象 | 代码过度设计 |
在大型类中使用#region进行区域划分 | 大型类中缺少区域划分 |
封装并隐藏信息 | 直接暴露信息 |
面向对象的代码 | 面条式的代码 |
设计模式 | 设计反模式 |
1. 劣质的代码
混乱的缩进
混乱的缩进会令代码难以阅读,在方法过长时尤为如此。
为了提高代码的可读性,需要进行合理的缩进。
混乱的缩进会凌然难以区分代码块之间的归属。
Visual Studio 默认会在括号或花括号闭合时正确地格式化并缩进代码。
但是这种格式化功能在代码包含异常情况时并非总是正确的,因此不正确的格式化往往能够引起你的注意。
但是如果使用普遍的文本编辑器,你就只能手动格式化代码了。
修正错误的缩进是一个费时的操作吗,花费大量编程时间来弥补这种易于避免的错误往往令人沮丧。
请看如下代码:
public void DoSomething()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Hello World");
}
}
上述代码虽然格式不佳但终究还是能够阅读的。
但是随着代码行数的增加,可读性也会随之下降。
当缩进不佳时很容易发生遗漏闭合括号的情况。
由于不容易分辨到底是哪个代码块遗漏了括号,因此要找到遗漏括号所在的位置就变得更难了。
解释显而易见的代码
我经常见到程序员对着一些显而易见的注释一筹莫展,也不止一次地在编程讨论中听程序员宣传他们如何讨厌代码注释。
它们认为,代码本身就应该有自解释能力。
我非常理解它们的感受。
如果能像读一本书那样读一段没有注释的代码,那么这段代码一定非常优秀。
而在字符串类型的变量声明后加上//string注释就显得很多余了。
请看以下范例:
public int _value; //This is used for storing integer value
由于变量的类型为int,因此其值必然为整数。
在这种地方继续用注释进行说明是没有必要的。
它不但会浪费时间和精力,还会把代码弄得一团糟。
解释低质量的代码
即使是工期紧张也不要做这种注释://我知道这段代码不怎么样但是至少它可以工作
这不但却反专业精神也会令其他程序员不满。
如果你真的需要尽快做出成果,那么可以创建一个重构标记,并将这个标记作为TODO注释的一部分。
例如://TODO:PBI23154 REFACTOR CODE TO MEET COMPANY CODING PRACTICES。
之后不论是你还是其他处理技术债的同事就可以从产品待办项中挑选这项任务并完成代码重构。
将代码注释掉
在尝试过程中将代码注释掉无可厚非,但是如果决定保留其他代码而放弃注释掉的代码,则最好在检入代码之前删除注释掉的代码。
有一两条注释掉的代码可能还不会太糟。
但是如果注释掉的代码过多,不但会分散注意力,使代码更难维护,甚至还会造成混淆。
/* No longer used as has been replaced by DoSomething()
public void IDidSomething()
{
// ...implementation...
}
*/
保留这段注释是没有必要的。
如果它已经被其他代码替代了,那么请删除它。
如果使用了版本控制系统,则可以浏览这个文件的历史并在需要时将方法找回。
命名空间组织混乱
当使用命名空间组织代码时,务必避免将无关的代码放置在命名空间中。
这将会令人难以找到甚至无法找到所需的代码,在规模庞大的代码库中尤为如此。
例如:
/// <summary>
/// Example of bad organisation.
/// </summary>
namespace MyProject.TextFileMonitor
{
public class Program { }
public class DateTime { }
public class FileMonitorService { }
public class Cryptography { }
}
上例所有的类位于同一个命名空间下,而将其划分在如下三个命名空间会更加合理;
- MyProject.TextFileMonitor.Core:该命名空间存放所有可以作为服务的类。例如:DateTime类。
- MyProject.TextFileMonitor.Services:该命名空间存放所有可以作为服务的类。例如:FileMonitorService。
- MyProject.TextFileMonitor.Security:该命名空间存放与安全相关的类。例如范例中的Cryptography类
混乱的命名规则
在使用VisaulBasic6编程的那个年代通常使用匈牙利命名法。
而Visual Basic .NET中却无须再使用匈牙利命名法了。
相反,匈牙利命名法会令代码变得丑陋。
因此不要再使用lblName、txtName和btnSave这命名方式了,请使用NameLabel、NameTextBox和SaveButton这种现代命名方式吧。
使用晦涩难懂与言不由衷的命名会令代码阅读变得异常艰难。
inri原来是Human Resources Index(人力资源索引),而且它还是一个整数类型,这里肯定想不到吧。
同时请避免使用诸如mystring、myint和mymethod这类命名,因为这些命名无法表达实际含义。
不要在单词之间加入下划线,例如Bad_Programmer。
它会给开发人员带来视觉压力并使代码更难阅读。
移除下划线就好了。
不要在类级别和方法级别的变量名上使用相同的命名规则,否则将难以区分变量的作用域。
推荐使用驼峰命名法为变量命名,例如alienSpawn;
使用Pascal命名法为方法、类、结构体和接口命名,例如EnmySpawnGenerator。
在成员变量(在类的构造器和方法之外定义的变量)的名称前添加下划线前缀可以使我们轻易地区分局部变量(在构造器或方法中的变量)和成员变量。
我在工作中就会这种规则,它效果良好而且为程序员所接受。
一个类执行多种任务
我曾经在工作中迷失在拥有太多层缩进、做了太多事情的方法中,其中的逻辑排列令人难以驾驭。
我确定如果能够将其划分为不同的方法,就可以明显缩减原有方法的大小。
以下例子中的方法接收一个字符串,并将其加密和解密。
这个方法很长,这样就更容易说明为何方法要保持短小:
public string security(string plainText)
{
try
{
byte[] encrypted;
using (AesManaged aes = new AesManaged())
{
ICryptoTransform encryptor = aes.CreateEncryptor(Key, IV);
using (MemoryStream ms = new MemoryStream())
using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter sw = new StreamWriter(cs))
sw.Write(plainText);
encrypted = ms.ToArray();
}
}
Console.WriteLine($"Encrypted data: {System.Text.Encoding.UTF8.GetString(encrypted)}");
using (AesManaged aesm = new AesManaged())
{
ICryptoTransform decryptor = aesm.CreateDecryptor(Key, IV);
using (MemoryStream ms = new MemoryStream(encrypted))
{
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
{
using (StreamReader reader = new StreamReader(cs))
plainText = reader.ReadToEnd();
}
}
}
Console.WriteLine($"Decrypted data: {plainText}");
}
catch (Exception exp)
{
Console.WriteLine(exp.Message);
}
Console.ReadKey();
return plainText;
}
上述方法包含超过10行代码,难以阅读。
此外,该方法有多于一种职责。
我们可以将上述代码分割为两个方法,每一个执行一种任务。
一个可以进行字符串加密操作,而另外一个进行字符串的解密操作。
方法代码大于10行
过长的方法不易阅读与理解。
并可能产生不易觉察的缺陷。
过长的方法的另一个问题是容易偏离方法原本的目标。
如果方法代码还被注释或区域分割为若干部分,这些弊端就更为显著。
如果必须上下滚动才能浏览方法的全貌,则这个方法应该是过长了。
这会给程序员阅读代码造成压力并产生误解,进而在修改该方法时可能破坏原因代码或曲解代码意图。
方法应当尽可能短小,但这需要加以练习,否则可能会将一个小方法过分细分。
保持平衡的关键在于确保方法的意图明确,实现整洁。
方法的参数大于两个
若方法参数很多则会稍显笨重,这不但不利于阅读,而且容易搞错参数的值从而破坏类型安全性。
方法的参数越多则参数的排列方式就越多,因而测试起来也越复杂,更容易丢失测试用例并造成产品的缺陷。
使用异常控制程序的执行流程
使用异常来控制程序流程容易隐藏代码的意图,导致意料之外的结果。
事实上,如果在编写代码的时候就预期代码会抛出一种或多种异常很有可能意味着设计上的问题。
使用业务规则异常(Business Rule Exception,BRE)来控制程序流程就是一个典型情况。
例如,方法在某些异常发生时会执行特定动作,即程序的流程会由于是否存在异常而确定。
而更好的方式是使用语言本身提供的结构来验证布尔值。
以下代码展示了使用BRE控制程序流程的做法:
public void BreFlowControlExample(BusinessRuleException bre)
{
switch (bre.Message)
{
case "OutOfAcceptableRange":
DoOutOfAcceptableRangeWork();
break;
default:
DoInAcceptableRangeWork();
break;
}
}
BreFlowControlExample()方法接收BusinessRuleException类的参数,并根据异常中消息的内容来决定应用调用DoOutOfAcceptableRangeWork()还是DoInAcceptableRangeWork()方法。
更好的做法是使用布尔逻辑来控制流程。
例如下面的BreFlowControlExample()方法:
public void BetterFlowControlExample(bool isInAcceptableRange)
{
if (isInAcceptableRange)
DoInAcceptableRangeWork();
else
DoOutOfAcceptableRangeWork();
}
以上方法接收一个布尔值,并使用该布尔值判断采取哪一条执行路径。
如果isInAcceptableRange满足判断条件,则调用DoInAcceptableRangeWork()方法;
反之,将调用DoOutOfAcceptableRangeWork()方法。
代码可读性弱
类似千层面或者意大利面的代码是难以阅读与理解的。
命名不当的方法可以掩盖其原意,也同样令然烦恼。
加上如果方法还很长,且关联方法被多个不相关的方法分割就更难以理解了。
千层面代码,指一般所说的间接的、引用抽象层级的代码。
这种引用指名称的引用而非动作的引用。
在面向对象编程OOP中,层的使用很常见,并通常都有好的效果。
但是间接引用越多,代码就越复杂。
此类代码会令项目上新程序员了解代码的过程越发艰难。
因此,维持间接性和易理解性之间的平衡就显得尤为重要。
而意大利面代码,指那些杂乱无章的低内聚紧耦合的代码。
这样的代码难以维护、重构、扩展和重新设计。
从积极的方面说,由于这种程序往往更加过程化,因此也许更易于阅读和模仿。