The Clean Coder《代码整洁之道:程序员的职业素养》
罗伯特 C. 马丁
译者序 1:享受职业素养
在你过去的工作中,遭遇过哪些印象深刻的困难,最后是怎么解决的?
你遇到的问题可能很容易也可能很难,但我看重的并不是问题的难度,而是解决问题的方式、步骤以及反思的程度。
技术人员往往太容易说“是”,总是在没有明确目标和期限的情况下,就草率给出了确认的答复,却不将其视为自己的承诺。屡见不鲜的项目延期,有相当原因就是在这种不负责任的情况下说“是”所致。
之所以会搞砸,因为开发人员没有坚决抵制各种不专业的需求(比如一些无关紧要但成本巨大的需求),抵制各种不专业的行为(比如为了赶工期而降低对程序质量的要求)
有时候,获取正确决策的唯一途径,便是勇敢无畏地说出“不”字
花三分的力气去抵制无理的需求,可以节省十分甚至二十分的开发时间
约定共同认可的验收测试标准,并在开发过程中保持沟通。
我曾经尝试在与业务部门确定目标原型之后,要求对方指派对接人在 IT 部坐班,负责协商、跟进整个开发流程,确认每一点修改。这样既保证最终结果符合业务部门的需求,又提高了开发人员的工作效率,综合来看成效非常显著。
译者序 2:负阴抱阳,知行合一
美的东西比丑的东西创建起来更廉价,也更快捷。
构建、维护一个美的软件系统所花费的时间、金钱都要少于丑的系统。
……美的系统是灵活、易于理解的,构建、维护它们就是一种快乐
序
技术人员不再找借口拖延,而是勇担重任
不再推卸估算工作或置身事外让其他人来做计划(然后对计划抱怨不休),而是真正做到了自组织,并做出了郑重承诺。
当程序员因为运维方面的问题受阻时,他们会打电话给系统管理员,之后,系统管理员就会马上着手清除障碍。
技术人员现在不会说“我们尽力而为吧”,而会代之以“这是我们的承诺; 如果你想调整目标,请随时联系我们”。
前言
在本书的实务性意见背后,隐隐体现出一种奋力突破的积极态度。
身为一名工程师,你比任何管理者可能都了解得更透彻。了解这些也意味着你肩负着要敢于行动的重大责任。
第 1 章 专业主义
“专业主义”有很深的含义,它不 但象征着荣誉与骄傲,而且明确意味着责任与义务。
实际上,专业主义的精髓就在于将公司利益视同个人利益。 看到了吧,“专业主义”就意味着担当责任。
我曾因不负责任尝尽了苦头,所以明白尽职尽责的重要意义。
我其实是想告诉你,要对自己的不完美负责。 代码中难免会出现 bug,但这并不意味着你不用对它们负责; 没人能写出完美的软件,但这并不表示你不用对不完美负责。
所谓专业人士,就是能对自己犯下的错误负责的人,哪怕那些错误实际上在所难免。
雄心勃勃的专业人士们,你们要练习的第一件事就是“道歉”。
你不能一而再、再而三地犯相同的错误。 职业经验多了之后,你的失误率应该快速减少,甚至渐近于零。 失误率永远不可能等于零,但你有责任让它无限接近零。
发布软件时,你应该确保 QA 找不出任何问题。
有些家伙会把 QA 当作啄木鸟看待。 他们把自己没有全盘检查过的代码发送过去,想等 QA 找出 bug 再反馈回来。
你怎么知道代码能否常运行呢? 很简单,测试! 一遍遍地测,翻来覆去、颠来倒去地测,使出浑身解数来测!
如果不停地花时间做测试,你就没时间写别的代码了。 言之有理!所以要实行自动化测试。 写一些随时都能运行的单元测试,然后尽可能多地执行这些测试。
要用这些自动化单元测试去测多少代码呢? 还要说吗?全部!全部都要测!
我是在建议进行百分百测试覆盖吗? 不,我不是在建议,我是在要求! 你写的每一行代码都要测试。完毕!
唯一的解决办法就是要设计易于测试的代码,最好是先写测试,再写要测的代码。
当然,也不排除有些系统因其任务极其关键特殊, 不能只靠简短的自动化测试来判断软件是否已经足够高质量,是否可以投入使用。 而且,作为开发人员,你需要有个相对迅捷可靠的机制,以此判断所写的代码可否正常工作,并且不会干扰系统的其他部分。 因此,你的自动化测试至少要能够让你知道,你的系统很有可能通过 QA 的测试。
所有软件项目的根本指导原则是,软件要易于修改。 如果违背这条原则搭建僵化的结构,就破坏了构筑整个行业的经济模型。
简言之,你必须能让修改不必花太高代价就可以完成。
如果你希望自己的软件灵活可变,那就应该时常修改它!
要想证明软件易于修改,唯一办法就是做些实际的修改。 如果发现这些改动并不像你预想的那样简单,你便应该改进设计,使后续修改变简单。
该在什么时候做这些简单的小修改呢? 随时! 关注哪个模块,就对它做点简单的修改来改进结构。 每次通读代码的时候,也可以不时调整一下结构。
这一策略有时也叫“无情重构”。
对每个模块,每检入一次代码,就要让它比上次检出时变得更为简洁。
每次读代码,都别忘了进行点滴的改善。
这完全与大多数人对软件的理解相反。 他们认为对上线运行的软件不断地做修改是危险的。 错!让软件保持固定不变才是危险的! 如果一直不重构代码,等到最后不得不重构时,你就会发现代码已经“僵化了”。
如果你有一套覆盖了全部代码的自动化测试,如果那套测试可以随时快速执行,那么你根本不会害怕修改代码。
专业开发人员对自己的代码和测试极有把握,他们会极其疯狂随意地做各种修改。
你应该计划每周工作 60 小时。 前 40 小时是给雇主的,后 20 小时是给自己的。 在这剩余的 20 小时里,你应该看书、练习、学习,或者做其他能提升职业能力的事情。
确实,行业正迅猛发展,而有趣的是,从多个方面来看,这种进展都只是很浅层的。
我们不再需要为拿到编译结果苦等上 24 小时,我们也已经可以写出 GB 级别的系统,我们置身覆盖全球的网络之中,各种信息唾手可得。
但另一方面,我们还是跟 50 年前一样,写着各种 if 和 while 语句。所以,改变说多也多,说少也少。
总的来说,那些在过去 50 年中来之不易的理念,绝大部分在今天仍像过去一样富有价值,甚至宝贵了。
“不能铭记过去的人,注定要重蹈覆辙。”
下面列出了每个专业软件开发人员必须精通的事项:
-
设计模式 必须能描述 GOF 书中的全部 24 种模式,同时还要有 POSA 书(这本书有点老了)中的多数模式的实战经验。
-
设计原则 必须了解 SOLID 原则,而且要深刻理解组件设计原则。
-
方法 必须理解 XP、Scrum、精益、看板、瀑布、结构化分析及结构化设计等。
-
实践 必须掌握测试驱动开发、面向对象设计、结构化编程、持续集成和结对编程。
-
工件 必须了解如何使用 UML 图、DFD 图、结构图、Petri 网络图、状态迁移图表、流程图和决策表。
不写代码的架构师必然遭殃,他们很快会发现自己跟不上时代了; 不学习新语言的程序员同样会遭殃,他们只能眼睁睁看着软件业一路发展,把自己抛在后面; 学不会新规矩和新技术的开发人员更可怜,他们只能在日渐沦落的时候看着身边 人越发优秀。
真正的专业人士往往勤学苦干,以求得自身技能的纯熟精炼。 只完成日常工作是不足以称为练习的,那只能算是种执行性质的操作,而不是练习。
那么软件开发者该怎样来不断训练自己呢? 我常用的一个技巧是重复做一些简单的练习,如“保龄球游戏”或“素数筛选”,我把这些练习叫作“卡塔”(kata)。 卡塔有很多类型。 练卡塔的目的不是找出解决方法(你已经知道方法了),而是训练你的手指和大脑。
专业软件开发人员往往会更加努力地尝试与他人一起编程、一起练习、一起设计、一起计划,这样他们可以从彼此身上学到很多东西,而且能在更短的时间内更高质量地完成更多工作。
最好的办法就是与你负责指导的人交流这些内容。 这样,传道授业的同时,导师也会从中受益。
让新人融入团队的最好办法是和他们坐到一起,向他们传授工作要诀。 专业人士会视辅导新人为己任,他们不会放任未经辅导的新手恣意妄为。
每位专业软件开发人员都有义务了解自己开发的解决方案所对应的业务领域。
如果编写旅游应用程序,那么你需要去了解旅游业。你未必需要成为该领域的专家,但你仍需要用功,付出相当的努力来认识业务领域。
开始一个新领域的项目时,应当读一两本该领域相关的书,要就该领域的基础架构与基本知识作客户和用户访谈,还应当花时间和业内专家交流,了解他们的原则与价值观念。
最糟糕、最不专业的做法是,简单按照规格说明来编写代码,但却对为什么那些业务需要那样的规格定义不求甚解。 相反,你应该对这一领域有所了解,能辨别、质疑规格说明书中的错误。
雇主的问题就是你的问题。你必须弄明白这些问题,并寻求最 佳的解决方案。
开发人员之间互相认同是容易的,但把一方换成雇主,人们就容易产生“彼”“此”之分。 专业人士会尽全力避免这样的狭隘之见。
第 2 章 说“不”
“能就是能,不能就是不能。不要说‘试试看’。”
专业人士敢于说明真相而不屈从于权势。 专业人士有勇气对他们的经理说“不”。
不应该照做。只要你是一名专业人士,那就不应该照做。
奴隶没有权利说“不”。劳工或许也对说“不”有所顾虑。 但是专业人士应该懂得说“不”。事实上,优秀的经理人对于敢于说“不”的人,总是求贤若渴。因为只有敢于说“不”,才能真正做成一些事情。
我的经验是,“为什么”远不如“事实”重要。 事实是,“登录页面”还需要两周才能完成。 而为什么需要两周,则只是个细节。
如果 Mike 恰好有技术背景和好脾气去倾听理解,这些解释也许会有用。 另一种情况则是,Mike 可能会不认同 Paula 的结论,他可能会觉得 Paula 的做法不对,他可能会告诉她不用做完整的测试和代码审查,或者可以把第 1,2 步省略掉,诸如此类。 有时候,提供太多细节,只会招致更多的微观管理。
如果承诺尝试,你其实也在承诺将改变自己原来的方案。 你是在承认原来的方案中存在不足。 如果承诺尝试,你其实是在告诉他们,你有新方案。 新方案是什么? 你将对自己的行为做出哪些改变? 你说你在“尝试”,那么你的做法将会有何不同? 如果你既没有新方案,又不准备改变自己的行为,如果事事仍然都按你承诺“尝试”之前的方法去做,那么,所谓的“尝试”指的又是什么呢?
如果你此前并未有所保留,如果你没有新方案,如果你不会改变你的行为,如果你对自己原先的估计有充分的自信,那么,从本质上讲,承诺“尝试”就是一种不诚实的表现。 你在说谎。 你这么做的原因,可能是为了护住面子和避免冲突。
她在被施压修改进度预估时说“不”,在对方软硬兼施、连哄带求时仍坚持说“不”。 最重要的是,她对 Mike 的自欺欺人和不作为也大胆说“不”。 Paula 的这些举动都是出于团队整体的考虑,Mike 需要帮助,而她确实也竭尽所能来帮他。
编程经验: 大多数情况下,选用“组合”比“继承”要好。
多年的从业经验让我渐渐明白,是客户需求阻碍我写出自己想要的真正高品质的应用程序。
客户所要的任何一项功能,一旦写起来,总是远比它开始时所说的要复杂许多,但最终你还是会接下这些活。
客户总会把项目截止日期往后拖延。 他们总是想要更多的功能,他们总是提出需求变更——而且常在最后关头这么做。 而且往往客户公司的经理的人数越多,最后增加的天数越多。
尽管客户一再声明交付日期很重要,尽管他们对此表现得似乎非常迫切,但他们永远不会像你那样在乎应用程序的按时交付。
第 3 章 说“是”
做出承诺,包含三个步骤。 (1)口头上说自己将会去做。 (2)心里认真对待做出的承诺。 (3)真正付诸行动。
我们有竭力逃避承担责任的倾向。
识别真正承诺的诀窍在于,要去搜寻与 下列相似的语句: 我将在……之前……(例如,我将在周二之前完成这个任务。) 这句话的关键在哪里呢? 你对自己将会做某件事做了清晰的事实陈述,而且还明确说明了完成期限。 那不是指别人,而是说的自己。 你谈的是自己会去做的一项行动,而且,你不是可能去做,或是可能做到,而是必须做到。
如果最终目标依赖于他人,那么你就应该采取些具体行动,接近最终目标。
有一点相当重要:如果你不尽早告诉他人可能的问题,就错失了让他们帮助你达成目标、兑现承诺的机会。
在碰到不确定的事件时,应该名确表达不确定的含义,如果可以的话,进一步该个合理的区间范围,给领导一个提前的心理预期。
Marge:“Peter,周五前你能完成对评价引擎的修改吗?” Peter:“可能可以,但也可能得到下周一。” Marge:“包括文档吗?” Peter:“写文档要再多花上几个小时,这样的话,有可能下周一可以完成,不过也可能会到下周二。”
这段对话里,Peter 的措辞更为实诚。 他清楚地向 Marge 表达了自己的不确定感。 Marge 或许能够应付得了这种不确定,但也可能无法接受。
这段对话里,Peter 的措辞更为实诚。 他清楚地向 Marge 表达了自己的不确定感。 Marge 或许能够应付得了这种不确定,但也可能无法接受。
专业人士不需要对所有请求都回答“是”。 不过,他们应该努力寻找创新的方法,尽可能做到有求必应。
第 4 章 编码
我发现,要精熟掌握每项技艺,关键都是要具备“信心”和“出错感知”能力。
如果感到疲劳或者心烦意乱,千万不要编码。 强而为之,最终只能再回头返工。
疲劳的时候,千万不要写代码。 奉献精神和职业素养,更多意义上指要遵循纪律原则而非成为长时间工作的工作狂。要确保自己已经将睡眠、健康和生活方式调整到最佳状况,这样才能做到在每天的 8 小时工作时间内全力以赴。
在办公室里花一个小时解决私人问题,是一件令人惭愧的事。 专业开发人员善于合理分配个人时间,以确保工作时间段中尽可能富有成效。
在家中时就应该专门安排时间去解决焦虑,这样就不会把焦虑情绪带到办公室里。
问题在于,在流态区状态下,你其实放弃了顾及全局,因此,你很可能会做出一些后来不得不推倒重来的决策。 在流态区写代码可能会快些,但是后面你将不得不更多地回头重新审视这些代码。
结对编程最大的一个好处在于,结对中的任一方都不可能进入流态区。 流态区是一种与世隔绝的状态,而结对则要求持续密切地进行沟通。
我经常听到关于结对编程的抱怨便是,结对会阻碍人们进入流态区。 很好!流态区正是要避免进入的状态。
有时候流态区正是你希望进入的状态。 这个时候,就是当你潜心练习的时候。
音乐并没有帮助我专注于编码。 事实上,听音乐似乎消耗了一部分宝贵的脑力资源,而这些资源本该用于编写设计良好的整洁代码。
结对是用以应对中断的一种好方法。 当你接答电话或回答其他同事的问题时,结对搭档能够维护住中断处的上下文。
另一种很有帮助的方法便是采用 TDD。失败的测试能帮你维护住编码进度的上下文。当处理完中断重新回去时,你很清楚下一步任务便是让这个失败的测试通过。
当然,中断无法避免,总有干扰会打断你、消耗你的时间。 发生这种情况时要记住一点,也许下次也会轮到你去打断别人请求帮助。 因此,礼貌地表现出乐于助人的态度才是专业的态度。
哪些原因会导致这些阻塞呢? 如果睡眠不足,我就什么代码也写不出来。 其他因素还包括焦虑、恐惧和沮丧等。
这个方法便是:找一个搭档结对编程。
出于某些原因,软件开发人员会认为调试时间并非编码时间。 他们认为存在调试时间是天经地义的,调试不等于编码。
之所以能够显著降低调试时间,是因为我采用了“测试驱动开发”这一实践,
不管是否采纳 TDD 或其他一些同等效果的实践,衡量你是否是一名专业人士的一个重要方面,便是看你是否能将调试时间尽量降到最低。 绝对的零调试时间是一个理想化的目标,无法达到,但要将之作为努力方向。
经常重新返工的医生或律师会被认为不专业。 同样,制造出许多 bug 的软件开发人员也不专业。
软件开发是一场马拉松,而不是短跑冲刺。 你无法全程一直以最快的速度冲刺来赢得比赛,只有通过保存体力和维持稳定节奏来取胜。
我也曾经在洗澡时解决了大量问题。 也许是清晨的水流能够将我彻底唤醒,使我可以深入盘点昨晚睡觉时大脑中浮现的所有解决方案。
埋头忙于解决问题时,有时候可能会由于和问题贴得太近,无法看清楚所有的可选项。 由于大脑中富有创造性的部分被紧张的专注力所抑制,你会错过很棒的解决方案。
管理延迟的诀窍,便是早期检测和保持透明。 最糟糕的情况是,你一直都在告诉每个人你会按时完成工作,到最后期限来临前你还在这样说,但最终你只能让他们大失所望。
使用三个考虑到多种因素的期限:乐观预估、标称预 估、悲观预估。
把全部这三个数字呈现给团队和利益相关者,并每天修正这些数字。
其实快速冲刺是做不到的。 你无法更快地写完代码。 你无法更快地解决问题。 如果试图这么做,最终只会让自己变得更慢,同时也只能制造出一堆混乱,让其他人也慢下来。 因此,必须明白告诉老板、团队和利益相关方,让他们不要抱有这种期望。
不应该采用额外加班加点工作的方案,除非以下三个条件都能满足: (1)你个人能挤出这些时间; (2)短期加班,最多加班两周; (3)你的老板要有后备预案,以防万一加班措施失败了。
最后一条至为关键。 如果老板无法向你清楚说明加班方案失败的后备预案,那么你就不该同意接受加班方案。
在程序员所能表现的各种不专业行为中,最糟糕的是明知道还没有完成任务却宣称已经完成。
但是随着经验渐长,你会开始意识到把这些 if 和 while 语句组装在一起的方式十分重要。
不能期望将它们简单混在一起就能得到最好的代码。
相反,必须小心谨慎地将系统分解为易于理解的小单元,同时使这些单元之间的关系越少越好,这并非易事。
事实上,仅凭一己之力无法写出优秀的代码。 既使你的技能格外高超,也肯定能从另外一名程序员的思考与想法中获益。
互相帮助是每个程序员的职责所在。 将自己封闭在格子间或者办公室里与世隔绝,有悖于专业的职业精神。
事实上,作为专业人士,要以能够随时帮助别人为荣。
帮助别人的时候,你可以坐下来和他一起写代码,为此需预留出一个小时甚至更长的时间,当然实际也许没那么久,但是不要让自己看起来十分仓促,仿佛只是随便应付。 要全情投入到任务中。 当你离开时,可能会发现自己从中收获的东西比给予的还要多。
要记住,如同要以乐于助人为荣一样,也要以乐于接受别人的帮助为荣。
如果帮助唾手可得却让自己一个人堵在那儿,是很不专业的表现。
除了自身的内驱力和资深导师的有效辅导之外,没有东西能将一名年轻的软件开发人员更快地提升为敏捷高效的专业人士。 因此,再强调一次,花时间手把手地辅导年轻程序员是资深程序员的专业职责所在。
向资深导师寻求辅导也是年轻程序员的专业职责。
第 5 章 测试驱动开发
测试驱动开发”(TDD)自在行业中首次亮相,至今已经有十余年了。 它最早是极限编程(XP)运动的一部分,但此后已经被 Scrum 和几乎所有其他敏捷方法所采纳。 即使是非敏捷的团队也在实践 TDD。
结论很清楚,TDD 的确切实可行,并且,每个开发人员都要适应和掌握 TDD。
- 在编好失败单元测试之前,不要编写任何产品代码。
- 只要有一个单元测试失败了,就不要再写测试代码; 无法通过编译也是一种失败情况。
- 产品代码恰好能够让当前失败的单元测试成功通过即可,不要多写。
看到混乱的函数时, 你的第一反应是:“真是一团糟,这个函数需要整理。” 你的第二反应是:“我不会去碰它!” 为什么? 因为你知道,如果去动它,就要冒破坏它的风险; 而如果你破坏了它,那么它就缠上你了。
这是 TDD 最强大之处。 拥有一套值得信赖的测试,便可完全打消对修改代码的全部恐惧。 当看见糟糕的代码时,就可以放手整理。 代码会变得具有可塑性,你可以放心打磨出简单而满意的结果。
遵循 TDD 三项法则的话,所编写的每个单元测试都是一个示例,用代码描述系统的用法。
单元测试即是文档。它们描述了系统设计的最底层设计细节。 它们清晰准确,以读者能够理解的语言写成,并且形式规整可以运行。
测试代码的一个问题是必须隔离出待测试的代码。 如果一个函数调用了其他函数,单独测试它通常会比较困难。 为了编写测试,你必须找出将这个函数和其他函数解耦的办法。 换言之,测试先行的需要,会迫使你去考虑什么是好的设计。
但是事后写的测试只是一种防守。 而先行编写的测试则是进攻,事后编写测试的作者已经受制于已有代码,他已经知道问题是如何解决的。 与采用测试先行的方式编写的测试代码比起来,后写的测试在深度和捕获错误的灵敏度方面要逊色很多。
TDD 是专业人士的选择。 它是一项能够提升代码确定性、给程序员鼓励、降低代码缺陷率、优化文档和设计的原则。 对 TDD 的各项尝试表明,不使用 TDD 就说明你可能还不够专业。
尽管 TDD 有诸多优点,但是它既非宗教信仰,也非魔力公式。
即使做到了测试先行,仍有可能写出糟糕的代码。 没错,因为写出的测试代码可能就很糟糕。
在某些场合照这三项法则去做会显得不切实际或不合适。 这种情况很少,但确实存在。
第 6 章 练习
专业人士都需要通过专门训练提升自己的技能,无一例外。乐手练习音阶,球员练习绕桩,医生练习开刀和缝针 ,律师练习论辩,士兵练习执行任务。要想表现优异,专业人士就会选择练习。
无论是搏斗还是编程,速度都来源于练习。
练习卡塔并不是为了舞台表演。训练意识和身体是为了真正搏斗时能够正确应对。 它的目的在于,在需要的时候,可以凭借本能完美出招。
编程卡塔的最终目标,也是逐步练习以达到纯熟。 反复的练习会训练大脑和手指如何动作和反应。 在不断练习当中,你或许会发现动作的细微进步,或者解决问题效率的小幅提升。
和习武者一样,程序员应该懂得多种不同的卡塔,并定期练习,确保不会淡化或遗忘。
或者一个简单问题,一个人写单元测试,另一个人写程序通过单元测试,然后交换角色。
写单元测试的程序员会极力控制解决问题的方式,他也有足够的空间来施加限制:如果程序员选择实现一个排序算法,写测试的人可以很容易地限制速度和内存,给同伴施压。这样整个游戏就非常考验人……也可以说是非常有趣。
职业程序员通常会受到一种限制,即所解决问题的种类比较单一。 老板通常只强调一种语言、一种平台,以及程序员的专门领域。 经验不够丰富的程序员,履历和思维中都存在某种贻害无穷的盲区。
保持不落伍的一种方法是为开源项目贡献代码,就像律师和医生参加公益活动一样。 开源项目有很多,为其他人真正关心的开源项目做一点贡献,应该可以算是提升技能的最好办法了。
无论如何,专业人士都需要练习。
练习的时候你是赚不到钱的,但是练习之后,你会获得回报,而且是丰厚的回报。