命名规范

代码之所以优美是因为撲写它们的人经过长期的锻炼而形成了良好的编码规范及习惯。每一个刚入行的程序员都应该会从公司内部得到-一个编码规范文档,建议选取其中的精要打印出来,放在案头以备时刻查阅。

不过,编码规范也是时刻在变的,十年前我们遵循匈牙利命名法,而现在我们被告知,不要对名字本身进行注释,要让命名看上去更像自然语言。规范之所以会演变,正是因为它始终被全体程序员关注着,大家总是在追求更好的标准。记住:没有规矩,不成方圆。

建议122 : 以Company.Component为命名空间命名

建议以<Company>.<Component>为程序集命名,比如Microsoft. Windows.Design.

另外一种有效并且肯定是唯一的表示命名空间的方式是使用域名。假设我们的域名是www.microsoft.com,那么命名空间应该命名为Com.Microsoft.<Component>。使用域名命名自己的程序的方法在Java世界中一直很流行,现在不妨把这种习惯带到.NET世界中来。如果是个人在开发软件,则更建议采用这种方式。去申请一个城名吧,这很有意义。

建议123 : 程序集不必与命名空间同名

程序集一般会和命名空间同名,但这并不是必需的。事实上,不同名的程序集和命名空间是很常见的。

建议124 : 考虑在命名空间中使用复数

如果有一组功能相近的类型被分组到了同一个命名空间下,可以考虑为命名空间使用复数。

建议125 : 避免用FCL的类型名称命名自己的类型

应该尽量避免在可见的范围内命名重复的类型,尤其是要避免与FCL中的类型重名。当然,这很大程度上依赖于我们对FCL类型的熟悉程度。如果对某个类型的命名没有把握,应当首先查询一下MSDN。记住,如果出现疑问,MSDN是释疑的首选位置。

建议126 : 用名词和名词组给类型命名

类型是什么?面向对象方面的先驱者会告诉我们,类型对应着现实世界中的实际对象。对象在语言学中意味它是一个名词。所以,类型也应该以名词或名词组去命名。类型定义了属性和行为。虽然它包含行为,但不是行为本身。

建议127 : 用形容词组给接口命名

接口规范的是“Can do”,也就是说它规范的是类型可以具有哪些行为。所以,接口的命名应该是一个形容词组,如:

IDisposable表示类型可以被释放。IEnumerable表示类型含有Items,可以被迭代。

正是因为接口表示的是类型的行为,所以从语义上可以让类型继承多个接口.

建议128 : 考虑让派生类的名字以基类名字作为后缀

派生类的名字可以考虑以基类名字作为后缀。这带来的好处是,从类型的名字上我们就知道它包含在哪一个继承体系中。

Exception及其子类就是这样-一个典型的例子。从第5章我们知道,所有的异常类都应该继承自System. Exception,而所有的异常类也应该命名为CustomedException。

建议129 : 泛型类型参数要以T作为前缀

作为一种约定,泛型类型的参数命名要以T作为前缀。如委托声明:Action<T1, T2>

其中,泛型类型参数的命名不应该处理成:Action<Arg1,Arg2>

当然,这仅仅是约定的一种习惯,如果使用第二种命名方式,编译器并不会报错,但是作为调用者,也许不能立刻意识到这里是一个泛型类型参数。这个问题在为类型指定泛型的时候尤其明显,因为为类型指定泛型类型参数的声明不会出现在公开的接口中.

不应该为泛型指定一个模棱两可的命名。记住,只要是泛型,就应该以T作为前缀命名。

建议130 : 以复数命名枚举类型,以单数命名枚举元素

枚举类型应该具有复数形式,它表达的是将一组 相关元素组合起来的语义。

建议131 : 用PascalCasing命名公开元素

开放给调用者的属性、字段和方法都应该采用PascalCasing命名方式,比如:

public string FirstName;

public string LastName;

它主要的特点是将描述变量作用所有单词的首字母大写,然后直接连接起来,单词之间没有连接符.

建议132 : 考虑用类名作为属性名

一般来说,如果属性对应一个类型,则应该直接用类型名命名属性名。当然,除非我们的类型当中有多个Company类型的属性,这样,就必须为我们的属性重构成不同的命名,例如:

public Company Company { get; set; }

public Company SecondCompany { get; set;}

建议133 : 用camelCasing命名私有字段和局部变量

私有字段和局部变量只对本类型负责,它们在命名方式也采用和开放的属性及字段不同的方法。camelCasing 很适合这类命名。camelCasing和PascalCasing的区别是它的首字母是小写的。之所以要采用两种不同的命名规则,是为了便于开发者自己快速地区分它们。

建议134 : 有条件地使用前缀

在.NET的设计规范中,不建议使用前缀。但是,即便是微软自己提供的FCL 4.0版本,依然广泛地使用着前缀。

最典型的前缀是使用m_,这种命名一方面是考虑到历史沿革中的习惯问题:另一方面,也许我们确实有必要这么做。

在一个不是很庞大的类型中,我们确实不应该使用任何前缀。各类设计规范也总建议我们保持一个娇小的类型,但往往事与愿违,大类型常常存在。以Task为例,使用微软提供的源码查看,可知它有2202行代码。在这种类型中,如果不使用前缀,我们将很难区分一个类型是实例变量还是静态变量,或者是一个const变量。

最常见的做法:

  • 前缀m_,表示这是一个实例变量。
  • 前缀s_,表示这是一个静态变量。

注意,有时候,如果类型中只有实例变量或者只有静态变量,我们也直接使用前缀,以区别该变量不是一个局部变量。

而const变量则常常使用名词加下划线的表示方法,如:

internal const int TASK_ STATE_ CANCELED = 0x400000 ;

记住,前缀仅限于此,匈牙利命名法中的其他规则(如用类型名做前缀)是绝对要禁止的。

static int s_price;
int m_price;
int _price;
const int BASED_PRICE=1000;

int int_price;在这个例子中,开发者尝试为price指定一个前缀int,试图通过命名指出变量是int

建议135 : 考虑使用肯定性的短语命名布尔属性

布尔值无非就是True和False,所以,应该用肯定性的短语来表示它,例如,以Is、Can、Has 作为前缀。

建议136 : 优先使用后缀表示已有类型的新版本

加后缀在某些情况下是很奇怪的形式,我们都不愿意看到OrderProcessor2这样的类型。但是,有的时候仍旧有必要这样做。最典型的是FCL中关于数字证书操作的X509Certificate和X509Certificate2这两个类型。

记住,当不得不出现一个类型的新版本时,应该加后缀,而不是前缀。这不仅仅是习惯问题,这还有助于Intellisense发现这个新版本的类。

建议137 : 委托和事件类型应添加上级后缀

委托类型本身是一个类,所以,这满足“建议128:考虑让派生类的名字以基类名字作为后缀”。事件类型是一类特殊的委托,所以事件类型也遵循本建议。

如果用传统的方式,我们可能看不出来这些类型是有基类的,但是委托和事件的关键字delegate和event已经指明了后面类型的基类是Delegate。委托按照委托类型的作用又单纯分为以Delegate结尾和CallBack结尾,我们在声明委托类型的时候一定要注意区分这一点。如果委托用于回调性质,则使用CallBack结尾。

建议138 : 事件和委托变量使用动词或形容词短语命名

在建议137中,首先确定了事件和委托类型的命名,那么本建议就是关于事件和委托变量的命名。事件和委托的使用场景是调用某个方法,只不过这个方法由调用者赋值。这决定了对应的变量应该以动词或形容词短语命名。

建议139:事件处理器命名采用组合方式

阐述完毕委托和事件类型,委托和事件变量,最后就是委托和事件处理器的命名。所谓事件处理器,就是实际被委托执行的那个方法。

事件变量所属对象+下划线+事件变量名

这种命名方式用于以注册的方法(即“+=”操作符)添加的事件处理器。如果我们要为委托或委托中的回调编写处理器,则应该使用如下的命名规则:

委托变量所属对象+ On+委托变量名

代码整洁

建议140 : 使用默认的访问修饰符

代码整洁的要求之一,就是尽量减少代码,这里,我们从使用默认的访向限制符开始。

类型成员的修饰符默认是private

类或接口的默认修饰符为internal

建议141 : 不知道该不该用大括号时,就用

如if条件语句下只有一行语句,要不要使用大括号? 答案是:建议使用。没有不使用的道理,一个括号不会增加多少代码,但是却让代码看上去增加了一致性。

建议142 : 总是提供有意义的命名

除非有特殊原因,否则永远不要为自己的代码提供无意义的命名。害怕需要过长的命名才能提供足够的意义?不要怕,其实我们更介意的是在读代码的时候出现一个iTemp。

int i这样的命名方式只应该出现在循环中( 如for循环),除此之外,我们找不到任何理由在代码的其他地方出现这样的无意义命名。

建议143 : 方法抽象级别应在同一层次

方法抽象级别应在同-一个层次上,我们来看下面的代码:

class SampleClass
{
   public void Init()
   {
      //本地初始化代码1
      //本地初始化代码2
      RemoteInit() ;
   }
   void RemoteInit()
   {
      //远程初始化代码1
      //远程初始化代码2
   }
}

Init方法本意要完成初始化动作,而初始化包括本地初始化和远程初始化。在这段代码中,Init 方法内部代码的组织结构是本地初始化代码直接运行在方法内部,而远程初始化代码却被封装为一个方法在这里被调用。这显然是不妥当的,因为本地初始化和远程初始化的地方是相当的。作为方法来讲,如果远程初始化代码作为方法存在,则本地初始化代码也应该作为方法存在。

所以,上面的代码应该重构为:

class SampleClass
{
   public void Init()
   {
      LocalInit();
      RemoteInit();
   }
   void LocalInit()
   {
      //本地初始化代码1
      //本地初始化代码2
   }
   void RemoteInit()
   {
      //远程初始化代码1
      //远程初始化代码2
   }
}

建议144 : 一个方法只做一件事

“单一职责原则”(SRP)要求每个类型只负责- -件事情。我们将此概念扩展到方法上,就变成了:一个方法只做一件事。

什么样的代码才能“做同一件事”?回顾上一个建议中的代码,其中,Locallnit方法和RemoteInit方法是两件事情,但是在同一抽象层次上,在类型这个层次对外又可以将其归并为“初始化”这一件事情上。所以,“同一件事”要看抽象所处的地位。

建议145 : 避免过长的方法和过长的类

如果违反“一个方法只做一件事”及类型的“单一职责原则”,往往会产生过长的方法和过长的类。

如果方法过长,意味着可以站在更高的层次上重构出若千个更小的方法。那么,有没有具体的指标提示方法是否过长?有,是以行数做指标的,有人建议一个方法不要超过10行,有人建议不要超过30行。当然,这没有唯一标准,在我看来,如果一个方法在Visual Studio中需要滚屏才能阅读完,那么就肯定有些过长了,必须想法重构它。

对于类型,除非有非常特殊的理由,类型的代码不要超过300行。如果行数太多了,则要考虑能否重构。

建议146 : 只对外公布必要的操作

那些不是很有必要公开的方法和属性,private之。如果需要公开的方法和属性超过9个,在Visual Studio默认的设置下,就需要滚屏才能显示在Intellisense中.

建议147 : 重构多个相关属性为一个类

若存在多个相关属性,就该考虑是否将其重构为一个类。

class Person
{
   public string Address { get; set;}
   public int ZipCode { get; set; }
   public string Mobile{ get; set;}
   public string Hotmail { get; set;}
   //略
}

上面代码中的这四个属性全部跟联系方式有关,所以,我们应该重构一个Contact类型

class Person
{
   Contact Contact{ get; set;}
}
class Contact
{
   public string Address { get; set;}
   public int ZipCode { get; set; }
   public string Mobile{ get; set;}
   public string Hotmail { get; set;}
   //略
}

记住,类型中的相关属性超过3个,就可以考虑将其重构为一个类了。

建议148:不重复代码

如果发现重复的代码,则意味着我们需要整顿一下,再继续前进。重复代码让我们的软件行为变得不一致。举例来说,如果存在两处相同的加密代码.结果在某一天,我们发现加密代码有个小Bug,然后修改之,却又忘记了角落里的某处存在着一份相同的代码,那么这个Bug就有会被隐藏起来。

建议149 : 使用表驱动法避免过长的if 和switch分支

随着代码变得复杂,我们很容易被过长的if和switch分支困扰。本书中反复用到的一个枚举类型是Week,代码如下所示:

如果要把Week的元素值用中文输出,简单而丑陋的方法也许是封装一个GetChineseWeek

static string GetChineseWeek (Week week)
{
   case Week.Monday:
     return "星期一"
     ...;
   default:
     throw new ArgumentOutOfRangeException("week","星期值超出范围");
}

之所以说这种方法是丑陋的,原因在于两点:

  • 1)分支太长了,而且出现了重复代码。
  • 2)不利于扩展。如果出现星期八、星期九怎么办?当然,星期制已经是固定的了,应该不会出现扩展的情况。但是,换种情景来考虑,假设我们正在渲染动画怎么办?谁知道下一秒美工会提交我多少个动画呢?

一种解决方案是使用多态,它很好地符合了“开闭”原则。如果增加条件分支,不必修改原有代码,直接增加子类就可以了。利用多态来避免多分支,这里暂且不表,本建议要采用的是 “表驱动法” 。可以把表驱动法简单理解为查字典。代码如下所示:

static void Main(string[] args)
{
   Console.WriteLine(GetChineseWeekInTable(Week.Friday));
}
static string GetChineseWeekInTable(Week week)
{
   string[] chineseWeek = {"星期一","星期二","星期三","星期四","星期五","星期六","星期日"};
   return chineseWeek[(int)week] ;
}

这是一种按照索引值驱动的表驱动法。枚举元素所代表的整型值,很容易和字符串数组索引组合起来了,用两行语句就可以完美地替代原先的GetChineseWeek方法。但是,这种方法有局限性,完成的功能也很有效,如果将需求换成:星期一Mike打扫卫生、星期二Rose清理衣柜、星期三Mike和Rose没事可以吵吵架、星期四Rose要去Shopin…..也就是说需求由静态属性变成了动态行为,那么事情就变得复杂了。

遇上这种情况,我们可能会想到使用多态,在这里仍然使用表驱动法并加上一点反射来实现这类动态的行为,代码如下所示:

static void Main(string[] args)
{
   SampleClass sample = new Samp1eClass() ;
   var addMethod = typeof(SampleClass).GetMethod(ActionInTable(Week.Monday));
   addMethod.Invoke (sample, null) ;
}

static string ActionInTable (Week week)
{
   string[] methods = { "Cleaning", "CleanCloset""Quarre1", " shopping"};
   return methods [(int)week] ;
}

class SampleClass
{
   public void Cleaning()
   {
      Console. WriteLine("打扫");
   }
   public void CleanCloset()
   {
      Console. WriteLine("清理表柜");
   }
}

表驱动法是一种设计思路, 也可以称为模式。在实际编码中,不应局限于用索引去驱动行为,而应当根据实际情况灵活运用。

建议150 : 使用匿名方法、Lambda表达式代替方法

方法体如果过小(如小于3行),专门为此定义一个方法就会显得过于烦琐。

我们还有更好的方法简化匿名方法,那就是Lambda表达式。Lambda 表达式由符号“=>”连接(读作“goes to”),符号左边是参数列表,右边是方法体。Lambda表达式进一步简化了匿名方法的语法.

建议151 : 使用事件访问器替换公开的事件成员变量

事件访问器包含两部分内容:添加访问器和删除访问器。如果涉及公开的事件字段,应该始终使用事件访问器。代码如下所示:

class SampleClass
{
   EventHandlerList events = new EventHandlerList();
   public event EventHandler Click
   {
      add
      {
         events.AddHandler(nu11, value);
      }
      remove
      {
         events.RemoveHandler(nu11,value);
      }
   }
}

使用事件访问器的好处是,提供对赋值更多细粒度的控制。这就好比应该使用属性而不使用字段一样。访问器本质上是方法,而成员变量,它仅仅是成员变量。

建议152 : 最少,甚至是不要注释

以往,我们在代码中不写上几行注释,就会被认为是种不负责任的态度。现在,这种观点正在改变。试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值。

即便再详细的注释也不能优化糟糕的代码。并且注释往往不会随着代码的重构自动更新,有的时候我们可能会在修改代码后忘记更新那段用来表达最初意图的文字了。所以,尽量抛弃注释吧,除非我们觉得只有良好的代码逻辑和命名仍旧不足以表达意图。

当然,有些注释可能不得不加,如一.些版权信息。另外,如果我们正在开发公共API,保持一份良好的严格按照格式要求所写的注释有利于生成API参考手册。

当前有一种主张是:不要写注释,或者尽量保持最少的注释。

建议153 : 若抛出异常,则必须要注释

有一种必须加注释的场景,就是异常。如果API抛出异常,则必须给出注释。调用者必须通过注释才能知道如何处理那些专有的异常。通常,即便良好的命名也不可能告诉我们方法会抛出哪些异常,在这种情况下,使用注释是最好的手段。

规范开发行为

而对于测试,本章给出的建议就是,写测试代码应作为开发人员的日常工作,如果某个功能没有测试代码,则意味着我们根本没有完成开发任务。

建议154 : 不要过度设计,在敏捷中体会重构的乐趣

建议155 : 随生产代码一起提交单元测试代码

在开始这个建议之前,首先提出一个问题:我们害怕修改代码吗?是否曾经有无数次面对乱糟槽的代码,下定决心进行重构, 然后在1个月后的某个周一,却收到来自测试部的报告:新的版本没有之前版本稳定,性能也更差了,Bug似乎也变多了。也就是说,重构的代码看上去质量更高了,可是实际测试结果却不尽如人意。

几乎每个程序员都因为此类问题纠结过。我们要修改的代码也许来自某些不负责任或经验欠佳的程序员,也许这些代码是自己一年前写的,但是现在看上去已经惨不忍睹。我们想要修改这些代码,却担心重构出别的问题。即便是在一个开发周期中的产品,也会有这样的选择出现。某个模块可能已经提交测试并确认过,不过现在发现有更优的算法和逻辑时,改还是不改,成了一个问题。

“单元测试”减轻甚至消除了开发者的这种恐惧。如果项目没有测试代码,说明我们只是在生产“定时炸弹”。很多人将生产代码和测试代码分别对待,这是一种过时的做法。程序员在提交自己的生产代码时,必须同时提交自己的单元测试代码。很多现代化的版本管理工具可以在后台制订项目构建计划,自动运行测试项目,统计代码覆盖率,并生成相应报告。我们应该在早上一边喝着咖啡,一边读取这样的报告。

有了测试代码做保证,在很大程度上我们可以放心地去重构了。如果某个功能偏离了既有成果,那就准备接受一个醒目的“红色”吧。

将单元测试放在首要地位的一种开发模式是TDD模式。TDD (Test Driven Development,测试驱动开发)有三条严格的定律:

  • 在编写不能通过的单元测试前,不要编写任何生产代码。
  • 只编写恰好无法通过的单元测试,不能编译也算不通过。
  • 只可编写刚好足以通过当前失败测试的生产代码。

即便我们的团队没有完全采用TDD的开发模式,也可以借鉴这些定律来编写我们自己的测试代码。我们无需一次性编写完 全部的测试代码,那没有必要。这跟过度设计一样,也不可能实现。事实上,我们应该逐步地编写测试代码,而且按如下步骤来编写:

测试代码→生产代码→测试代码

另外,不要害怕为UI编写测试代码。针对UI的单元测试,并不要求有很高的代码覆盖率。通常我们不需要对页面布局、视觉感官进行测试,因为代码无法告诉我们:没错,这就是我想要的界面:但是,我们需要对UI的逻辑部分进行测试。

可以看到,测试结果窗口对于我们编写的测试方法的正确性进行了呈现。不过,VisualStudio的这个可视化测试工具太重量级了,导致开发的过程中运行测试代码太烦琐也太耗时。这里给大家介绍一个有效简化单元测试的运行步骤的武器,那就是TestDriven.NET. TestDriven.NET 安装完毕后,会在Visual Studio的编辑窗口的右键菜单中增加如下所示的菜单项:

如果我们要单独运行某个测试方法,只要将光标移到方法内,然后选择Test With -> Debugger,就会以Debug的形式运行当前的测试方法。我们可以在输出窗口看到最终的测试结果,如果设置了断点的话,程序将会在断点处停下来,以备我们进行调试。TestDriven.NET的更多快捷功能大家可以在开发过程中慢慢体会。

至此,我们已经初步理解了架构模式的目的,以及简单的单元测试方法的编写。本建议最后还要强调在单元测试中非常重要的几个观点,了解之后,你将清楚什么才是真正的单元测试。

  • 首先,单元测试不应引入任何人机交互的内容。如测试过程中不应弹出对话框,等待用户输入或确认。单元测试不应该是被阻滞的。
  • 其次,多线程也不属于单元测试范畴,单元测试应该是快速被执行的,而不是需要等待的。
  • 最后,单元测试不应跨应用程序域,例如,数据访问或者远程通信属于集成测试范畴,而不是单元测试。

虽然以上所述这些模块不属于单元测试范畴,但是软件逻辑几乎总是需要和这些模块进行交互,如果失去了这些交互,那么单元测试所发挥的作用就不大。

单元测试总体分为两大类:基于状态的测试(State-based testing)基于交互的测试(Interaction-based testing)

基于状态的测试相对来说较为简单,如我们演示的Add方法:而基于交互的测试,就相对较为复杂,但是其基本思想则无非就是将操作方法提炼成接口,然后让测试替身(test doubles)去实现这些接口,用来代替同样实现了这些接口的实际方法。目前在.NET世界中,出现了越来越多的动态模拟库供我们使用,如Moq、RhinoMocks, 它们简化了测试替身的生成,如果我们想快速编写基于交互的单元测试,则应该掌握至少一种这样的动态模拟库。

建议156 : 利用特性为应用程序提供多个版本

基于如下的理由,需要为应用程序提供多个版本:

  • 应用程序有体验版和完整功能版。
  • 应用程序在迭代过程中需要屏蔽一些不成熟的功能。

无论是什么理由,所谓应用程序的多个版本,就是出于某种理由,不对用户开放应用程序的全部功能。

假设我们的应用程序共有两类功能:第一类功能属于单机版,而第二类的完整版还提供了在线功能。那么,在功能上,需要定制两个属性“ONLINE”和“OFFLINE”。在体验版中,我们只开放“OFFLINE”功能。

要实现此目的,不应该提供两套应用程序,而应该通过最小设置,为一个应用程序输出两个发布版本。这一切,可以通过.NET中的特性(Atribute)来实现。

输出两个发布版本。这一一切,可以通过.NET中的特性(Atribute)来实现。

[Conditional("ONLINE")]
public void Testing()
{
   Console.WriteLine("完整版");
}
[Conditional("ONLINE")]
[Conditional("OFFLINE")]
public void Gey()
{
   Console.WriteLine("单机版");
}

要实现两个不同的功能,需要在程序入口这个文件中定义:

#define ONLINE
//#define OFFLINE

记住,这个条件编译符号一定要在文件的最开头处。同时,该定义只对本文件有效。如果要想定义全局条件编译符号,则必须在项目属性中定义,如图12-9所示。

如果想定义多个全局宏定义,则用逗号隔开,如“ONLINE,OFFLINE”。

接下来的问题就比较简单了,如果要发布所有功能,就输入条件编译符号#define ONLINE

如果要发布离线版功能,就注释掉#define ONLINE,然后定义OFFLINE就行了。

建议157 : 从写第一个界面开始,就进行自动化测试

如果说单元测试是白盒测试,那么本建议所讲的自动化测试,更多意义上指的是黑盒测试。黑盒测试要求捕捉界面上控件的句柄,并对其进行编码,以达到模拟人工操作的目的。

以往,黑盒测试需要调用Windows底层的API去实现,但是,在Visual Studio 2010出现后,我们不妨去看看Code UI Automation这个好东西。本建议着重演示如下两点:

  • 1)使用Code UI Automation录制手工操作UI的动作,让VS根据这些操作自动生成测试代码。
  • 2)新建Winform项目(黑盒工具),在该项目中调用这些自动生成的代码。

第一点之所以要让VS自动生成代码,是为了免去我们手工编写测试代码的繁杂工作;第二点则是为了让我们的测试工具可以脱离VS。

借助Code UI Automation自动生成的代码,繁杂而细致的测试代码编写工作就交给VS的测试引擎去实现了,我们可以更多地将细节放在测试的业务逻辑上,而不是努力地去获取各种控件的句柄并操作它们。

当然,如果要应付GUI频繁发生变化的情况,手写那些生成的代码比较好,这取决于我们对Code UI Automation类库的熟悉度。不过,为了让应用程序更健壮,花点时间学习Code UI Automation是很值得的。