一:前言
没有规矩,不成方圆。在代码的世界中,尤其这样。作为程序员,我们不想让我们的代码写出去之后被人耻笑:看,连个换行都换的这么不专业。作为开发主管,我们则不想我们的组员写出来的代码各类风格都有,五颜六色的,极其丑陋。写出规范的代码,首先需要训练,其次,也有一定的手段或者工具来进行辅助。本小节,我们就要从这两方面入手,讲讲如何规范我们的代码。当然,由于我们现在学到的编码知识还有限,最为规范来讲,本小节也将仅仅会设计那些最基本,最常用的编码的规范,但是即便如此,学完本小节之后,也会让我们的代码看上去专业多了。
注意:我不喜欢一次性将全部的知识点讲完,比如,规范,我们今天可能只会涉及到80%的部分。我喜欢这个“二八原则”,即,我们只花20%的时间来完成80%的事情,但是,如果我们想完成剩下的20%的事情,反过来就要额外付出80%的时间,这就有点性价比不那么高了。对于培训或者说学习知识来说,这个“二八原则”很重要。我们的培训,如果要讲解100%的知识点,首先会很枯燥(因为有些知识需要我们具备钻牛角尖的精神才能悟透),其次会很费时间(难道我们不想花最少的时间学习最多的知识吗?),最后,成为真正的专家,从来不是被培训出来的,所以我们的培训,会教会你这80%的部分,剩下的,则希望这80%中已经培养给你的习惯,自己去挖掘。OK,今天废话有点多,言归正传,虽然随着我们的课程我们还没写了多少行的代码,但是即便如此,我相信你也一定觉得现在到了该规范代码的时候了。我们到目前为止,也进行了几次的重构,重构的过程,实际就是讲代码一步步引导到更规范的过程。当然,有些规范,可能是学习完本小节课程我们就会掌握的,而更深入的规范,就需要我们在今后的学习中慢慢掌握了,而且有意思的一点是:规范本身可能还存在冲突性。。。好了,不管怎么样,个中滋味,以后我们慢慢体会吧,现在,GO……
二:命名规范
1: 考虑在命名空间中使用复数
如果有一组功能相近的类型被分组到了同一个命名空间下,则可以考虑为命名空间使用复数。
最典型的例子有,在FCL中,我们需要把所有的非泛型集合类集中在一起存放,所以我们就有了System.Collections命名空间。这样的命名规范,好处就是即便我们从来没有使用过集合类,但是看到这样的命名空间,我们也会知道在它之下是和集合(即Collection)相关的一些类型。不要出现System.AllCollections、System.TheCollection这样的命名,这看上去要么太繁琐、要么含义不清。
举一个实际的例子,如果我们的项目中存在一系列Processor类型,则可以使用命名空间Processors。
2: 用名词和名词组给类型命名
类型是什么?面向对象方面的先驱者会告诉我们,类型对应着现实世界中的实际对象。对象在语言学中意味它是一个名词。所以,类型也应该以名词或名词组去命名。
类型定义了属性和行为。它包含行为,但不是行为本身。所以,下面的一些命名对于类型来说是好的命名:
OrderProcessor;
ScoreManager ;
CourseRepository;
UserControl;
DomainService;
相应的,如下的类型名称则被认为是不好的典范:
OrderProcess
ScoreManage
CourseSave
ControlInit
DomainProvide
动词类的命名更像是类型内的一个行为,而不是类型本身。
3: 用形容词组给接口命名
接口规范的是“Can do”,也就是说它规范的是类型可以具有哪些行为。所以,接口的命名应该是一个形容词组,如:
IDisposable,表示类型可以被释放;
IEnumerable,表示类型含有Items,可以被迭代。
正是因为接口表示的是类型的行为,所以从语义上我们可以让类型继承多个接口,如:
class SampleClass : IDisposable, IEnumerable<SampleClass>
{
//省略
#region 实现IDisposablepublic void Dispose()
{
throw new NotImplementedException();
}#endregion
#region 实现IEnumerable
public IEnumerator<SampleClass> GetEnumerator()
{
throw new NotImplementedException();
}System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
#endregion
}
以上的代码我们写起来会觉得既符合语法,又符合语义。如果,我们将接口命名为IDisposal,这给人造成的误解是该类型是一个类,而不是接口,虽然我们在前面加了前缀I,但仍然感觉这是符合语义的。
不过话又说来,FCL中也有一些违反此规定的例外,比如IEnumerator接口。但是,这种情况相对来说还是比较少的,在大多数情况下,我们需要始终考虑用形容词来为接口命名。
4: 以复数命名枚举类型,以单数命名枚举元素
枚举类型应该具有复数形式,它表达的是将一组相关元素组合起来的语义。比如:
enum Week
{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
在这里,Week对于星期几来说,具备复数含义。如果我们这里将Week修改为Day,那么在调用的代码会变成如下:
Day.Monday
它不会比下面的代码来的简洁明了:
Week.Moday
5: 用PascalCasing命名公开元素
开放给调用者的属性、字段和方法,都应该采用PascalCasing命名方式,比如:
class Person
{
public string FirstName;
public string LastName;public string Name
{
get
{
return string.Format("{0} {1}", FirstName, LastName);
}
}public string GetName()
{
return Name;
}
}
这样,调用者在调用方的代码看起来如下:
person.Name
如果我们不注意这样的命名规则,让调用方的代码看起来是这样的:
person.name
我们首先会怀疑name是个什么类型,其次也会怀疑其可访问性。
6: 用camelCasing命名私有字段和局部变量
私有字段和局部变量只对本类型负责,它们在命名方式也采用和开放的属性及字段不同的方法。camelCasing很适合这类命名。
camelCasing和PascalCasing的区别是它的首字母是小写的。之所以要采用两种不同的命名规则,是为了便于开发者自己快速地区分它们。
在建议123中我们为公开元素给出了一个命名示例,下面的示例,则是私有字段和局部变量的一个示例:
class Person
{
private string firstName;
private string lastName;public string Name
{
get
{
return string.Format("{0} {1}", firstName, lastName);
}
}private int doSomething(int a, int b)
{
int iTemp = 10;
return a + b + iTemp;
}
}
在这里例子中,我们可以看到,所有的私有字段,包括方法的参数及局部变量全部遵循首字母小写的camelCasing规则。一旦我们脱离了这种规则,那么在编码过程中很容易就给自己造成混淆。firstName是什么,难道它不是个私有字段,而是个公开属性吗?这太混乱了,也太可怕了,因为作为开发者的我们不得不回到变量的命名处才知道它的访问范围。
7: 考虑使用肯定性的短语命名布尔属性
布尔值无非就是True和False,所以,应该用肯定性的短语来表示它,如:以Is、Can、Has作为前缀。
布尔属性正确命名的一个示例如下:
class SampleClass
{
public bool IsEnabled { get; set; }public bool IsTabStop { get; set; }
public bool AllowDrop { get; set; }
public bool IsActive { get; }
public bool? IsChecked { get; set; }
}
以上的这些命名都来自于.NET最新的WPF子集,其中AllowDrop虽然不是以肯定性短语作为前缀,但是其作为动作表达了一个是与否的含义,所以也是一个推荐的布尔型属性的推荐命名。
布尔型属性命名的反面教材如下:
class SampleClass
{
public bool Checked { get; set; }
public bool Loaded { get; set; }
}
肯定性形容词或者短语虽然表达了一个肯定的含义,但是这些单词或者短语现在都被用于命名事件或委托变量,所以不应该用于布尔属性。
三:代码整洁
1: 总是提供有意义的命名
除非恶意为之,否则永远不要为自己的代码提供无意义的命名。
害怕需要过长的命名才能提供足够的意义?不要怕,其实我们更介意的是在读代码的时候出现一个iTemp。
int i这样的命名方式只应该出现在循环中(如for循环),除此之外,我们找不到任何理由在代码的其他地方出现这样的无意义命名。
2: 方法抽象级别应在同一层次
方法的抽象级别应在同一个层次上,我们来看下面的代码:
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
}
}
重构后的代码看上去清晰明了,所有的方法的抽象级别都在一个层次上,作为阅读者的我们一眼看上去就知道Init方法完成了什么样的功能。
3: 一个方法只做一件事
“单一职责原则(SRP)”要求每个类型只负责一件事情。我们将此概念扩展到方法上,就变成了:一个方法只做一件事。
什么样的代码才叫“做同一件事”?参照上一个建议中的代码,其中,LocalInit方法和RemoteInit方法是两件事情,但是在同一抽象层次上,在类型这个层次对外又可以将其归并为“初始化”这一件事情上。所以,“同一件事”要看抽象所处的地位。
下面的方法就完成了太多事情,我们来看这段实际的代码:
private uint status;
private uint DeveloperID;
private uint flags;
public string CheckDogAndGetKey()
{
flags = SentinelKey.SP_STANDALONE_MODE;
status = oSentinelKey.SFNTGetLicense(DeveloperID, oSentinelKeysLicense.SOFTWARE_KEY, SentinelKeysLicense.LICENSEID, flags);if (status != SentinelKey.SP_SUCCESS)
{
throw new FileNotFoundException("未检查到合法的加密狗,或者未正确安装驱动");
}status = oSentinelKey.SFNTReadString(SentinelKeysLicense.SP_1ST_STRING, readStringValue, MAX_STRING_LENGTH);
if (status != SentinelKey.SP_SUCCESS)
{
throw new FileNotFoundException("读取加密狗数据失败");
}return System.Text.Encoding.ASCII.GetString(readStringValue).Substring(0, 7);
}
在方法CheckDogAndGetKey中,方法既要负责检测加密狗是否被正确安装,又要负责从加密狗中读取相关的信息。显然,这让CheckDogAndGetKey来说,责任太多。我们可以考虑将方法重构为如下两个方法:
void CheckDog()
{
flags = SentinelKey.SP_STANDALONE_MODE;
status = oSentinelKey.SFNTGetLicense(DeveloperID, oSentinelKeysLicense.SOFTWARE_KEY, SentinelKeysLicense.LICENSEID, flags);if (status != SentinelKey.SP_SUCCESS)
{
throw new FileNotFoundException("未检查到合法的加密狗,或者未正确安装驱动");
}}
string GetKeyFormDog()
{
status = oSentinelKey.SFNTReadString(SentinelKeysLicense.SP_1ST_STRING, readStringValue, MAX_STRING_LENGTH);
if (status != SentinelKey.SP_SUCCESS)
{
throw new FileNotFoundException("读取加密狗数据失败");
}return System.Text.Encoding.ASCII.GetString(readStringValue).Substring(0, 7);
}
经过重构,每个方法都只要负责一件事情。并且,从命名来看,CheckDog负责检测加密狗,而GetKeyFormDog则负责获取信息。
4: 避免过长的方法和过长的类
若不遵循“一个方法只做一件事”及类型的“单一职责原则”,则往往会产生过长的方法和过长的类。
如果方法过长,则意味着可以站在更高的层次上重构出若干个更小的方法。那么,有没有具体的指标提示方法是否过长?有,是以行数做指标的,有人建议一个方法不要超过10行,有人建议不要超过30行。当然,这没有唯一标准,在我看来,如果一个方法在Visual Studio中需要滚屏才能阅读完,那么就肯定有些过长了,必须想法重构它。
对于类型,除非有非常特殊的理由,类型的代码不要超过300行。如果行数太多了,则要考虑能否重构。
5: 只对外公布必要的操作
那些不是很必要公开的方法和属性,private之。如果需要公开的方法和属性超过9个,在Visual Studio默认的设置下,就需要滚屏才能显示在Intellisense中了,查看图:
在上图中我们可以看到,Intellisense在可见范围内为我们提示的方法还包括了从Object继承过来的3个方法,实际真正在这个例子中能为我们显示的有价值的信息只有6条。Sample类型的全部代码如下:
class SampleClass
{
int field1;
int field2;
int field3;public int MyProperty1 { get; set; }
public int MyProperty2 { get; set; }
public int MyProperty3 { get; set; }
public int MyProperty4 { get; set; }
public int MyProperty5 { get; set; }
public int MyProperty6 { get; set; }
public void Mehtod1()
{
}public void Mehtod2()
{
}public void Mehtod3()
{
}
}
如果我们为SampleClass增加更多的公开属性或方法,则意味着我们在使用Intellisense的时候增加了查找成本。
若我们打算将某个方法public或internal,请仔细考虑这种必要性。记住,Visual Studio默认给我们生成的类型成员的访问修饰符就是private的,在我看来,这是微软在给我们心理暗示:除非必须,否则关闭访问。
除了类成员外,类型也一样,应将不该对其他项目公开的类型设置为internal。想想类型默认的访问限制符是internal,这意味着类型如果我们没有有意为之,类型就应该只对本项目开放。所以,遵守这个规则,这会使我们的API看上去清爽很多。
四:代码规范静态检查工具
除了我们自己要习惯性掌握的规范外,这个世界上当然还有一些静态检查工具来帮我们分析我们的代码是否符合一定的规范。目前来说,我们习惯性的做法就是使用StyleCop来帮我们完成代码规范的静态检查。
StyleCop是什么?
StyleCop早年是微软自己内部的静态代码和强制格式美化工具。虽然流出来的微软的一些开源项目,跑跑StyleCop,我们仍旧会发现出现很多警告(当然,我们也可以理解为MS各个项目本身定义了自己都有的一些规范)。其官方地址为:
http://archive.msdn.microsoft.com/sourceanalysis
在本小节的此时此刻,当前版本为:StyleCop-4.7.44.0,下面我们来看看如何使用StyleCop进行代码的规范检查。
备注:视频中使用到的规范设置文件下载地址为:http://back.zuikc.com/Settings.zip
五:视频
非公开部分,请联系最课程(www.zuikc.com)