1.2 良好的代码与劣质的代码
首先,良好的代码和劣质的代码都是可以编译的代码。其次,不论良好的代码还是劣质的代码都有其成因。表1-1中分别列举了一些成因及其对比。
表1-1 良好的代码与劣质的代码
这可真是一份详细的清单。在以下小节中,我们将讨论良好和劣质代码的特性与差异,以及它们将对代码产生何种影响。
1.2.1 劣质的代码
在本节中,我们将简要介绍上述每一种错误的编码实践,并详细说明这些实践如何对代码造成影响。
1. 混乱的缩进
混乱的缩进会令代码难以阅读,在方法过长时尤为如此。为了提高代码的可读性,需要进行合理的缩进。混乱的缩进会令人难以区分代码块之间的归属。
Visual Studio 2019默认会在括号或花括号闭合时正确地格式化并缩进代码。但是这种格式化功能在代码包含异常情况时并非总是正确的,因此不正确的格式化往往能够引起你的注意。但是如果使用普通的文本编辑器,你就只能手动格式化代码了。
修正错误的缩进是一个费时的操作,花费大量编程时间来弥补这种易于避免的错误往往令人沮丧。请看如下代码:
上述代码虽然格式不佳但终究还是能够阅读的。但是随着代码行数的增加,可读性也会随之下降。
当缩进不佳时很容易发生遗漏闭合括号的情况。由于不容易分辨到底是哪个代码块遗漏了括号,因此要找到遗漏括号所在的位置就变得更难了。
2. 解释显而易见的代码
我经常见到程序员对着一些显而易见的注释一筹莫展,也不止一次地在编程讨论中听程序员宣称他们如何讨厌代码注释。他们认为,代码本身就应该有自解释能力。
我非常理解他们的感受。如果能像读一本书那样读一段没有注释的代码,那么这段代码一定非常优秀。而在字符串类型的变量声明后加上// string
注释就显得很多余了。请看以下范例:
由于变量的类型为int,因此其值必然为整数。在这种地方继续用注释进行说明是没有必要的。它不但会浪费时间和精力,还会把代码弄得一团糟。
3. 解释低质量的代码
即便是工期紧张也不要做这种注释:// I know this code sucks but hey at least it works!
(我知道这段代码不怎么样但是至少它可以工作)。这不但缺乏专业精神也会令其他程序员不满。
如果你真的需要尽快做出成果,那么可以创建一个重构标记,并将这个标记作为TODO注释的一部分。例如:// TODO: PBI23154 Refactor Code to meet company coding practices
。之后不论是你还是其他处理技术债的同事就可以从产品待办项(Product Backlog Item,PBI)中挑选这项任务并完成代码重构。
请看另一个例子:
上述例子则更加恶劣。虽然可以从注释中得到此处可能发生除数为零的错误,但是有没有创建缺陷标记,或者有没有分析问题的根源并修正错误就完全不得而知了。如果项目上一起工作的同事没人接触这部分代码,那么也就没人知道此处存在含有缺陷的代码了。
至少,应当在相应位置保留一个// TODO:
注释。这样,注释内容就会显示在任务列表中,提醒大家解决其中的问题。
4. 将代码注释掉
在尝试过程中将代码注释掉无可厚非,但是如果决定保留其他代码而放弃注释掉的代码,则最好在检入代码之前删除注释掉的代码。有一两条注释掉的代码可能还不会太糟。但是如果注释掉的代码过多,不但会分散注意力,使代码更难维护,甚至还会造成混淆。
保留这段注释是没有必要的。如果它已经被其他代码替代了,那么请删除它。如果使用了版本控制系统,则可以浏览这个文件的历史并在需要时将方法找回。
5. 命名空间组织混乱
当使用命名空间组织代码时,务必避免将无关的代码放置在命名空间中。这将会令人难以找到甚至无法找到所需的代码,在规模庞大的代码库中尤为如此。例如:
上例所有的类位于同一个命名空间下,而将其划分在如下三个命名空间会更加合理:
MyProject.TextFileMonitor.Core
:该命名空间存放核心类。其中的成员会被普遍使用,例如DateTime
类。MyProject.TextFileMonitor.Services
:该命名空间存放所有可以作为服务的类。例如FileMonitorService
。MyProject.TextFileMonitor.Security
:该命名空间存放与安全相关的类。例如范例中的Cryptography
类。
6. 混乱的命名规则
在使用VisualBasic 6编程的那个年代通常使用匈牙利命名法。而Visual Basic.NET中却无须再使用匈牙利命名法了。相反,匈牙利命名法会令代码变得丑陋。因此不要再使用lblName
、txtName
和btnSave
这种命名方式了,请使用NameLabel
、NameTextBox
和SaveButton
这种现代命名方式吧。
使用晦涩难懂与言不由衷的命名会令代码阅读变得异常艰难。ihridx原来是Human Resources Index(人力资源索引),而且它还是一个整数类型,这你肯定想不到吧。同时,请避免使用诸如mystring
、myint
和mymethod
这类命名,因为这些命名无法表达实际含义。
不要在单词之间加入下划线,例如Bad_Programmer
。它会给开发人员带来视觉压力并使代码更难阅读。移除下划线就好了。
不要在类级别和方法级别的变量命名上使用相同的命名规则,否则将难以区分变量的作用域。推荐使用驼峰命名法为变量命名,例如alienSpawn
;使用Pascal命名法为方法、类、结构体和接口命名,例如EnemySpawnGenerator
。
在成员变量(在类的构造器和方法之外定义的变量)的名称前添加下划线前缀可以使我们轻易地区分局部变量(在构造器或方法中的变量)和成员变量。我在工作中就会使用这种规则,它效果良好而且为程序员所接受。
7. 一个类执行多种任务
一个定义良好的类应当仅执行一种任务。如果一个类有如下功能:连接到数据库、获得数据、处理数据、加载报告、将数据添加到报告中、显示报告、保存报告、打印报告与导出报告,就显得职责过多了。应当将其重构为一系列更小的、组织良好的类。这种包罗万象的类阅读起来是令人痛苦的。我本人对这种类心存畏惧。当遇到这种类时,可以先将其中的函数划分为若干区域,而后将每一个区域的代码移动到新的类中,令其只执行一种任务。
以下范例中的类就执行了多种任务:
上述代码中的类有两个主要功能:执行数据库操作与执行文件操作。其代码从逻辑上整齐地划分到了两个恰当命名的区域中。但该类依然破坏了单一职责原则(Single Res-ponsibility Principle,SRP)。我们可以先将这段代码中的数据库操作重构到一个独立的类中,例如DatabaseManager
。
之后从DbAndFileManager
类中移除数据库操作,只保留文件操作,并将该类重命名为FileManager
。同时我们也需要考虑各个文件所在的命名空间是否合适。例如,是否应当将DatabaseManager
放到Data
命名空间中,而将FileManager
放到FileSystem
命名空间或者程序中的其他此类命名空间中。
以下范例展示了将数据库代码从DbAndFileManager
类提取到自己的类中,并在正确的命名空间中的结果:
以下范例展示了抽取出的FileManager
类及FileSystem
命名空间:
以上我们阐述了如何发现职责过多的类,以及如何将其重构为职责单一的类。接下来我们将在职责过多的方法上重复上述过程。
8. 一个方法做多件事情
我曾经在工作中迷失在拥有太多层缩进、做了太多事情的方法中,其中的逻辑排列令人难以驾驭。我想重构这些代码来降低维护的难度,但是我的上级制止了我。我确定如果能够将其划分为不同的方法,就可以明显缩减原有方法的大小。
以下例子中的方法接收一个字符串,并将其加密和解密。这个方法很长,这样就更容易说明为何方法要保持短小:
上述方法包含超过10行代码,难以阅读。此外,该方法有多于一种职责。我们可以将上述代码分割为两个方法,每一个执行一种任务。一个可以进行字符串加密操作,而另外一个进行字符串的解密操作。本例很好地说明了为何一个方法最好不要超过10行。
9. 方法的代码大于10行
过长的方法不易阅读与理解,并可能产生不易觉察的缺陷。过长的方法的另一个问题是容易偏离方法原本的目标。如果方法代码还被注释或区域分割为若干部分,这些弊端就更为显著。
如果必须上下滚动才能够浏览方法的全貌,则这个方法应该是过长了。这会给程序员阅读代码造成压力并产生误解,进而在修改该方法时可能破坏原有代码或曲解代码意图。方法应当尽可能短小,但这需要加以练习,否则可能会将一个小方法过分细分。保持平衡的关键在于确保方法的意图明确,实现整洁。
上一小节的代码展示了为何需要保持方法短小。短小的方法易于阅读和理解。一般来说,如果代码超过10行,那么方法通常做了比原始意图要多的事情。请确保方法名称直接反映其意图,例如,OpenDatabaseConnection()
和CloseDatabaseConnection()
。这有助于坚持且不偏离原意。
接下来将讨论方法的参数。
10. 方法的参数大于两个
若方法参数很多则会稍显笨重,这不但不利于阅读,而且容易搞错参数的值从而破坏类型安全性。
方法的参数越多则参数的排列方式就越多,因而测试起来也越复杂,更容易丢失测试用例并造成产品的缺陷。
11. 使用异常控制程序的执行流程
使用异常来控制程序流程容易隐藏代码的意图,导致意料之外的结果。事实上,如果在编写代码的时候就预期代码会抛出一种或多种异常很有可能意味着设计上的问题。我们会在第5章中介绍更多的细节。
使用业务规则异常(Business Rule Exception,BRE)来控制程序流程就是一个典型情况。例如,方法在某些异常发生时会执行特定动作,即程序的流程会由于是否存在异常而确定。而更好的方式是使用语言本身提供的结构来验证布尔值。
以下代码展示了使用BRE控制程序流程的做法:
BreFlowControlExample()
方法接收BusinessRuleException
类的参数,并根据异常中消息的内容来决定应该调用DoOutOfAcceptableRangeWork()
还是DoInAcce-ptableRangeWork()
方法。
更好的做法是使用布尔逻辑来控制流程。例如下面的BetterFlowControlExample()
方法:
以上方法接收一个布尔值,并使用该布尔值判断采取哪一条执行路径。如果isInAcce-ptableRange
满足判断条件,则调用DoInAcceptableRangeWork()
方法;反之,将调用DoOutOfAcceptableRangeWork()
方法。
下一节将介绍代码可读性弱的问题。
12. 代码可读性弱
类似千层面或者意大利面的代码是难以阅读与理解的。命名不当的方法可以掩盖其原意,也同样令人烦恼。加之如果方法还很长,且关联方法被多个不相关的方法分隔就更难以理解了。
千层面代码,指一般所说的间接的、引用抽象层级的代码。这种引用指名称的引用而非动作的引用。在面向对象编程(Object-Oriented Programming,OOP)中,层的使用很常见,并通常都有好的效果。但是,间接引用越多,代码就越复杂。此类代码会令项目上新程序员了解代码的过程越发艰难。因此,维持间接性和易理解性之间的平衡就显得尤为重要。
而意大利面代码,指那些杂乱无章的低内聚紧耦合的代码。这样的代码难以维护、重构、扩展和重新设计。从积极的方面说,由于这种程序往往更加过程化,因此也许更易于阅读和模仿。我曾经作为初级程序员在一个VB6地理信息系统项目上工作(这个项目主要销售给其他公司用于市场营销)。该项目的技术负责人和高级程序员曾经尝试去重新设计这套系统,但均失败了。此后,他们将重新设计程序的重担交给了我。由于我当时也并不擅长软件分析与设计,因此不出意外也遭遇了失败。
这个项目的代码太过复杂、难以理解并难以分类整理为相关的部分,同时它也太大了。现在想来,我当时应当整理出程序所做的所有事项,并按照功能对列表分组,而后整理出需求列表。在进行这些事项的过程中甚至无须查看代码。
因此我的经验是,在重新设计软件时不要一头扎入代码中。写出程序的所有功能,以及它应当包含的新功能。将这个列表整理为一系列的软件需求,包括相关的任务、测试和验收标准,然后按照规范进行开发。
13. 代码耦合紧密
耦合紧密的代码难以测试、扩展和修改。同时依赖系统其他代码的代码也不易复用。
代码在参数中引用具体类的类型而非接口就是代码紧耦合的一个范例。当引用具体类时,任何对具体类的修改都会直接影响引用它的类。因此如果客户端起初连接的是SQL Server数据库,而对另一个用户连接的是Oracle数据库,那么程序就需要针对特定用户将所依赖的具体类修改为Oracle数据库相关的类型。这就会产生两个版本的代码。
因此不同用户越多,所需的代码版本越多。这会令程序很快变得羸弱不堪并成为维护人员的噩梦。假设该数据库连接类拥有100 000个不同的客户端,每一个客户端会使用该类的30个变体之一。当发现这些变体均含有同种缺陷时,那么这30个变体类就都需要进行相同的修正、测试、打包和部署。这样不但维护工作量巨大而且价格昂贵。
以上这种特定的场景可以通过引用接口类型并使用数据库工厂创建所需的数据库连接对象来解决。数据库的连接字符串可以由用户设置在配置文件中并传递给工厂,而工厂则生成一个具体的连接类,该连接类为连接字符串中指定类型的数据库实现了连接接口。
以下是一个紧耦合代码的负面范例:
从上述代码中可以看出,该数据库类和SQL Server紧绑定在一起。如果想要更改数据库类则需要进行硬编码修改。本书将在后续章节中用具体代码来说明重构此类代码的方法。
14. 低内聚的代码
低内聚的代码指将执行不同功能的不相干代码聚合在一起的代码形式。例如工具类(utility class)中包含多种处理日期、文本、数字,进行文件读写、数据验证、加密解密等功能的方法。
15. 遗留对象
当对象遗留在内存中时,它们可能导致内存泄漏。
静态变量可以通过几种形式造成内存泄漏。使用DependencyObject
、INotifyPro-pertyChanged
或者直接订阅事件都有可能造成内存泄漏。例如,在通过PropertyDescri-ptor
的AddValueChanged
方法使用ValueChanged
事件时,公共语言运行时(Common Language Runtime,CLR)将创建一个强引用,令PropertyDescriptor
的内部存储引用其绑定的对象。
除非之后将事件解绑,否则将造成内存泄漏。除此之外,使用静态变量引用的对象时,若后续不进行释放,也会造成内存泄漏。由于静态变量引用的对象属于垃圾回收器(Garbage Collection,GC)的根对象,而根对象会被垃圾回收器标记为不可回收,因此任何被静态变量引用的对象都会被垃圾回收器标记为不可回收。
使用匿名方法捕获类的成员时,相应类的实例也会被引用。只要匿名方法仍然存活,则该类的实例也会继续存活。
使用非托管代码(或COM)时,如不能释放相应的托管和非托管对象并显式释放内存,就会造成内存泄漏。
在无特定存储期限的缓存中不使用弱引用、不清理未使用的缓存或未限制缓存的大小都将令内存最终耗尽。
在不会终止的线程中创建对象引用也会导致内存泄漏。
非匿名引用类的事件订阅也可能造成内存泄漏。只要这些事件仍然被订阅,则相应的对象就依然会保留在内存中。除非在不再需要进行时间订阅时解绑,否则就非常可能造成内存泄漏。
16. 使用Finalize()方法
终结器虽可用于释放没有被正确销毁的对象中的资源并避免内存泄漏,但是它仍然有很多缺点。
首先,终结器的调用时机是不确定的。其次,垃圾收集器在回收之前,会将含有终结器的对象及其对象图中所有依赖的对象提升到下一代内存中,直至垃圾回收器将其回收。这意味着这些对象将在内存中停留较长时间。因此,在使用终结器的情况下,如果创建对象的速度比垃圾回收的速度快,则会发生内存用尽异常。
17. 代码过度设计
过度设计可能会成为十足的麻烦。对任何人来说,在一个庞大的系统中跋涉,去理解它、使用它以及找到功能的位置都是耗时耗力的。更麻烦的是,当没有文档时,你对系统很陌生,甚至对系统熟悉的人也无法解决你的问题。
在上述系统中,开发人员如需在规定的期限内完成工作将会面临巨大的压力。
令代码整洁易懂
这个例子发生在我之前工作的单位。我当时需要为一个网络应用程序编写一个测试。该应用程序从一个服务获取JSON数据,使用一个子程序进行一些测试并将测试结果发送到另一个服务。我起初并没有按照公司的规定使用OOP、SOLID或是DRY原则,而是使用KISS原则并使用过程式编程和事件处理在很短的时间内完成了功能。但我因此而受到了警告,我必须使用他们自研的测试执行器重写这个功能。
于是我开始学习如何使用这种测试执行器。它不但没有文档,而且没有遵守DRY原则,真正理解其用法的人少之又少。我之前完成这些功能仅仅用了几天时间,但是使用自研工具的新版本程序足足花了几周,这是因为其系统中根本没有我需要的功能。我不得不等待其他人先将这些功能开发完成,而这拖慢了进度。
我的第一个方案完全能够满足业务需求,该方案是一段和系统其他部分无关的独立代码。而第二个方案满足了开发团队的技术要求。这个项目最终超过了预定的交付期限。任何超过预定交付期限的项目都会给企业带来超出计划的成本。
我特别想指出的一点是与使用“通用”的测试执行器重写的系统相比,第一个受到团队警告的系统实际上更简单也更容易理解。
我们并不需要非得遵循OOP、SOLID和DRY。有时不这样做也是有意义的。毕竟,虽然我们可以编写漂亮的符合OOP的系统,但是最终生成的代码仍会是更容易被计算机系统理解的过程式代码。
18. 大型类中缺少区域划分
若一个大型类中拥有太多的区域,并且方法并没有归并分类,其代码就难以阅读和理解。区域可以将相似的成员聚拢在一起。因此请尽量使用这一功能。
19. 失去焦点的代码
当一个类做的事情太多时,就往往会忘记其原始意图。在处理输入输出的命名空间的文件类中找到了处理日期的方法,这是否合理呢?显然是不合理的。若开发人员并不清楚代码的结构,那么他们将难以找到相应的方法。请看以下代码:
你能想到这个类最初的职责吗?它的名字没有给我们任何提示。其中的MyMethod
又是做什么用的呢?看上去这个类还在处理日期数据以及查询产品数据。显然,AddDates
方法应当位于一个仅用来处理日期的类中,而GetData
方法则应当位于产品的视图模型中。
20. 直接暴露信息
在类中直接暴露信息是错误的。除去造成紧密的耦合并容易导致缺陷之外,如需更改相应信息的类型则必须更改任何使用该信息的代码。另外,如果想在赋值之前进行正确性验证又该怎么办呢?请看以下范例:
在上述代码中,如果将UnitsInStock
的类型从long
更改为int
,则需要更改所有引用该字段的代码。而对于ProductCode
字段也一样。如果新的产品编号必须严格满足格式要求,那么将字符串直接赋值给相应类的字段的做法将无法达到数据验证的目的。
1.2.2 良好的代码
介绍完错误的实践后,是时候来看一看良好的代码实践了。良好的代码实践有助于编写赏心悦目且高效执行的代码。
1. 合理的缩进
使用合理缩进的代码更加易读。从代码的缩进上很容易辨识代码块的起始和结束位置,以及代码和代码块的归属关系:
上述范例虽然简单,但也能够展现出清晰易读的特点。各个代码块的起始与终止位置均清晰可见。
2. 有意义的注释
有意义的注释是能够表达程序本意的注释。有时虽然代码是正确的,但其含义并不容易被新接触这段代码的开发人员理解,甚至即使作者本身事隔几周之后也不易回想其含义。此时这类注释就会带来很大的帮助。
3. API文档注释
良好的API应当拥有记录详细、易于理解的文档。API注释是一种可以生成HTML文档的XML注释。HTML文档对API的使用者来说是非常重要的。文档越易用,开发人员使用API的意愿就越强。例如:
上述代码充分展示了良好的API文档注释的实践。它摘录自Kusto Query Language项目。
4. 使用命名空间合理组织代码
使用命名空间合理组织的代码可以直观地节省开发者查找代码的时间。例如,如果需要查找和日期与时间相关的代码,那么可以以DateTime
为命名空间,将时间相关的方法集中在Time
类中,而将日期相关的方法集中在Date
类中。
表1-2展示了恰当组织命名空间的方式。
表1-2 恰当组织命名空间的方式
5. 合理的命名规则
遵循Microsoft C#的命名规则是良好的实践。在命名空间、类、接口、枚举和方法上应当使用Pascal命名法,而在变量名称、参数名称上应当使用驼峰命名法。在成员变量上必须加上前缀下划线。
请看如下范例:
上述代码展示了符合命名规则的命名空间、类、成员变量、参数以及局部变量。
6. 一个类执行一种任务
设计良好的类应当只执行一种任务,而且能够清晰地表达设计意图。类中的内容恰到好处,没有与之无关的代码。
7. 一个方法做一件事情
一个方法应当仅做一件事情;避免做多件事情,例如解密字符串并进行字符的替换。方法的意图应当明确。仅做一件事情的方法更容易成为短小的、可读的与表意清晰的方法。
8. 方法的代码少于10行,最好不超过4行
理想情况下,方法代码不应超过4行。但这并非总是可行的。因此我们的目标是令方法的代码长度小于10行,以保持其可读性和可维护性。
9. 方法的参数不多于两个
方法最好没有参数,当然有一到两个也是可以的。但当方法开始拥有两个以上的参数时就需要考虑类和方法的职责是不是太多了。如果方法的确需要两个以上的参数,那么最好将其合并为一个对象参数。
任何多于两个参数的方法都会逐渐变得难以阅读和理解。不多于两个参数的方法可以令代码易读,而只含有一个对象参数的方法比起含有多个参数的方法要易读得多。
10. 合理使用异常
永远不要使用异常对象来作为流程控制的手段。使用不会触发异常的手段来处理一般情况下可能触发异常的条件。设计良好的类使用这种手段来避免抛出异常。
可以使用try/catch/finally
从异常状态中恢复并(或)释放资源。请使用可能从代码中抛出的特定异常类型进行捕获,以便能够得到更多细节信息进行日志记录或进行后续处理。
.NET中预定义的异常无法适用于所有场景。因此,在一些场景自定义异常是非常必要的。异常的名称应当以Exception
结束并至少应当包含以下三种构造器:
Exception()
:该构造器使用默认值创建异常。Exception(string)
:使用字符串作为异常消息。Exception(string, Exception)
:使用字符串作为异常消息,并接收一个内部异常(作为产生当前异常的原因)。
如果要使用异常就不要再返回错误代码,直接抛出包含有意义信息的异常即可。
11. 代码可读性强
代码可读性越强,开发人员越乐于去使用它。这种代码易于学习与使用。即使项目上人员发生了更迭,新人也能够毫不费力地阅读、扩展并维护其代码。可读性强的代码也不容易出现缺陷和安全问题。
12. 代码耦合程度低
耦合度低的代码更容易进行测试和重构,更容易在需要时进行替换或更改。低耦合度的代码还有易于复用的优势。
之前我们介绍过向数据库类传递SQL Server连接对象的反面案例。我们可以通过将具体的类重构为接口类使方法耦合度降低。重构之后的范例如下:
在这个简单的例子中,我们可以传递任意实现了IDatabaseConnection
接口的数据库连接类。如果SQL Server连接类出现问题,那么只会影响SQL Server的客户端。而其他数据库类的客户端仍然可以正常工作。在修复时,只需修复采用SQL Server数据库的客户使用的那个类即可。这降低了维护的开销,同时也降低了整体维护的成本。
13. 高内聚的代码
将公共的功能正确地分组的代码具有高度的内聚性。这样的代码易于查找。例如,在Microsoft System.Diagnostics
命名空间中包含的代码必然只和诊断相关。在Diagnostics
命名空间中包含集合或文件系统相关的代码是没有意义的。
14. 对象会被恰当销毁
使用可销毁的对象时,请务必调用Dispose()
方法明确地销毁使用中的资源。这有助于降低内存泄漏的可能性。
有时,我们需要将对象(引用)设置为null
以使其超出作用范围。例如,在静态变量持有的对象引用不再继续使用时。
若使用的是可销毁对象,那么使用using
语句可以在对象超出作用域时自动将其销毁。这种做法无须显式调用Dispose()
方法。如以下代码所示:
以上代码在using
语句中定义了一个可销毁对象,并在大括号范围内使用该对象。而在跳出大括号之前,该对象会自动销毁。因此无须手动调用其Dispose()
方法,它会被自动调用。
15. 避免使用Finalize()方法
使用非托管资源时最好实现IDisposable
接口并避免使用Finalize()
方法。终结器执行的时机是不确定的。它很可能不会按照我们所期望的顺序或时机执行。因此,最好在更加可靠的Dispose()
方法中来销毁非托管资源。
16. 合理地进行抽象
当设计只向更高的级别开放,并仅开放必需的内容时,它就处在正确的抽象层次上。合理地进行抽象可以避免在实现中迷失方向。
过度抽象会使我们在工作中纠缠于各种实现细节,而抽象不足则会发生多个开发者同时工作在一个类上的情况。不论遇到哪种情况,我们都可以使用重构来回到正确的抽象层次上。
17. 在大型类中使用#region进行区域划分
“区域”可以进行折叠,因此适于在大型类中将不同的成员进行分组。阅读大型的类并在其方法中跳来跳去是令人沮丧的。将类中相互调用的方法分为一组是一个不错的办法。在处理代码时,可以根据需要折叠或者展开这些方法。
从上述说明中不难看出,良好的编码实践可以令代码更加易于阅读和维护。接下来我们将探讨编码标准和原则的必要性。我们还将介绍一些软件开发的方法,例如SOLID和DRY原则。