内容没毛病,但排版看着累人,就像是用刚安装好的 Obsidian 预览一样。
推荐序
如果你在寻求帮助时感受到了敌意,大可不必心烦意乱,因为这在每个人身上都可能发生。不要让某一次糟糕的遭遇阻断了你再次寻求帮助的欲望。但总是打断他人的工作并非是合适的,
译者序
似乎从来都是程序员为他人撰写 README,从来没有人为程序员撰写过 README。
在模棱两可的情况下,你会主动寻求帮助并得到明确的结果。你能以建设性的方式提出问题和定义课题。
重点并不是要写一份完美的文档,而是要写得足够多,以引发讨论,充实细节。这是坎宁安定律的一个应用,该定律认为:“在互联网上获得正确答案的最好方法并不是提出问题,而是发布错误的答案。”
定律的作者同样是 Wiki 软件的发明者。
过度集中在细枝末节上的讨论总是会很冗长,这种现象被称为“自行车棚”(bike-shedding)效应。
第2章 步入自觉阶段
马丁·M.布罗德威尔在其文章《为学而教》(“Teaching for Learning”)中定义了能力的4个阶段:“无意识的无能力”(unconscious incompetence)、“有意识的无能力”(conscious incompetence),“有意识的有能力”(conscious competence)和“无意识的有能力”(unconscious competence)。具体说来,无意识的无能力意味着你无法胜任某项任务,并且没有意识到这种差距。有意识的无能力意味着你虽然无法胜任某项任务,但其实已经意识到了其中的差距。有意识的有能力意味着你有能力通过努力完成某项任务。最后,无意识的有能力意味着你可以很轻松地胜任某项任务。
切记善用个人的时间——虽然说持续进步非常重要,但是把所有清醒的时间都花在工作上是不健康的。
错误难免会发生。每名工程师都有类似的故事。尽你所能,努力理解你在做什么,但要知道这种事情总会发生。
错误是不可避免的。成为一名软件工程师的路途艰辛,我们有时会失败。这几乎是所有人都知道的事情。降低系统风险并使这些错误不那么致命是你的管理者和团队的工作。如果你失败了,也不要被击垮:写下经验教训,然后继续前行。
一个看起来有些笨拙但非常实用的方法是在程序执行的入口处输出一行特殊的语句。这样你就可以很容易地知道现在运行的程序究竟是你修改过的,还是原来的旧版本。你会节省出那些跟踪程序“谜之行为”的时间。“谜之行为”通常都是由正在被调用的程序是旧版且你修改的内容没有生效造成的。
是这样的,我一般是在原日志上随机加几个字符(当然测试后会删掉)。
每周都花一部分时间去阅读。可供阅读的内容有很多:团队文档、设计文档、代码、积压的任务票、书籍、论文和技术网站。不要试图一下子把所有东西都读完。请从团队文档和设计文档入手。这些文档会就事情是如何组合在一起的给你一个整体的概念。要特别注意那些关于如何权衡取舍和背景的讨论。
罗恩·杰弗里斯所说“代码从不说谎。注释有时却会”(Code never lies. Comments sometimes do
不要只读你自己的代码库,还要去阅读高质量的开源项目,特别是那些你使用的类库。不要像阅读小说一样从前到后地通读代码:请利用你的 IDE 来浏览代码。为关键的操作绘制控制流和状态图。仔细研究代码的数据结构和算法。注意那些临界值的处理。留意那些惯用写法和风格,也就是去学习“本地方言”(local dialect)。
出版物和在线资源是互补关系。阅读书籍和论文是深入研究某个主题的很棒的途径。出版物大多很可靠,只是有些过时。在线资源则正好相反,不那么可靠,但很能跟上潮流。在实施黑客新闻(hacker news)中的最新想法之前要记得“踩下刹车”,因为采用保守一些的技术选型是有益处的。
他开始对写这个东西的人有些敬畏,因为这个人可以在没有合理的变量命名的情况下,把所有的东西都记在脑子里。最后,在午餐时,德米特里流露出了敬佩之情。他的同事看着他,仿佛他长出了第二个脑袋。“德米特里,我们没有原始的代码。你正研究的东西是反编译器的输出结果。正常人是不会这样写代码的!”
哈哈哈哈哈哈
你通常可以用1.5倍速甚至2倍速观看视频,以节省时间,但不要被动地观看。你需要做笔记来帮助记忆,并学习任何不熟悉的概念或术语。
结对编程(pair programming)也是一种很好的学习方式。两名工程师一起写代码,轮流打字。这需要一些时间来适应,但这是相互学习最快的方式之一。这种技术的倡导者还声称,它可以提高代码质量。如果你的队友愿意,我们强烈建议你尝试一下。结对编程也不仅仅是针对初级工程师的,所有级别的队友都可以从中受益。
不要根据你认为你需要学习的领域来选择项目。找到你有兴趣去解决的问题,并使用你想学习的工具来解决这些问题。一个可以从内激励你的目标会让你更长时间地参与,你也会学到更多
可以试试
在提出问题时描述你已经知道的情况。不要只是分享你的原始笔记。简要地描述你所做的尝试和发现,这表明你已经花了很多时间去试图自己解决这个问题。这样做也会给别人一个回答你的起点。
在网络通信中,组播(multicast)是指将消息发送到一个组而不是个人目标;异步(asynchronous)是指可以稍后处理的消息,而不需要立即响应。这些概念也适用于人们之间的通信。
他们确信自己的想法是正确的。他们的默认模式是直接回绝或无视反馈。拒绝所有的建议会亮起一盏巨大的红灯:完全自信标志着盲点。
混乱的代码是变化的自然副作用,不要把代码的不整洁归咎于开发者。这种走向无序的趋势被称为软件的熵(software entropy)。
第3章 玩转代码
技术债(technical debt)是造成软件的熵的一个主要原因。技术债是为了修复现有的代码不足而欠下的未来工作。与金融债务一样,技术债也有“本金”和“利息”。本金是那些需要修复的原始不足。利息是随着代码的发展没有解决的潜在不足,因为实施了越来越复杂的变通方法。随着变通办法的复制和巩固,利息就会增加。复杂性蔓延开来,就会造成 bug。未支付的技术债很常见,遗留代码里有很多这样的债务。
不要等到世界都停转一个月了才去解决问题。相反地,要边做边解决,着手去做小幅的重构。在小幅的、独立的提交(commit)和拉动请求(pull request)中推动问题的修改。
在短期内,偿还技术债会拖慢交付特性的速度,而承担更多的技术债会加速交付。长期来看,情况正好相反:偿还技术债会加快交付的速度,而承担更多的债务则会减缓交付。
不要把你的呼吁建立在价值判断上(“这代码又老又难看”),将重点放在技术债的成本和修复它带来的好处上。要具体,如果有人要求你证明这种改动会带来哪些好处,不要感到惊讶。
打破依赖关系意味着改变代码结构,使其更容易测试。你只有改变代码,才能将你的测试挂起来,并提供合成的输入。这些代码变更一定不要改变原有的代码行为。
互联网上的编程传说经常引用童子军的原则:“住过的营地要比住之前更干净”。就像营地一样,代码库是共享的,如果能继承一个清爽的代码库,善莫大焉。将同样的哲学——“过手的代码要比之前更干净”应用于代码会帮助你的代码随着时间的推移而变得更好。在不影响整个项目持续运转的情况下要持续地重构工程,这样重构的成本就会平摊在多次的版本更迭中。
尽量将清理代码的提交和改变行为的提交各自分开。分开提交可以让你在不会丢失针对代码清理的提交的基础上,更容易地去恢复代码变更。较小的提交也更容易针对变更的部分进行评审。
你的团队可能会决定忽略重构,而去开发新特性,正是这样的决定增加了团队的技术债,但这可能同时也是正确的决定。重构的成本也可能超过其价值。正在被替换的旧的、废弃的代码不需要被重构,同理,低风险或很少被触及的代码也不需要。在重构的时候要务实。
很多“精英程序员”会认为使用集成开发环境(IDE)是一种耻辱。他们认为从编辑器中获得“帮助”是一项弱点,并迷恋Vim或Emacs,所谓“一种更文明时代的优雅武器”。这是无稽之谈。利用一切你所拥有的工具。如果你的编程语言有一个优秀的IDE,就去使用它。
在开发过程中,尽早并频繁提交你的修改。频繁地提交可以显示出代码随着时间的推移而发生的变化,方便你撤销修改,并将之作为一份远程备份。
压缩后的提交信息应该遵循你的团队的惯例。一个常见的做法是在提交信息前加上一个问题编号作为前缀:“[MYPROJ-123]让后端与数据库的处理生效”。将提交信息与问题联系起来,可以让开发人员找到更多的背景,并允许使用脚本和工具。如果没有既定的规则,可以遵循克里斯·比姆斯的建议。
- 用一个空行将标题与正文分开。
- 标题行限制在50个字符以内。
- 标题行要大写。
- 不要以句号结束标题行。
- 在标题行中使用命令式语气。
- 将正文限制在72个字符之内。
- 用正文解释修改的内容和原因,而不解释如何修改。
本·霍洛维茨在他的《[[创业维艰]]:如何完成比难更难的事》(The Hard Thing About Hard Things,已由中信出版集团于2015年引进出版)一书中说: 任何技术创业公司必须做的主要的事情是建立一个产品,这个产品在做某件事情时至少要比目前流行的方式好十倍。两倍或三倍的改进不足以让人们快速或大量地转向新事物。
如果你想重构代码或重定义标准,你的改进就必须是一个数量级层面的改进。小的收益是远远不够的,因为成本太高了。大多数工程师低估了惯例的价值,而高估了忽视惯例的收益。重构、打破惯例或在技术栈中添加新技术时要谨慎。把重构代码的机会留给高价值的情况。在可能的情况下使用保守一些的技术。不要忽视惯例,即便你不同意它,也要避免对代码硬分叉。
网上“大火”的东西相比,现有的代码看起来有些过时。然而,成功的公司留用旧的代码——比如旧的类库和旧的模式——是有原因的:成功需要时间,而在技术上大动干戈会让人分心。
所有的技术都会发生故障,但旧的东西以可预测的方式发生故障,新东西往往会以令人惊讶的方式发生故障。
重构工作常常升级为全方位的重写。重构现有的代码令人望而生畏,为什么不扔掉旧系统,从头开始重写一切呢?把重构看作最后的手段——这是从多年的经验中艰难得出的建议。
第4章 编写可维护的代码
对象模式会使用一个对象来代替空值。这种模式的一个例子:对于某个搜索方法,当它没有找到任何结果时,会返回一个空列表而不是null。返回空列表可以允许调用者安全地遍历这个返回值,而不需要特别的代码来处理空结果集。
限制变量可以被赋的值。例如,只有几个可能的字符串的变量应该是一个Enum而不是一个String。限制变量将确保意外的值会立即失效(甚至可能无法编译),而不是任由其引发潜在的bug。在定义变量时,尽可能使用最具体的类型。
使用前置条件和后置条件的方式来校验方法中输入的变量。当你使用的数据类型不能完全地捕获有效的变量值时,可以使用校验前置条件的类库和框架。大多数语言都有类似的库,其中有像 checkNotNull 这样的方法或像 @Size(min=0, max=100) 这样的注解。尽可能地限制可能的取值。校验输入的字符串是否符合预期的格式,并记得处理前面或后面的空格。校验所有的数字是否在适当的范围内:如果一个参数应该大于0,那就要确保它大于0;如果一个参数是 IP 地址,那就要检查它是否是一个有效的IP地址。
精确的异常使代码更容易使用。尽可能地使用内置的异常,避免创建通用的异常。使用异常处理来应对故障,而不是控制应用程序的运行逻辑。
遵循“早抛晚捕”的原则来处理异常。“早抛”意味着在尽可能接近错误的地方引发异常,这样开发人员就能迅速地定位相关的代码。等待抛出异常会使我们更难找到错误实际发生的位置。当一个错误发生之后,却在抛出异常之前执行了其他代码,你就有可能触发第二个错误。如果第二个错误抛出了异常,你就不知道第一个错误其实已经发生了。跟踪这类错误是令人“抓狂”的——你修复了一个bug,却发现真正的问题出在上游。“晚捕”意味着在调用的堆栈上传播这个异常,直到你到达能够处理异常的程序的层级。假想一个应用程序试图向一个已满的磁盘写入数据,下一步操作有许多可能性:阻塞和重试,异步重试,写入另一块不同的磁盘,提醒用户甚至程序崩溃。适当的反应取决于该应用程序的具体情况。一个数据库的预写日志必须被写入,而一个文字处理程序的后台可以延迟保存。能够在上述选择中做出决定的代码段很可能与遇到磁盘已满情况的底层类库中间相差了好几层,所有的中间层都需要将异常向上传播,而不是试图过早地进行补救处理。
当调用可能抛出异常的代码时,要么完全地处理它们,要么将它们在堆栈中进行传播。
谨慎的做法是使用一种叫作“退避”(backoff)的策略。退避会非线性地增加休眠时间(通常使用指数退避,如(retry number)^2)。
处理重试的最好方法是构建幂等系统。一个幂等的操作是可以被进行多次并且仍然产生相同结果的操作。将一个值添加到一个集合中就是一个幂等操作。无论该值被添加了多少次,只要它被添加过,它就在集合中存在。通过允许客户端单独为每个请求提供一个唯一ID的方式,远程API就可以变为幂等API。当客户端重试时,它提供的唯一ID与失败时的相同。如果该请求已经被处理过了,服务器可以移除重复的请求。让你的所有操作都成为幂等操作,这可大大简化系统的交互,同时也可消除一大类潜在的错误。
你必须为每条日志消息提供一个适当的严重程度,这样日志级别才有用。虽然日志级别并没有完全统一的标准,但下面的分级很常见。TRACE:这是一个极其精细的日志级别,只对特定的包或类开放,在开发阶段之外很少使用这个级别。如果你需要逐行的日志或数据结构临时信息,那么可以使用这个级别。如果你发现自己经常使用TRACE,那你应该考虑用一个调试器来代替它去检查代码。DEBUG:这个日志级别多用于那些只在调查产品出故障时有用,但在正常操作中没有用的日志。如果输出了很多这个级别的日志,可以将这些日志调整到TRACE级别。INFO:这个日志级别一般用于输出应用程序运转良好的日志,不应该用于输出任何问题的指示。像“服务开始”和“在端口5050上监听”这样的应用程序的状态信息可以应用这个日志级别。
- INFO是默认的日志级别。不要用INFO级别发出无意义的日志,“以防万一”类的日志应该放在TRACE或DEBUG中。INFO级别的日志应该在正常操作中告诉我们一些有用的信息。
- WARN:这个日志级别一般用于提示那些潜在问题。一个资源已经接近其容量上限,就应该是一个WARN。每当你记录一个WARN时,应该对应一个你希望看到这个日志的人去采取的具体行动。如果这个WARN没有可操作性,就应该把它记录到INFO级别。
- ERROR:这个日志级别表明正在发生需要注意的错误。一个无法写入的数据库通常需要一个ERROR日志。ERROR日志应该足够详细,以便诊断问题。记录明确的细节,包括相关的堆栈信息和软件正在执行的操作。
- FATAL:这属于“最后一搏”类型的日志信息。如果程序遇到非常严重的情况,必须立即退出,就可以在FATAL级别上记录关于问题原因的信息。应包括该程序状态的上下文内容,恢复或诊断相关数据的位置也应该被记录下来。
所谓原子日志,就是指在一行消息中包含所有相关的信息。原子日志与日志聚合器搭配使用更方便。不要假设日志会按照特定的顺序被看到,许多操作工具会重新排序,甚至弃用一些消息。不要依赖系统的时间戳来排序,系统时钟可能被重置或来自不同的主机,从而造成日志信息难以理解。避免在日志信息中使用折行,许多日志聚合器会把每一个新行当作一串单独的消息。要特别确保堆栈跟踪被记录在一条消息中,因为它们在输出时经常包含折行。
大多数可视化系统提供了一系列语言的系统指标的客户端。我们将在一个简单的Python网络应用程序中使用StatsD客户端来展示系统指标库的例子。
许多有创意的好心人花了大量的时间来制作花哨的配置系统,可悲的是,配置方案越聪明,bug就越奇怪。不要在配置上搞新花样,而应该使用最简单、有效的方法。理想状态应该是单一标准格式的静态配置文件。
通常动态配置带来的收益往往比不上它引入的复杂性,你需要仔细考虑运行过程中因为各种配置变化而产生的所有影响。它还会使你更难跟踪哪项配置被改变了,谁改变了它,以及它的值是什么,这些信息在调试运维问题时可能是至关重要的。它还会增加对其他分布式系统的外部依赖性。这听起来很简单,但重新启动一个进程来获取新的配置通常在操作上和架构上都更好一些。
如果用户不得不配置大量的参数,你的系统将很难运行起来。提供良好的默认值,这样你的应用程序对大多数用户来说开箱即用。如果没有配置端口,应该默认大于1024的网络端口,因为更小的端口会受到限制。如果没有指定目录路径,那么就使用系统的临时目录或用户的主目录。
配置即代码(configuration as code,CAC)的哲学认为,配置应该受到与代码同样严格的要求。配置错误可能是灾难性的,一个错误的整数或缺失的参数就可以毁掉一个应用程序。
第5章 避免相依性地狱
任何未指定版本的依赖项不仅会拉取对bug的最新修复,还会拉取更多的东西,比如它们会拉取最新的bug、软件行为,甚至是不兼容的变化。
遵循帕累托法则,我们不建议你在刚开始的时候就对语义化版本进行过深的研究,除非这是你工作范畴中明确的一环,或者你需要更多的信息来解决某个具体的问题。
第7章 代码评审
但代码评审的价值不仅仅是让人来代替自动测试和代码质量检查工具。优秀的代码评审可以作为一个教学工具,传播认识,记录实现的决策,并提供代码的更改记录以确保安全性与合规性。
预排会议的目的是帮助你的团队理解为什么要提出修改,并给他们一个良好的心理模型,以便他们可以自行去进行详细的代码评审。
第8章 软件交付
金丝雀部署和蓝绿部署是两种非常常见的并行部署策略。
- 金丝雀部署用于处理高流量并会部署到大量实例的服务。一个新的应用程序版本被首先部署到一组受限的计算机上,全部用户中的一个小的子集会被路由到这个金丝雀版本。
- 蓝绿部署指的是运行两个不同版本的应用程序:一个是主动的,一个是被动的。
在摸黑启动模式下,应用程序的代理位于实时流量和应用程序之间。该代理重复向影子系统发出请求,对不同系统根据相同请求做出的响应进行比较,并记录差异。只有生产环境下的系统响应被发送到用户手中。这种做法允许运维人员在不影响用户的情况下观察他们在真实流量下的服务。当只有读取流量被发送到系统,而没有数据被修改时,系统被称为处于“暗中读取”模式。某个系统在暗中读取模式下运行时,可能使用与生产系统相同的数据存储。当写入请求也被发送到系统中,并且使用一个完全独立的数据存储时,它被认为处于“暗中写入”模式。
第9章 On-Call
而依赖“救火队员”的团队不会拓展自己的专业知识和提高排除故障的能力。“救火队员”的英雄主义也会导致那种修复严重的潜在问题的工作被置于次要地位,因为“救火队员”总在旁边修修补补。
第10章 技术设计流程
写作拥有一种暴露你不知道的东西的能力(在这一点上请相信我们)。迫使你自己写下你的设计,迫使你去探索问题空间,并使你的想法具体化。
传播设计知识将帮助其他人保持对系统的工作方式拥有准确的心理认知。团队将会做出更好的设计和实施决策,On-Call 工程师会正确地理解系统的行为方式,工程师们也可以利用设计文档来向他们的队友学习。
觉得自己不擅长写作的工程师可能会被写作的前景所吓倒,不要这样。写作作为一项技能,就像其他技能一样,是通过实践来进步的。充分利用写作的机会——设计文档、电子邮件、代码评审意见——努力写得清晰。
以目标受众的视角重读你所写的内容:你是否理解并不重要,重要的是他们是否能理解。文档要简明扼要。为了帮助你获得读者的视角,你需要去阅读别人写的东西。想一想你会如何编辑他们的文章:哪些是多余的,哪些还需要补充。在你的公司里寻找优秀的文档作者,并征求他们对你所写内容的反馈。
相反,当你最初做研究时,从其他团队和技术领导那里获得早期的反馈,将催生更好的设计,使他们了解你的工作,并使他们在你的设计中获益。早期参与你工作中的各方都可以在以后成为你工作的拥护者。
第11章 构建可演进的架构
保持代码简单的最简单的方法之一是避免什么代码都要写出来。告诉你自己,你不是真的需要(You ain’t gonna need it,YAGNI)。当你写代码的时候,要使用最小惊讶原则和封装原则。这些设计原则将使你的代码易于演进。
所谓最小惊讶原则非常明确:不要让用户感到惊讶,构建特性表现得要像用户最初期望的那样,具有上扬的学习曲线或奇怪表现的特性会使用户感到沮丧;同样地,不要让开发者感到惊讶,令人惊讶的代码通常晦涩难懂,这会导致复杂性。你可以通过保持代码的针对性、避免隐性知识,以及使用标准类库和模式来消除惊讶。
第13章 与管理者合作
管理者通过与其他管理者合作来进行横向管理。一名管理者有两个团队:他们所管理的所有人和管理者的同行们。管理者同行们一起配合,使团队在共同目标上保持一致。关系的维护、清晰的沟通以及合作的规划,可以确保团队有效地合作。
如果你一直对过去的 [[PPP]] 进行记录,更新 PPP 就会很容易。每当需要报告新的 PPP 时,去创建一个新的条目即可。看看你在上一次 PPP 中的问题,问问自己,其中哪些问题得到了解决,其中哪些问题还持续存在。已解决的问题放在新 PPP 的“进展”部分,持续存在的问题则继续留在“问题”部分。接下来,看一下你在上一份 PPP中 的“计划”部分。你是否完成了计划中的工作?如果是,就把它添加到新 PPP 的“进展”部分。如果不是,你是否计划在下一次 PPP 报告之前完成该任务,或者是否有什么问题阻碍了你计划工作的进展?相应地更新“计划”或“问题”部分。最后,看看你即将着手的工作和日程安排,将你在下一次 PPP 截止日之前预计要做的任何新工作都更新到“计划”部分。整个过程不应该超过 5 分钟。
第14章 职业生涯规划
成为资深工程师或主任工程师需要时间和毅力,但你可以通过对自己的职业发展负起责任来帮助自己。培养T型技能,参加工程师训练营,主导晋升过程,不要过于频繁地更换工作,并多自我调节。
软件工程有许多专业领域:前端、后端、运维、数据仓库和机器学习等。“T型”工程师在大多数领域内都能有效地工作,并且至少是某一个领域的专家。我们第一次接触到T型人才的概念是在Valve公司的《新员工手册》(Handbook for New Employees,你可以在Valve公司的官方网站上找到原文)。该手册将T型人才描述为:……这些人既是通才(在一系列广泛的有价值的事情上有很高的技能——T的顶端横线),也是专家(在某个垂直领域中成为佼佼者——T的竖线)。
塔尼娅·赖利的演讲和博文建议,如果你的管理者不认为你贡献的价值是晋升的途径,你就不要再做胶水工作了——即使它会在短期内伤害团队。这让人如鲠在喉,而且可能看起来不公平,但是让事情公平的责任在管理层,而不在你。
当然,即使在短暂的任期内,也有很好的理由去更换工作。有些公司或团队并不适合你,与其任由事情拖下去,不如迅速地摆脱糟糕的局面。特殊的机会不会在方便的时间内出现,但当它出现时,你应该对它们持开放的态度。让自己接触不同的技术栈、同事和工程师团体也有价值。工程师的工资一直在快速增长,你的公司可能不会像市场那样快速地加薪。可悲的是,换工作往往更容易赶上市场的步伐。但是,如果你仍然有适当的薪酬、成长和学习,就请坚持你现在的工作。看到团队、公司和软件随着时间的推移而不断发展是非常有价值的。
反过来说,不要待得太久。工作僵化、停滞不前是改变现状的正当理由。在一家公司工作时间长的工程师自然会成为“历史学家”,他们教导工程师事情是如何运转的、谁知道什么,以及为什么事情是按照他们的方式完成的。这样的知识是有价值的,甚至是一名主任工程师职责的一部分,但如果你的价值更多来自过去的工作而不是现在的贡献,那么它就会阻碍你的成长。更换公司并在一个新的环境中找到自己,可以重启你的成长。