1.3 编码标准、原则和方法的必要性
如今的大多数软件都是由多个开发团队共同写就的。而这些开发者就像你我一样有自己独特的编码方式,都有某种形式的编程思想。开发者经常为不同的软件开发方式而争论。但大家一致认为,如果大家都遵守同一套给定的编码标准、原则及方法,那么开发者的生活将变得更加轻松。
接下来我们将详细解释其中的含义。
1.3.1 编码标准
编码标准规定了一系列必须遵守的事项。这种标准可以使用工具(例如FxCop)或者通过人工同行评审来保证。所有的公司都有其自主规定并需要强制遵守的编码标准。但在现实工作中,当进度已逼近业务限期时,在限期之内完成工作就比保持代码质量显得更加重要,因而编码标准也就被无视了。通常,可以在缺陷列表中将这些需要重构的代码添加为技术债,以便在发布之后进行修正。
Microsoft也制定了自己的编码标准,其标准被广泛采纳并根据各类业务的需要进行了相应的修改。以下列出了一些可以在线访问的编码标准:
- https://www.c-sharpcorner.com/UploadFile/ankurmalik123/C-Sharp-codingstandards/
- https://www.dofactory.com/reference/csharp-coding-standards
- https://blog.submain.com/coding-standards-c-developers-need/
当一个团队或者多个团队的人员共同遵守相同的编码标准时代码就会趋向统一。统一的代码更易于阅读、扩展和维护,并且也不易出错。如果的确有错误存在,由于所有的开发者都遵守同一套标准,因此找到它们也会更加容易。
1.3.2 编码原则
编码原则同样是一系列规定,它关注于如何编写高质量的代码,如何测试并对代码进行调试,以及如何对代码进行维护。不同的开发团队所遵循的编码原则也不尽相同。
即使你是一个独立开发者,也能够通过定义并坚持自己的编码原则来提供优秀的服务。在团队合作中,若能够对一系列编码原则达成一致,则益处更大。它会令团队在共享代码上的工作变得更加简单。
你将在本书中看到各种编码原则(例如SOLID原则、YAGNI原则、KISS原则以及DRY原则)的范例及其详细解释。但目前只需知道SOLID原则是单一职责原则(Single Respon-sibility Principle)、开闭原则(Open-Closed Principle)、里氏替换原则(Liskov Substitution)、接口隔离原则(Interface Segregation Principle)和依赖倒置原则(Dependency Inversion Principle)的缩写。YAGNI原则是“你不会需要它”(You Ain't Gonna Need It)的缩写;KISS原则是“保持软件简单易懂”(Keep It Simple,Stupid)的缩写;而DRY原则是“避免重复的代码”(Don't Repeat Yourself)的缩写。
1.3.3 编码方法
编码方法将开发软件的过程分解为一系列事先定义好的阶段。每一个阶段中包含若干步骤。不同的开发人员和开发团队可能会有其自身遵循的编码方法。其主要目的是提高从最初的概念阶段经由编码阶段达到部署和运维阶段的效率。
本书将介绍测试驱动开发(Test-Driven Development,TDD)、行为驱动开发(Behavioral-Driven Development,BDD),以及面向方面编程(Aspect-Oriented Programming,AOP)的方法。我们将使用SpecFlow进行行为驱动开发,使用PostSharp进行面向方面编程。
1.3.4 编码规则
我们建议使用Microsoft的C#编码规则。请参见:https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/coding-conventions。
采用Microsoft的编码规则可以确保代码的格式得到最广泛的认同。这种C#编码规则可以让开发者集中于代码本身而不用花心思去关注格式排布等问题。因此一般来说,Microsoft的编码标准对编码的最佳实践起到了促进作用。
1.3.5 模块化
将大型的程序拆解为若干小模块有着重大的意义。小的模块更容易测试、更容易阅读及复用,并可以独立于其他模块来执行。小的模块也更易于扩展和维护。
模块化的程序由不同的程序集与其中不同的命名空间构成。模块化程序中的不同的模块可以由不同的团队来维护,因此它更适合团队开发的方式。
在同一个项目中,代码可以通过与命名空间匹配的目录进行模块划分。命名空间应该包含那些和它的名字相称的代码。例如,如果命名空间的名字为FileSystem
,则和文件与目录相关的类型就可以定义在命名空间对应的目录中。如果命名空间的名字是Data
,则其中应当只包含和数据与数据源相关的类型。
恰当的模块划分的另一个好处在于:如果模块能够保持小巧整洁,那么它的代码就更容易阅读。通常,开发者除去编码工作之外,大部分时间都会花在阅读和理解代码上。因此模块化越合理、代码量越少就越容易阅读和理解。开发者在充分理解代码的基础上可以提高接受和使用相应代码的能力。
1.3.6 KISS原则
你也许是不世出的计算机编程天才。你写出的代码是如此优雅以至于其他开发者难以望其项背,只能在键盘上哀叹。但是其他开发者能够一眼看出这个程序的功能吗?如果你最近十周都在其他浩如烟海的代码中工作,挣扎着赶在最后期限前交付,你还能够清晰地解释十周前你所编写的代码的用途,以及你选择的编码方式背后的理由吗?你在编写代码的时候有没有想过日后仍然会持续地工作在这些代码上呢?
当你在几天之后再次查看之前编写的代码,有没有觉得“我怎么会写出这种垃圾代码呢?我当时到底在想什么?”不仅我会有这样的负罪感,我的一些前同事也有相同的感觉。
在编写代码时,务必要保持代码整洁易读,确保即使是新手程序员也能够理解其含义。通常情况下,初级程序员会接触更多的代码阅读、理解以及维护工作。代码越复杂,需要的时间也就越长。即便是高级程序员也会在这些复杂系统中挣扎,当系统复杂到一定程度时他们也会选择去寻找一份新的、不会对身心健康构成如此巨大压力的工作。
再举一个例子。如果你正工作在一个简单的网站上,那么可以问自己以下几个问题。你真的需要使用微服务吗?当前的项目真的很复杂吗?有没有可能让它变得简单一点以便于维护?如需开发一个健壮的、易维护、可扩展的高效解决方案,我们需要的最少的变化部分有几个?
1.3.7 YAGNI原则
YAGNI是敏捷开发领域中的一个原则,它规定开发者除非绝对必要不应添加任何代码。诚实的开发者会根据设计编写执行结果失败的测试;然后,编写必要的代码令测试通过;最后,将代码重构并移除重复的部分。使用YAGNI软件开发方法可以确保类、方法和整体代码行数保持绝对最小水平。
YAGNI的主要目标是避免开发者对软件系统进行过度设计。如果不需要则无须增加复杂性。仅仅编写必要的代码,不要为不需要的功能编写代码,更不要出于试验和学习的目的添加代码。可以将试验或学习的代码放在沙盒项目中。
1.3.8 DRY原则
DRY即“避免重复的代码”。如果在多个区域都出现了相同的代码那么可以考虑对其进行重构。可以通过观察这些代码来确认是否能够抽取出共同的部分,将其定义在另一个辅助类中,并在整个系统中统一调用或放在一个库中供其他项目使用。
如果相同的代码散放在多处,那么在需要修正其中的缺陷时必须修改各处的代码。在这种情况下,很容易漏掉某处需要修改的代码,造成发布时部分代码得到修正而另一部分代码仍然存在问题的状况。
因此,当遇到重复代码时应当尽早将其移除。如果不这样做的话会导致更多的问题。
1.3.9 SOLID原则
SOLID原则是五个设计原则的统称。它意在令软件更容易理解和维护。软件代码应当易于阅读,并在扩展时无须修改现有代码。这五个设计原则是:
- 单一职责原则:类和方法应当仅具备单一职责。所有组合为单一职责的元素应当组合在一起并进行封装。
- 开闭原则:类和方法应当对扩展开放,对修改封闭。当软件需要更改时应当可以对软件进行扩展,而无须修改既有的代码。
- 里氏替换原则:若函数接收一个基类的指针,那么该指针应当可以替换为任何从基类派生的类(的指针)而无须事先知晓具体类信息。
- 接口隔离原则:如果接口很大,则客户端可能并不需要使用接口中的所有方法。因此应当使用接口隔离原则将方法拆分到若干不同的接口中。因此,与其设计一个大而全的接口不如拆分为若干小型接口,而类可以选择实现需要的接口中的方法。
- 依赖倒置原则:高层次的模块不应当依赖低层次的模块。低层次的模块的替换不应当影响高层次模块的使用。不论是高层次的模块还是低层次的模块都应当依赖于抽象。
抽象不应当依赖于细节,但是细节应当依赖于抽象。
声明变量时应当使用静态类型中的接口或抽象类,而后将实现接口或抽象类的具体类(对象)赋值给相应变量。
1.3.10 奥卡姆剃刀法则
奥卡姆剃刀法则:如无必要则勿增实体。换言之,其本质上意味着最简单的方案也最可能是正确的那个方案。因此,在软件开发中如果总采取不必要的假设,并放弃使用简单的方案,则破坏了该法则。
软件项目通常建立在一系列事实和假设上。事实一般容易处理,但是假设就不一样了。通常,我们会以团队讨论的形式从问题产生一个软件解决方案。在选择方案的时候应当永远选择假设最少的方案,因为这种实现方案将是最准确的。如果方案中的确有一些合理的假设,那么这种假设越多,设计方案包含缺陷的可能性就越大。
项目的构成组件越少,出问题的可能性就越少。因此,请遵循该法则,通过减少不必要的假设,仅处理事实,保持项目整洁并尽可能减少实体数目。