软件质量,不但依赖于架构及项目管理,而且跟代码质量息息相关。代码质量与其整洁度成正比。如同干净的地板能减少事故发生,归置到位的工具能提升生产力一般。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好的基础。
有人说过我们正在临近代码的终结点。很快,代码就会自动产生出来,不需要再人工编写。程序员完全没用了,因为产品经理可以直接写出需求,生成代码。
扯淡!我们期待语言的抽象程度继续提升,那会是好事一件,但那终结不了代码。在较高层次上撰写的语言也将是代码。它需要严谨、精确、规范和详细,而不是产品经理提出的天马行空的需求。将需求明确到机器可以执行的细节的程度,就是编程要做的事。
大约在1951年,一种名为“全员生产维护”(Total Productive Maintenance,TPM)的质量保证手段在日本出现。它关注维护甚于关注生产。TPM的主要支柱之一是所谓的5S原则体系。5S是一套规程,包括以下概念:
丹麦有一句谚语:“小处诚实非小事”,著名建筑师路德维希·密斯·凡德罗也说过:“神在细节之中”,本书介绍了一些小小的整洁代码的技巧,涵盖从命名到重构的多个方面。只要遵循这些规则,就能编写出干净的代码,从而有效提升代码质量。
我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。
————C++语言发明者 Bjarne Stroustrup
犯罪心理学中有一个破窗理论,以一幢有少许破窗的建筑为例,如果那些窗不被及时修理好,可能会有破坏者破坏更多的窗户。最终他们甚至会闯入建筑内,如果发现无人居住,也许就在那里定居或者纵火。一面墙,如果出现一些涂鸦没有被清洗掉,很快地,墙上就布满了乱七八糟、不堪入目的东西;一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会理所当然地将垃圾顺手丢弃在地上。
在编程过程中,也同样经常出现这些问题。当我们接手了一份结构混乱的代码,如果不及时加以整理,在一团乱麻中继续添加代码,最终将导致代码结构越来越混乱,当我们review代码时,听到最多的辩解就是:前一个人就是这么写的。
程序员面临一种基础价值谜题。有那么几年经验的开发者都知道,之前的混乱拖了自己的后腿,但开发者背负期限的压力,只好继续制造混乱。简言之,他们没有花时间让自己做得更快。
美国童子军有一条简单的军规:让营地比你来时更干净。
清理代码也许只是改好一个变量名,拆分一个有点过长的函数,消除一点点重复代码,清理一个嵌套if语句。当梳理代码时,坚守此军规:每次review代码,让代码比你发现它时更整洁。
整洁的代码总是看起来像是某位特别在意它的人写的。几乎没有改进的余地。代码作者什么都想到了,如果你企图改进它,总会回到原点,赞叹那位全心投入的某人留给你的代码。
————《修改代码的艺术》作者 Michael Feathers
你或许会问:代码真正“读”的成分有多少呢?难道力量主要不是用在“写”上吗?
你是否玩过“编辑器回放”?20世纪80、90年代,Emac之类编辑器记录每次击键动作。你可以在一小时工作之后,回放击键过程,就像是看一部高速电影。我这么做过,结果很有趣。回放过程显示,多数时间我都是在滚动屏幕、浏览其他模块!
鲍勃进入模块。
他向下滚动到要修改的函数。
他停下来考虑可以做什么。
哦,他滚动到模块顶端,检查变量初始化。
现在他回到修改处,开始键入。
喔,他删掉了键入的内容。
他重新键入。
他又删除了!
他键入了一半什么东西,又删除掉。
他滚动到调用要修改函数的另一函数,看看是怎么调用的。
他回到修改处,重新键入刚才删掉的代码。
他停下来。
他再一次删掉代码!
他打开另一个窗口,查看别的子类,那是个负载函数吗?
…
你该明白了,读与写花费时间的比例超过10:1,写新代码时,我们一直在读旧代码。既然比例如此之高,我们就该让读的过程变得轻松,即便那会使得编写过程更难。
在意你的代码,就像给自己的孩子起名一样给每个变量、每个函数起名。
给函数和变量取个好名字是优秀程序员的基本功,取的名字要名副其实,见文知意。
笔者之前修改过一份遗留代码,其中的变量命名很不规范,例如:
val grzx
val fanhui
val a
val aa
梳理许久,我才知道"grzx"变量是一个TextView,代表"个人中心","fanhui"变量是一个ImageView,代表返回按钮。更好的命名方式为:
val tvUserCenter
val ivBack
"a"和"aa"这样无意义的名字更不要使用,读起来让人不知所云,应该为它取个名副其实的名字。
变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存在,它做什么事,应该怎么用。如果名称需要注释来补充,那就不算是名副其实。
var d // 消逝的时间,以日计
修改为:
var elapsedTimeInDays
var daysSinceCreation
var daysSinceModification
var fileAgeInDays
选择体现本意的名称能让人更容易理解和修改代码。以下是一个扫雷游戏的部分代码,你能看出它的目的是什么吗?
var theList = listOf(
intArrayOf(0, 0, 0),
intArrayOf(1, 0, 1),
intArrayOf(-1, 1, 0),
intArrayOf(-1, 1, 1)
)
fun getThem(): List<IntArray> {
val list = mutableListOf<IntArray>()
theList.forEach {
if (it[0] == -1) list.add(it)
}
return list
}
这份代码很简单,但是却很模糊。看了之后只会疑惑:theList是什么?theList的数据中0下标的意义是什么?-1的意义是什么?返回的列表是什么?
将其修改如下:
companion object {
const val STATUS_VALUE = 0
const val FLAGGED = -1
}
var gameBoard = listOf(
intArrayOf(0, 0, 0),
intArrayOf(1, 0, 1),
intArrayOf(-1, 1, 0),
intArrayOf(-1, 1, 1)
)
fun getFlaggedCells(): List<IntArray> {
val flaggedCells = mutableListOf<IntArray>()
gameBoard.forEach { cell ->
if (cell[STATUS_VALUE] == FLAGGED) flaggedCells.add(cell)
}
return flaggedCells
}
很容易看出,gameBoard代表游戏面板,gameBoard中的数据的0下标代表状态值,-1的意义是已被标记。返回的列表是游戏面板中已被标记的格子。
本例中,代码的简洁性完全未修改,但代码变得明确多了。
我们还可以更进一步优化:
...
fun getFlaggedCells(): List<IntArray> {
val flaggedCells = mutableListOf<IntArray>()
gameBoard.forEach { cell ->
if (isFlagged(cell)) flaggedCells.add(cell)
}
return flaggedCells
}
private fun isFlagged(cell: IntArray) = FLAGGED == cell[STATUS_VALUE]
1.如果数据本身不是个List,否则别用 personList 来指代一组数据。List一词对程序员有特殊意义。
var personList = mapOf("Martin" to "Author", "Alpinist Wang" to "Reader")
修改为:
var personMap = mapOf("Martin" to "Author", "Alpinist Wang" to "Reader")
2.谨防名字类似,笔者在使用 EasyRecyclerView 库时,看到作者写了两个函数:
fun onCreateViewHolder(){}
fun OnCreateViewHolder(){}
这两个词实在是太相似了,容易对阅读者造成误导,将其修改为:
fun onCreateViewHolder(){}
fun onCreateEasyViewHolder(){}
1.废话都是冗余,Variable一词永远不应当出现在变量名中,Table一词永远不应当出现在表名中。 nameString 会比 name 好吗? ProductInfo 、 ProductData 和 Product 有什么区别?更糟糕的是,如果代码中同时存在 Article 和 ArticleInfo 类,程序员怎么知道该调用哪个类呢? Info 和 Data 就像 a 、 an 和 the 一样,属于意义含混的废话。
如果缺少明确的约定, moneyAmount 就与 money 没区别, customerInfo 与 customer 没区别, accountData 和 account 没区别, theMessage 也与 message 没区别,要区分名称,就要以读者能鉴别不同之处的方式来区分。
还有一种使用前缀的情况:设想你有名为 firstName、lastName、street、houseNumber、city、state 和 zipcode 的变量。当它们搁在一块儿时,我们可以很明确的知道这是一个地址。不过,假使是在某个方法中看见孤零零一个 state 变量呢?你还会觉得他是某个地址的一部分吗?这时你可能会添加前缀 addrFirstName、addrLastName、addrState等,以此提供语境。至少,读者会明白这是某个更大的结构的一部分。但是,更好的方案是创建名为 Address 的类,将这些变量放在同一个类中。这样,即使是编辑器也会知道这些变量隶属于某个更大的概念了。
2.在早期的代码中,全局变量带有"m"前缀,这么做的原因是:使用时知道这是全局变量。而在阅读代码时,我们都会自动忽略前缀(和后缀),只看到代码中有意义的部分,代码读得越多,眼中就越没有前缀。最终,前缀变成了不入法眼的废料,成为旧代码的标志物。况且,现代化的IDE会把全局变量设为不同的颜色,现在使用"m"前缀纯属多余。与此类似的还有接口的 “I” 前缀。部分程序员喜欢在每个变量名称前加上"_"前缀一样,去掉他们,让代码更为整洁。
在阅读程序时,我往往会再脑海里默念程序执行步骤,如果变量命名如下:
var genymdhms // 生成日期,年、月、日、时、分、秒
阅读时我就会在脑海里默念这些奇怪组合的字母,一边默念一边思考含义。当与其他开发者交流时,他一定会问你,你说的这个gen-Y-M-D…是什么变量?修改为:
var generationTimestamp
这样,就让思维和交流都更加顺畅。
假设你有一个全局的帮助类,GlobalHelper是一个好名字。而你觉得它像个超人一样非常有用,抖机灵把它命名为DiJia(全能的迪迦奥特曼!Biu Biu Biu~),别这样做,因为这会对阅读者造成误解,请让它保持GlobalHelper——宁可明确,毋为好玩。
近年来,我开始研究贝克的简单代码规则,差不多也都琢磨透了。简单代码,依其重要次序:
- 能通过所有测试
- 没有重复代码
- 体现系统中的全部设计理念
- 包括尽量少的实体,比如类、方法、函数等
————《C#极限编程探险》作者 Ron Jeffries
过长的函数不易理解,如果某天这个函数需要修改的话,一个长长的函数让人望而生畏。我们也许只需要修改其中的两行代码,而梳理出这个长函数中的两行代码大大增加了理解成本。并且,小函数也能更好地复用。
1.函数应该清晰明了的实现它的名称声明的事。如果一个函数做了多件事,一个明显的标志是无法为它起一个精准的名字。这时你就应该重构了,将函数提炼成只做一件事的小函数。
2.尽量不要在函数签名中传递状态值。状态值是函数做了多件事的明显标志。将其拆分为不同状态的函数,阅读代码和调用时都会更方便。例如:
fun setLoading(status: Boolean) {
if (status) {
loading.VISIBILITY = View.VISIBLE
} else {
loading.VISIBILITY = View.GONE
}
}
修改为:
fun showLoading() {
loading.VISIBILITY = View.VISIBLE
}
fun hideLoading() {
loading.VISIBILITY = View.GONE
}
同样,类也应该保持短小、并符合单一权责原则。高内聚,低耦合。隔离让系统对每个元素的理解变得容易。
单一权责原则:在面向对象编程领域中,单一权责原则(Single responsibility principle)规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。一个类或者模块应该有且只有一个改变的原因。
条件表达式通常有两种表现形式。第一种形式是:所有分支都属于正常行为。第二种形式是:条件表达式提供的答案中只有一种是正常行为,其他都是不常见的情况。
如果两条分支都是正常行为,就应该使用if-else语句。如果其中一个条件是极少发生的,就应该单独检查该条件,并在其发生时,立即从函数中返回。这样的单独检查常常被称作“卫语句”(guard clauses)。例如:
fun getPayAmount() {
var result: Int
if (isSeparated()) result = separatedAmount()
else {
if (isRetired()) result = retiredAmount()
else result = normalPayAmount()
}
return result
}
这是一份获取工资的代码,如果员工已经离职了,获取离职工资;如果员工已经退休了,获取退休工资,否则获取正常工资。
离职和退休都属于特殊情况,修改为:
fun getPayAmount() {
if (isSeparated()) return separatedAmount()
if (isRetired()) return retiredAmount()
return normalPayAmount()
}
另外,在条件表达式中,应避免否定性条件,否定式要比肯定式难明白一些。所以,尽可能将条件表示为肯定形式。
同一个函数中的代码应该属于同一层级。创建分离较高层级一般性概念与较低层级细节概念的抽象模型,这很重要。例如:只与细节有关的常量、变量或工具函数不应该在基类中出现。基类应该对这些东西一无所知。
良好的软件设计要求分离位于不同层级的概念,将它们放到不同容器中。有时,这些容器是基类或派生类,有时是源文件、模块或组件。无论哪种情况,分离都要完整。较低层级概念和较高层级概念不应混杂在一起。
fun printOwing() {
printBanner()
// 打印详情
System.out.println("name: $name")
System.out.println("amount: ${getOutstanding()}")
}
修改为:
fun printOwing() {
printBanner()
printDetails()
}
fun printDetails() {
System.out.println("name: $name")
System.out.println("amount: ${getOutstanding()}")
}
查询函数中不要做修改操作。这样调用者就可以多次调用查询函数而且没有任何副作用。将有副作用的修改函数单独提出。否则若在查询数据时修改了数据,调用者会产生迷惑。这也容易成为bug之源。
副作用是一种谎言。函数承诺只做一件事,但却偷偷做了其他事。这样的谎言是破坏性的,有时会导致古怪的时序性耦合及顺序依赖。
switch语句天生就要做多件事。随着switch条件的增多,它做的事情也会增多。如果出现了,考虑使用多态替换之。
1.最理想的函数参数数量是0,其次是1,再次是2,应尽量避免3。有足够特殊的理由才能使用三个以上参数——所以无论如何也不要这么做。
2.参数带有太多概念性,没有参数的函数能够见名知意,理解带参数的函数不仅要理解其名字,还要理解其所有参数。
3.太多的参数会造成测试困难。如果参数多于两个,测试覆盖所有可能值的组合让人望而生畏。
更多的重构手法参见《重构改善既有代码的设计》一书。
如果注释纯属废话,请去掉它,例如:
// 如果count > 0,返回1,否则返回-1
if (count > 0) {
return 1
} else {
return -1
}
代码已经清晰的表达了意图,这些注释看起来就像是喃喃自语。
再比如:
// 当this.closed变为true时,直接返回。如果超时未关闭,抛出一个异常
fun waitForClose(timeoutMillis: Long) {
if (closed) return
wait(timeoutMillis)
if (!closed) throw Exception("发射器关闭异常")
}
读这段注释的时间没准比读代码花的时间还要长。而且,你是否发现这个注释有误导读者的嫌疑?
当this.closed中途变为true时,函数并不会直接返回。函数仅仅在开始时判断了this.closed是否为true,否则就只是等待遥遥无期的超时。然后在超时等待时间结束后,再判断this.closed状态。
这一细微的误导信息,放在比代码本身更难阅读的注释里面,有可能导致某位程序员快乐地调用这个函数,并期望在 this.closed 变为true时立即返回。那位可怜的程序员将会发现自己陷入调试困境之中,拼命想找出代码执行得如此之慢的原因。
曾经有一段时间,注释掉的代码可能有用。但我们已经拥有良好的源代码控制系统如此之久,这些系统可以为我们记住不要的代码。我们无需再用注释来标记,删掉即可。当你需要时,很容易就能找回。事实证明,注释掉的代码往往用着过时的函数和变量,很少有需要的时候。
为每个类添加头注释,用@author字段告诉别人你是谁。写代码的时候,记得自己是作者,要为评判你工作的读者写代码,对自己的代码负责。
用于警告其他程序员会出现某种后果的注释是有用的,例如:
fun getDataFormat(): SimpleDateFormat {
// SimpleDateFormat不是线程安全的,所以我们需要每次创建新的实例
val df = SimpleDateFormat("yyyy-MM-dd")
df.timeZone = TimeZone.getTimeZone("GMT")
return df
}
你也许会抱怨说,还会有更好的解决方法。我同意你所说的。不过上面的注释绝对有道理存在,它能阻止某位急切的程序员以效率之名使用静态初始器。
有时,我们需要使用 TODO 注释来表示自己认为应该做,但由于某些原因目前还没做的工作。它可能要提醒删除某个不必要的特性,或者要求他人注意某个问题。它可能是恳请别人取个好名字,或者提示对依赖于某个计划事件的更改。无论 TODO 的目的如何,它都不是在系统中留下糟糕代码的接口。
一定要定期查看 TODO 注释,删除不再需要的。不要让 TODO 变成程序员的谎言。
注释并不像辛德勒的名单。它们并不“纯然的好”。若代码足够有表达力,用代码来展示意图往往会更好。注释总是一种失败。当我们无法找到不用注释就能表达自我的方法时,我们写了注释,这并不值得庆贺。
如果你发现自己需要写注释,再想想看是否有办法翻盘——用代码来表达。每次用代码来表达,你都该夸奖一下自己。每次写注释,你都该做个鬼脸,羞愧于自己在表达能力上的失败。
我为什么极力贬低注释?因为注释会撒谎。也不是说总是如此或有意如此,但出现得实在太频繁。注释存在的时间越久,就离其所描述的代码越远,越来越变得全然错误。原因很简单。程序员不能坚持维护注释。
代码在变动,在演化。从这里移到那里。彼此分离、重造又合到一起。很不幸,注释并不总是随之变动。注释常常会与其所描述的代码分割开来,孑然飘零,越来越不准确。
当你想写一句注释时,更好的方式是将你想要写注释的代码提取出一个单独的方法,再给它起个好名字。
你愿意看到这个:
// 如果员工是优秀员工
if (employee.flags == AWESOME && employee.value > 9) {
// 颁发优秀员工奖励
employee.holiday += 5
employee.money += 2000
}
还是这个:
if (employee.isExcellent) {
employee.getReward()
}
写注释的常见动机之一是糟糕代码的存在。我们编写一个模块,发现它令人困扰、乱七八糟,它烂透了。我们告诉自己:“喔,最好写点注释!”—— 不!最好是把代码整理干净。
// 初始化数据和初始化视图
fun init(){}
修改为:
fun initData(){}
fun initView(){}
想想看你阅读新闻时,在顶部,你期望有个头条,告诉你故事的主题,好让你决定是否要读下去。第一段是整个故事的大纲,给出粗线条概述,但隐藏了故事细节。接着读下去,细节渐次增加,直至你了解所有的日期、名字、引语、说法及其他细节。
源文件也要像新闻文章一样,最顶部给出高层次概念和算法,细节向下渐次展开,直至找到源文件中最底层的函数和细节。函数应该紧跟调用处,保证垂直方向上的靠近。当阅读代码时,能够很流畅的读完。否则若滑上滑下的阅读代码,容易导致思维不流畅,影响理解。
现代化的IDE都有格式化代码快捷键,例如Mac的Android Studio格式化代码快捷键是"Command + Alt + L",你也可以在设置中搜索"Reformat Code",自定义格式化代码快捷键。随时格式化,并去掉多余的空行,让代码保持清爽是一个好习惯。
著名的得墨忒定律(The Law of Demeter)认为:模块不应该了解它所操作对象的内部情形。
换言之,只跟朋友谈话,不与朋友的朋友谈话。不要为了节省变量,写过长的链式调用。在关键的地方为其取个有意义的名字,更容易理清结构,调试也更方便。
val appId = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA).metaData.get("app_id")
修改为:
val applicationInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
val metaData = applicationInfo.metaData
val appId = metaData.get("app_id")
try / catch 代码块丑陋不堪,他们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把 try 和 catch 代码块的主体部分抽离出来,另外形成函数。
错误处理就是一件事。
现代化的语言都有异常机制,对于绝对不应该出现的情况,有的程序员会选择返回0或者-1等错误码,保持程序不崩溃。
请不要这样做,将错误码替换为抛出异常,出现错误时立马就可以发现,更容易定位问题。而不是在错误的状态下继续执行,将来造成更加迷惑的错误。
要讨论错误处理,就一定要提及那些容易引发错误的做法。第一项就是返回 null 值。我不想去计算曾经见过多少几乎每行代码都在检查 null 值的应用程序。返回 null 值基本上就是在给自己增加工作量,也是在给调用者添乱。只要有一处没有检查 null 值,应用程序就会失控。返回 null 不如抛出 NullPointerException ,或是替换为一个空对象。让调用者不再需要检查 null,代码也就更整洁了。
如果每个例程都让你感到深合己意,那就是整洁代码,如果代码让编程语言看起来像是专为解决那个问题而存在,就可以称之为漂亮的代码。
————Wiki发明者,极限编程创始人之一 Ward Cunningham
测试带来的好处不必多说。有了测试,你就不必担心对代码的修改。修改代码后,只需一键运行测试代码,便可验证本次修改是否破坏了原有逻辑。
几年前,有人请我去指导一个开发团队。那个团队认定,测试代码的维护不应遵循生产代码的质量标准。他们彼此默许在单元测试中破坏规矩。“速而不周”成了团队格言。变量命名不用好,测试函数不必短小和具有描述性。测试代码不必做良好设计和仔细划分。只要测试代码还能工作,只要还覆盖着生产代码,就足够好。
有些读者可能会同意这种做法。从编写那种用后即扔的测试到编写全套自动化单元测试是一大进步。所以,就像那个我指导过的团队一样,你或许会认为脏测试好过没测试。
这个团队没有意识到的是,脏测试等用于——如果不是坏于的话——没测试。问题在于,测试必须随着生产代码的演进而修改。测试越脏,就越难修改。测试代码越缠结,你就越有可能花更多时间塞进新测试,而不是编写新生产代码。修改生产代码后,旧测试就会开始失败,而测试代码中乱七八糟的东西将阻碍代码再次通过。于是,测试变得就像是不断翻番的债务。
随着版本递进,团队维护测试代码组的代价也在上升。最终,它变成了开发者最大的抱怨对象。当经理们问及为何超支如此巨大,开发者们就归咎于测试。最后,他们只能扔掉了整个测试代码组。
但是,没有了测试代码组,他们就失去了确保对代码的改动能如愿工作的能力。没有了测试代码组,他们就无法确保对系统某个部分的修改不会影响到系统的其他部分。故障率开始增加。随着并非出自有意的故障越来越多,他们开始害怕做改动。他们不再清理生产代码,因为他们害怕修改带来的损害多于收益。生产代码开始腐坏。最后,他们只剩下没有测试、纷乱而缺陷缠身的生产代码,沮丧的客户,还有对测试的失望。
在某种意义上,他们说对了。测试的确让他们失望。不过是他们自己决定让测试变得乱七八糟的,而那正是失败的根源。如果他们保持测试整洁,测试就不会令他们失望。我可以拍着胸脯这么说,因为我曾经参与并指导了多个凭借整洁单元测试获得成功的团队。
故事的寓意很简单:测试代码和生产代码一样重要。它可不是二等公民。它需要被思考、被设计和被照料。它该像生产代码一样保持整洁。
《代码整洁之道》讲述的大多是一些编程细节和好习惯,并不是那种让人醍醐灌顶的书,它更像是一个指引,一本避坑指南,Martin将其从业多年遇到的一些好习惯和坏习惯列举出来一一对比,告诉读者哪些习惯应该坚持,哪些习惯应该摒弃。
比如笔者在阅读之前,确实受到一些流言影响,认为注释越多越好,越细越好。Martin告诉我,注释并不全然的好,程序员维护程序的同时往往不会花时间维护注释,导致注释说谎;以及一些喃喃自语或抖机灵的注释实际上会影响代码的阅读。有的源代码中在大括号‘}’旁添加注释,表示它是哪一个条件的闭括号,在阅读本书之前,我可能认为这是一种好习惯,甚至我可能会效仿,在阅读本书后我知道这也是一种冗余的代码,并不值得坚持。
再比如有的源代码中采用长长的链式调用,作者甚至会鼓吹“我只写了一行代码便实现了此功能”,Martin告诉我,这样容易导致“火车失事”,正确的做法是遵守“得墨忒定律”:适当拆解链式调用,只和朋友谈话,不和朋友的朋友谈话,使得代码阅读和调试都更方便。
再比如笔者在学习写测试代码时,确实如Martin所唾弃的那样,不管不顾测试代码的结构,认为有测试好过没测试。Martin告诉我,测试不是“二等公民”,它同样应该保持良好的结构,以保证测试代码的可维护。
Martin在本书中告诉我们正确的代码、好的代码应该怎样怎样,这样即使我们在工作中,在公司凌乱不堪的源码中深陷泥淖,心中仍然知道代码的伊甸园长什么样。有了这样一个清晰的目标,我们就能一步一步地向着它前进,而不至于走偏方向。或许这也是译者将其译作“道”的原因吧。
最重要的是,Martin向我们传递出自己“不向肮脏代码低头”,用他近乎偏执的决心清理代码、重构代码,保证它的整洁。阅读本书时,笔者心中充满了感动与敬畏,真切地感受到一位程序大师的工匠之心,我们要像照顾孩子一样照顾代码,像热爱生命一样热爱代码。
以上,就是笔者对《代码整洁之道》的阅读分享。