开发人员老是喜欢就编码规范进行争论,但更重要的是如何可以在项目中自始至终地遵循编码规范,以保证项目代码的一致性。而且团队中的全部人都须要明确编码规范所起到的做用。在这篇文章中,我会介绍一些在我多年的从业过程当中所学习和总结的一些较好的实践。express
咱们先来看一个 FizzBuzz 示例。FizzBuzz 要求编写一个程序,遍历从 1 到 100 的数字。其中若是某数字是 3 的倍数,则程序输出 “Fizz”。若是某数字是 5 的倍数,则输出 “Buzz”。若是某数字便是 3 的倍数也是 5 的倍数,则输出 “FizzBuzz”。若是数字既不是 3 的倍数也不是 5 的倍数,则只需输出该数字自己。app
示例1:ide
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public
static
void
Test()
{
for
(
int
i = 1; i < 101; i++)
{
if
(i % 3 == 0 && i % 5 == 0)
{
Console.WriteLine(
"FizzBuzz"
);
}
else
if
(i % 3 == 0)
{
Console.WriteLine(
"Fizz"
);
}
else
if
(i % 5 == 0)
{
Console.WriteLine(
"Buzz"
);
}
else
{
Console.WriteLine(i);
}
}
}
|
什么感受?这段代码须要改进吗?函数
示例2:工具
1
2
3
4
5
6
7
8
9
10
11
|
public
static
void
Check()
{
for
(
int
i = 1; i <= 100; i++)
{
string
output =
""
;
if
(i % 3 == 0) { output =
"Fizz"
; }
if
(i % 5 == 0) { output = output +
"Buzz"
; }
if
(output ==
""
) { output = i.ToString(); }
Console.WriteLine(output);
}
}
|
如今感受如何?还能不能进一步改进?oop
好,让咱们来尝试改进下。代码命名对全部软件开发人员来讲都是件很是困难的事情。咱们花费了大量的时间来作这件事,并且有太多的须要被命名的元素,例如属性、方法、类、文件、项目等。不过咱们的确须要花费一些精力在这些命名上,以使代码中的名称更有意义,进而能够提升代码的可读性。单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public
void
DoFizzBuzz()
{
for
(
int
number = 1; number <= 100; number++)
{
var output = GetFizzBuzzOutput(number);
Console.WriteLine(output);
}
}
private
static
string
GetFizzBuzzOutput(
int
number)
{
string
output =
string
.Empty;
if
(number % 3 == 0)
{
output =
"Fizz"
;
}
if
(number % 5 == 0)
{
output +=
"Buzz"
;
}
if
(
string
.IsNullOrEmpty(output))
{
output = number.ToString();
}
return
output;
}
|
此次感受怎样?是否是比以前的示例要好些?是否是可读性更好些?学习
首先就是代码要为人来编写,其次是为机器。从长期来看,编写可读性好的代码不会比编写混乱的代码要花费更长的时间。若是你可以很是容易地读懂你写的代码,那么想确认其能够正常工做就更容易了。这应该已是编写易读代码足够充分的理由了。在不少状况下都须要阅读代码,例如在代码评审中会阅读你写的代码,在你或者其余人修复Bug时会阅读你写的代码,在代码须要修改时也会读到。还有就是当其余人准备在相似的项目或有相似功能的项目中尝试复用你的部分代码时也会先阅读你的代码。开发工具
“若是你只为你本身写代码,为何要使代码更具可读性?”测试
好,编写易读的代码最主要的缘由是,在将来的一到两周,你将工做在另外一个项目上。而此时,有其余人须要修复当前项目的一个Bug,那么将会发生什么?我敢保证他确定会迷失在你本身编写的恐怖代码中。
从个人我的观点来看,好的代码应该拥有如下几个特征:
因此,要时刻考虑先为人来编写代码,而后再知足机器的须要。
首先,你须要阅读学习其余人编写的代码,来了解什么是好的代码,什么是很差的代码。也就是那些你感受很是容易理解的代码,和感受看起来超级复杂的代码。而后,进行实践。最后花费一些时间、经验和实践来改进你的代码的可读性。通常来说仅经过培训这种方式,在任何软件公司中推进编码规范都有些困难。而诸如结对代码评审,自动化代码评审工具等也能够帮助你。目前流行的工具备:
依据维基百科上的描述:"Coding conventions are a set of guidelines for a specific programming language that recommend programming style, practices and methods for each aspect of a piece program written in this language. These conventions usually cover file organization, indentation, comments, declarations, statements, white space, naming conventions, programming practices, programming principles, programming rules of thumb, architectural best practices, etc. These are guidelines for software structural quality. Software programmers are highly recommended to follow these guidelines to help improve the readability of their source code and make software maintenance easier. Coding conventions are only applicable to the human maintainers and peer reviewers of a software project. Conventions may be formalized in a documented set of rules that an entire team or company follows, or may be as informal as the habitual coding practices of an individual. Coding conventions are not enforced by compilers. As a result, not following some or all of the rules has no impact on the executable programs created from the source code."。
你应该能说出属性、局部变量、方法名、类名等的不一样,由于它们使用不一样的大小写约定,因此这些约定很是有价值。经过互联网,你已经了解了不少相应的准则和规范,你所须要的仅是找到一种规范或者创建你本身的规范,而后始终遵循该规范。
下面使用到的源代码(类库设计准则)是由微软的 Special Interest Group 团队开发的,我只是作了些扩展。
下面是一些关于C#编码标准、命名约定和最佳实践的示例,能够根据你本身的须要来使用。
Pascal Casing
标示符中的首字母,后续串联的每一个单词的首字母均为大写。若是须要,标示符的前几个字母都可大写。
Camel Casing
标示符的首字母为小写,后续串联的每一个单词的首字母为大写。
参考:标示符大小写规则
在互联网上你能够找到足够多的资源,我只是推荐几个其中我最喜欢的:
这里我展现了一些最基本的示例,但就像我上面已经提到的,找到一个适合你的规范,而后坚持使用。
要使用 Pascal Casing 为类和方法命名。
1
2
3
4
5
6
7
8
9
10
11
|
public
class
Product
{
public
void
GetActiveProducts()
{
//...
}
public
void
CalculateProductAdditinalCost()
{
//...
}
}
|
要使用 Camel Casing 为方法的参数和局部变量命名。
1
2
3
4
5
6
7
|
public
class
ProductCategory
{
public
void
Save(ProductCategory productCategory)
{
// ...
}
}
|
不要使用缩写语。
1
2
3
4
5
|
// Correct
ProductCategory productCategory;
// Avoid
ProductCategory prodCat;
|
不要在标示符中使用下划线。
1
2
3
4
5
|
// Correct
ProductCategory productCategory;
// Avoid
ProductCategory product_Category;
|
要在接口名称前使用字母 I 。
1
2
3
|
public
interface
IAddress
{
}
|
要在类的顶端定义全部成员变量,在最顶端定义静态变量。
1
2
3
4
5
6
7
8
9
10
11
12
|
public
class
Product
{
public
static
string
BrandName;
public
string
Name {
get
;
set
; }
public
DateTime DateAvailable {
get
;
set
; }
public
Product()
{
// ...
}
}
|
要使用单数的词汇定义枚举,除非是BitField枚举。
1
2
3
4
5
6
7
|
public
enum
Direction
{
North,
East,
South,
West
}
|
不要为枚举名称添加Enum后缀。
1
2
3
4
5
6
7
8
|
//Avoid
public
enum
DirectionEnum
{
North,
East,
South,
West
}
|
在大型项目中,开发人员会常依赖于编码规范。他们创建了不少规范和准则,以致于记住这些规范和准则已经变成了平常工做的一部分。计算机并不关心你写的代码可读性是否好,比起读懂那些高级的程序语言语句,计算机更容易理解二进制的机器指令。
编码规范提供了不少明显的好处,固然有可能你获得的更多。一般这些项目总体范围的规划,将使可以将精力更多的集中在代码中更重要的部分上。
你须要编写可读性高的代码,以此来帮助其余人来理解你的代码。代码命名对咱们软件开发人员来讲是件很是困难的事情,咱们在这上面已经花费了大量的时间,而且有太多的须要命名的元素,例如属性、方法、类、文件、项目等。因此咱们确实须要花费一些精力在命名规范上,以使名称更有意义,进而提升代码的可读性。
还有,编码规范可让你晚上睡得更香。
我曾经看到过,而且也曾写过一些超大的类。并且不幸的是,结果老是很差的。后来我找到了真正缘由,就是那些超大的类在尝试作太多的事情,这违反了单一职责原则(SRP),也就是面向对象设计原则 SOLID中的 S。
“The single responsibility principle states that every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.”
或者按照 Martin Fowler 的定义:"THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE."
为何必定要将两个职责分离到单独的类中呢?由于每个职责都是变化的中心。在需求变动时,这个变动将会出如今负责该职责的类中。若是一个类承担了多个职责,就会有一个以上的缘由致使其变化。若是一个类有多重职责,则说明这些职责已经耦合到了一块儿。而且某个职责的变化将有可能削弱或限制这个类知足其余职责的能力。这种耦合将会致使很是脆弱的设计,进而在职责发生变化时,设计可能被意想不到的破坏了。
先说什么过期的注释。按照 Robert C. Martin 的定义:
"A comment that has gotten old, irrelevant, and incorrect is obsolete. Comments get old quickly. It is best not to write a comment that will become obsolete. If you find an obsolete comment, it is best to update it or get rid of it as quickly as possible. Obsolete comments tend to migrate away from the code they once described. They become floating islands of irrelevance and misdirection in the code."
针对这个主题,不一样水平的开发人员可能都会有本身的看法。个人建议是尝试避免为单独的方法或短小的类进行注释。由于我所见过的大部分的注释都是在尝试描述代码的目的或意图,或者某些注释可能自己就没什么意义。一般开发人员经过写注释来提升代码的可读性和可维护性,但要保证你所写的注释不会成为代码中的噪音。比起注释,我认为合理的方法命名将更为有效,好比你能够为一个方法起一个更有意义的名字。大部分注释均可能变成了无心义的代码噪音,让咱们来看看下面这些注释:
1
2
3
4
5
6
7
8
9
10
11
12
|
//ensure that we are not exporting
//deleted products
if
(product.IsDeleted && !product.IsExported)
{
ExportProducts =
false
;
}
// This is a for loop that prints the 1 million times
for
(
int
i = 0; i < 1000000; i++)
{
Console.WriteLine(i);
}
|
若是咱们不写注释,而是命名一个方法,好比叫 CancelExportForDeletedProducts() ,状况会怎样?因此,合适的方法命名比注释更有效。然而某些状况下,代码注释也会很是有帮助,好比 Visual Studio 会从注释生成 API 文档。此处的注释略有不一样,你须要使用 “///” 标识符来注释,这样其余开发人员才能看到 API 或类库的智能提示。
我没有说老是要避免注释。按照 Kent Beck 说法,可使用更多的注释来描述程序总体是如何工做的,而不是对单独的方法进行注释。若是注释是在尝试描述代码的目的或意图,那就错了。若是你在代码中看到了密密麻麻的的注释,你可能就会意识到有这么多注释说明代码写的很糟糕。了解更多信息能够阅读下面这几本书:
Region 是 Visual Studio 提供的一个功能,它容许你将代码分块。Region 的存在是由于它可使大文件导航变得容易。Region 还常被用于隐藏丑陋的代码,或者类已经膨胀的很是大了须要分块。而若是一个类作了太多的事情,也就说明其违反了单一职责原则。因此,下次当你想新增一个 Region 时,先考虑下有没有可能将这个 Region 分离到一个单独的类中。
方法中的代码行数越多,则方法越难理解。咱们推荐每一个方法中只包含 20-25 行代码。但有些人说 1-10 行更合理,这只是些我的喜爱,没有硬性的规则。抽取方法是最多见的重构方式之一。若是你发现一个方法过长,或者已经须要一个注释来描述它的目的了,那么你就能够应用抽取方法了。人们老是会问一个方法到底多长合适,但其实长度并非问题的根源。当你在处理复杂的方法时,跟踪全部局部变量是最复杂和消耗时间的,而经过抽取一个方法能够节省一些时间。可使用 Visual Studio 来抽取方法,它会帮助你跟踪局部变量,并将其传递给新的方法或者接收方法的返回值。
Using ReSharper
Using Microsoft Visual Studio
更多的信息能够参考 MSDN。
按照《重构:改善既有代码设计》中的描述,
"Extract Method is one of the most common refactoring I do. I look at a method that is too long or look at code that needs a comment to understand its purpose. I then turn that fragment of code into its own method. I prefer short, well-named methods for several reasons. First, it increases the chances that other methods can use a method when the method is finely grained. Second, it allows the higher-level methods to read more like a series of comments. Overriding also is easier when the methods are finely grained. It does take a little getting used to if you are used to seeing larger methods. And small methods really work only when you have good names, so you need to pay attention to naming. People sometimes ask me what length I look for in a method. To me length is not the issue. The key is the semantic distance between the method name and the method body. If extracting improves clarity, do it, even if the name is longer than the code you have extracted."
经过声明一个类来代替多个参数。建立一个类,用于包含全部的参数。一般来说,这是一个较好的设计,而且这个抽象很是的有价值。
1
2
3
4
5
6
7
8
9
10
|
//avoid
public
void
Checkout(
string
shippingName,
string
shippingCity,
string
shippingSate,
string
shippingZip,
string
billingName,
string
billingCity,
string
billingSate,
string
billingZip)
{
}
//DO
public
void
Checkout(ShippingAddress shippingAddress, BillingAddress billingAddress)
{
}
|
咱们须要引入类来代替全部的参数。
1
2
3
4
5
|
if
(product.Price>500 && !product.IsDeleted &&
!product.IsFeatured && product.IsExported)
{
// do something
}
|
复杂的表达式意味着其背后隐藏了一些涵义,咱们能够经过使用属性来封装这些表达式,进而使代码更易读些。
若是你注意看代码,你会发现一个变量被声明了但从没被使用过。正常来说,咱们编译工程后会获得一个警告,但仍能够运行工程而不会发生任何错误。可是咱们应该尽量地移除这些警告。经过以下步骤能够在工程上设置将警告等同于错误:
在每段程序中都减小函数返回的数量。假设从底部开始阅读代码,你很难意识到有可能在上面的某处已经返回了,这样的代码将是很是难理解的。
仅使用一处返回能够加强可读性。若是程序这么写的话可能看起来比较干净,但不当即返回也意味着须要编写更多代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//avoid
if
(product.Price>15)
{
return
false
;
}
else
if
(product.IsDeleted)
{
return
false
;
}
else
if
(!product.IsFeatured)
{
return
false
;
}
else
if
()
{
//.....
}
return
true
;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//DO
var isValid =
true
;
if
(product.Price>15)
{
isValid=
false
;
}
else
if
(product.IsDeleted)
{
isValid=
false
;
}
else
if
(!product.IsFeatured)
{
isValid=
false
;
}
return
isValid;
|
你能够想象在这 20-30 行代码中就散落了 4 个退出点,这会使你很是难理解到底程序内部作了什么,到底会执行什么,何时执行。
关于这一点我获得了不少人的回复,一些人赞成这个观点,有些则不一样意这是一个好的编码标准。为了找出潜在的问题,我作了些单元测试,发现若是复杂的方法包含多个退出点,一般状况下会须要一组测试来覆盖全部的路径。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
if
( BADFunction() ==
true
)
{
// expression
if
( anotherFunction() ==
true
)
{
// expression
return
true
;
}
else
{
//error
}
}
else
{
//error
}
return
false
;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
if
( !GoodFunction())
{
// error.
return
false
}
// expression
if
( !GoodFunction2())
{
//error.
return
false
;
}
// more expression
return
true
;
|
进一步理解能够参考 Steve McConnell 的《代码大全》。
在软件开发中,断言代码常被用于检查程序代码是否按照其设计在执行。一般 True 表明全部操做按照预期的完成,False 表明已经侦测到了一些意外的错误。断言一般会接收两个参数,一个布尔型的表达式用于一个描述假设为真的假定,一个消息参数用于描述断言失败的缘由。
尤为在开发大型的、复杂的高可靠系统中,断言一般是很是有用的功能。
例如:若是系统假设将最多支持 100,000 用户记录,系统中可能会包含一个断言来检查用户记录数小于等于 100,000,在这种范围下,断言不会起做用。但若是用户记录数量超过了 100,000,则断言将会抛出一个错误来告诉你记录数已经超出了范围。
一个循环一般会涉及三种条件值:第一个值、中间的某值和最后一个值。但若是你有任何其余的特定条件,也须要进行检测。若是循环中包含了复杂的计算,请不要使用计算器,要手工检查计算结果。
一般在任何软件公司中推行编码规范都须要按照组织行为、项目属性和领域来进行,在此我想再次强调“找到一个适合你的编码规范,并一直遵循它”。
若是你认为我遗漏了某个特别有用的编码准则,请在评论中描述,我会尝试补充到文章中。
Coding For Fun.