前言
这本书的上一版《从小工到专家》其实我就早有耳闻了,但是当时觉得这个翻译的名字很糟糕,且周围并没有人去推荐它,我就打消了看它的念头。然后今年闲暇下来发现这本书的再版,书籍的名称也变为了《程序员修炼之道》,抱着看闲书和京东打折的契机,把它买了下来。看完两章发现自己两年前错失了一部能快速获得经验,提高生产力的的书籍。书有可前浅尝者、有可吞食者,少数需咀嚼消化,这本书我认为就属于需要咀嚼消化的那种书,帮助你提取一些之前成功项目的的原因,也可以让你思考生活与工作遇到同一类问题的处理方式。
务实的方法
人生是你的
我活着不是为了满足你的期望,正如你不是因为我的期望而活着
—— 李小龙
软件开发在任何职业列表中,都绝对是你自己最能掌控的职业之一。我们的技能供不应求,我们的知识不限于地域,我们可以远程工作。我们收入颇丰。我们真的可以做到我们任何想做的事。但是总有一些原因导致开发者们拒绝改变,他们蜷缩在那里,期盼事情会自己变好,眼睁睁的看着自己的技能过时,再去抱怨世界日新月异,工作环境糟糕。
虽然看起来作者有种何不食肉糜的感觉,但其实在现如今的行业划分里,软件开发确实是最能自己掌握的职业之一。互联网上基本上有你所需要的所有资料,而且即使你只会一种编程语言,今年你可以做金融,明年就可以无缝做购物,后年可以跑去做车载,你的职业经验仍然大部分适用。职业环境并不会对你的事业有很大的限制,能限制的只有你自己。
我的源码被猫吃了
如果你面临供应商帮不上忙这样的风险,就应该制定一个应急方案。如果磁盘挂起——你所有的源码都在里面,这就是你的错。跟你的老板说“我的源码都被猫吃了” 解决不了问题
团队信任
你的团队需要你能信赖和依赖你——你也应该同样地放心依赖他们每个人
- 不必直接掌控事情的各个方面
- 表达你的思想,说出你的想法。缔造一个以信任为基础的健康环境
承担责任
在承接需求或任务前,你必须分析超出你控制范围的风险情况,如果责任的伦理内含过于含糊,或是面对无法实现的情况,亦或是风险过大,你都有权不承担责任。在这个阶段就及时把问题抛出,重新评估任务的可行性或是明确风险情况的责任人
在决定对一个结果承担责任时,要明白你将承接相关的义务。当你犯了错误,或是做出了错误的判断,诚实地承认它,并尝试给出选择
给出选择,而非借口 给出选择并非要求你一定像上文例子里一样,做事都有 plan B, 即使在世界末日都能从兜里掏一台时空穿梭器。而是你跑过去告诉他们坏消息前,最好能解释下做些什么才能挽回局面。队友和领导都很乐于与这种人公事,他们在这种场景下能以更低的成本去参与到新问题和新需求的处理中
软件的熵
虽然软件开发不受绝大多数物理法则的约束,但我们无法躲避来自熵增加重击。熵是一个物理学术语,它定义了一个系统的“无序”总量。不幸的是,热力学法则决定了宇宙中的熵会趋向最大化。而软件开发因为各种 Bug 的修复,需求的新增和补充,软件的复杂度和无序化也在逐步增加,本小节主要是探讨如何避免软件的熵的增加
- “破窗效应”——制定了规则就要需要严格遵守,例如 Code Review 、Code Lint 软件架构 等 ,这些规则都是为了保持软件的可读性以及降低程序的复杂性,既然制定了就要好好遵守,而非流于表面。窗户一旦开始破裂,运转良好的系统会迅速恶化。
- “领头羊效应”—— 在必要的时候,主动承担责任,做推动变革的催化剂。
- “温水煮青蛙”—— 留意大局,持续不断的审视你身边发生的事情,不要只专注你个人在做的事情。即使你只负责部分的事务,也需要对宏观上能影响你的事件保持敏锐。
做够好即可的软件
为了追求更好,我们毁损了原已够好的
—— 莎士比亚《李尔王》
现实世界不会让我们生产出真正完美的产品,尤其是没有 Bug 的软件。时间、技术、急躁合力对抗着我们。对软件开发者来说要明白以下几点
- 将质量视为需求问题——对于你创建的系统,其应用领域和要达到的质量,必须作为系统需求的一部分加以讨论
- 让用户参与权衡—— 无视用户的需求,一味的堆砌功能,一次次打磨代码,这是不专业的表现。你可能会承诺一个无法兑现的时间尺度,然后为了赶上截止日期,再回过头去删减工程。
- 知道何时止步——不要让过度地修饰和精炼侵蚀掉一个完好的程序。继续前行,让代码在它该有的位置驻留一段时间。它或许不完美,不要紧的 —— 它就算不完美也没关系 (这部分代码既不影响性能,也不影响功能,你可能永远都不需要再改动它了)
知识组合
投资知识,收益最佳
—— 本杰名·富兰克林
知识和经验的确是你最重要的专业资产,可惜的是,它们是一种时效性资产。随着新技术的出现,以及语言和环境的发展,你的知识会变得过时 。不断变化的市场力量可能会使得经验变得陈旧而无关紧要。鉴于技术社会变化的速度越来越快,这种事情可能会发生得特别迅速。当你的知识价值下降时,你对于公司和客户的价值也在下降。想要阻止这一切的发生,学习新事物的能力是你重要的战略资产。
管理知识组合和管理金融投资组合非常的类似:
- 正规投资有定期投资的习惯
及时更新自己的专业视野,了解新的工具和技能,阅读相关的新闻和技术贴。 - 多样化是长线成功的关键
扩展自己的技能领域,熟悉的技能越多,越能适应变化 - 聪明的投资者会平衡保守型和高风险形回报投资的组合
及时了解新的行业动向,并尝试接触与学习,不要去当听众,主动参与。 - 投资者用低买高卖来获得最大的回报
例如前些年的大数据、前端开发,又如现在的低代码以及车联网。它们在这时会有相较于平常较高的溢价,如果你有足够的能力和储备,那么你可以在工作回报上又较高的提升,但需要明确的是,在蓝海回归红海后,也可能有较大的风险。需要你有应对风险的能力 - 定期审查和重新平衡投资组合
根据当前的行业趋势,去重新审视自己的技术树,调整自己学习的优先级
是否在某个项目中使用这些技术或者是否把它放入你的简历中,这并不重要。学习的过程将扩展你的思维,使你向着新的可能性和新的做事方式扩展。思想的“异化授粉”(cross-pollination) 十分重要。设法把你所学的东西应用到你当前的项目中,即使你的项目没有使用该技术,你也可以借鉴一些想法
学习的机会
- 提前把一些资料准备在触手可及的地方,在有兴趣的时候能够及时拿到
- 把找到答案当做一个个人挑战,去尝试用各种方法更新自己的知识组合
- 不要把问题搁置,自己找不到答案,就去找出能找到答案的人。与他人交谈既可以帮你建立人际网络,在这个过程中也可以受益良多
批判性思维
Critically Analyze What You Read and Hear
批判性思考读到的和听到的东西。你需要确保组合中的知识是精准的,未受到创作者的影响。
批判性思维是一门完整的学科,这里起个头:
- 问 “五个为什么” —— 在得到答案后继续为什么,来更接近本源。在日常沟通中要注意技巧,防止认为是挑衅而被揍
- 谁从中受益——虽然听起来比较世俗,不过这样更容易理清脉络
- 有什么背景——每件事都发生在它自己的背景下,这也是为何“能解决所有问题”的方案通常不存在,而那些兜售“最佳实践”的书或文章经不起推敲(好吐槽)
交流
我认为被人从头打量到脚总比被人视而不见的好
—— 《九十岁的美女》
只是拥有是不够的,还要看你如何包装它。即使拥有最好的想法、漂亮的代码、最务实的思想,如果不能和它人交流,最终都无法孕育出果实。
把你的母语当做一门编程语言,像写代码一样用自然语言写文章,尊重DRY原则,ETC、自动化 等等(后续会提到)。
- 了解听众 —— 根据听众不同,选择用不同的方式和风格描述你要介绍的系统
- 明白自己想说什么 —— 写好大纲,提炼核心
- 选择时机 —— 确定轻重缓急
- 让听众参与——让听众参与,把会议变成一场谈话,你可以更有效地表达你自己的观点,还可以听取他们的反馈,汲取他们的智慧。
- 发送文字前检查拼写和接收人
务实的方法
有一些技巧和方法适用于软件开发的所有层级,其中蕴含的思想几乎成了公理,实施过程中也非常通用。然而,这些方法很少被规范成文档。现在,这些想法和过程集中这里。形成了 DRY ETC 等原则,以及原型和便签等方法论
ETC (Easier To Change)
能适应使用者的就是好的设计。对代码而言,就是要顺应变化(让代码更容易阅读、解耦、单一职责、可替换)ETC(更容易变更) 是一种价值观念,而不是一条规则;当你在软件领域思考时,ETC 是个向导,它能帮助你在不同路线中选出一条。
在你有能力辨别时,常识通常都不会错,有事如果你找不到线索,你可以做以下的事情
- 假设不确定什么形式的改变会发生,那么就回到问题的可能产生的源头——让你的写的东西可替换。这样无论将来发生了什么,这块代码都不会称为障碍。
- 把它当做培养直觉的一种方式。在工程日记中留下你面临的处境:你有哪些选择,以及改变的一些猜测。以便以后必须修改这块代码时,方便回顾。在在遇到类似的分叉口时,这会有所帮助。
DRY (Dot Replay You)
知识并不稳定,知识会改变——通常频率还很高。可能只要和客户开个会,对需求的理解马上就变了。政府改了条规定,一些逻辑就过时了。当我们进行维护时,必须找到变更事物的表达——那些嵌入程序的知识胶囊。问题是,在规范、流程、开发的程序中复制知识太容易了,一旦我们动手这么做,就会招致维护的噩梦。想要可靠地开发软件,或让开发项目变得跟容易理解和维护,唯一的方法是遵循 DRY 原则—— 在一个系统中,每一处知识都必须单一、明确、权威的表达
DRY 指的不要重复自己,所以它并不是但指的“不要复制粘贴底代码”,这的确是 DRY 的组成部分,但这是很小的部分,一点都不重要。
DRY 针对的是你对知识和意图的复制,它强调的是,如果两个地方表达的东西其实相同的,只是表达方式不同,那么它也违反了 DRY 原则。同理,如果两个地方的表达方式完全相同,但他们在不同模块中承担不同的职责,那么他们并不违反 DRY 原则。
Code Lab
- 统一访问原则——《面向对象软件构造》中描述:一个模块提供的服务都应该通过统一的约定来提供,该约定不应该表露出其实现是基于储存还是基于运算。
- 你努力的方向,应该是孕育出一个更容易找到和复用已有事物的环境,而不是自己重新编写。若果复用不容易,人们就不会这么做。如果你未能复用,就有重复知识的风险
正交性
“正交性”是从几何学中借用来的术语。若两条直线相交后构成直角,它们就是正交。
线段 AB 与 CD 彼此正交
在计算科学中,这个术语象征着独立性和解耦性。良好的架构系统中,两个模块之间应该相互独立,其中一个模块的修改不应该影响另一个模块。正交的系统可以提高生产力和降低风险,更好的应对不断变化的业务和需求。
如何保持系统的正交性:
- 保持代码解耦、避免全局数据、避免相似的函数
- 养成不断质疑代码的习惯。有机会就重新组织、改善其结构性和正交性
- 基于正交性设计和实现的系统更容易测试。因此编写单元测试本身就是一个有趣的正交性测试。做什么才能让单元测试构建并运行起来?如果需要导入系统其余的大部分代码,那么恭喜你就发现一个与系统其余部分没有很好解耦的模块
- 记录并评估和问题的修复方案和范围,对已经修复的问题归档和记录,在阅读报告中去分析每个 Bug 修复的所影响的模块和文件数量趋势。以此来发觉系统中不稳定的模块,或是设计的不正交的模块。
可逆性
- 保持灵活的架构
- 放弃追逐时尚
曳光弹
曳光弹能够快速抵达目标,枪手可以得到及时反馈——如果曳光弹集中了目标,那么之后的常规子弹也会被击中。
同样地原则也适用于做项目。特别是在要构建一些从做过的东西时,我们可以进行曳光弹式的开发
优势
- 能够构造一个在其中工作的框架(Core)
- 能对进度有更好的感觉,通过对这些案例的更重,度量性能和向用户展示进度要容易的多
- 更小的试错成本,你可以将核心部分快速同用户沟通展示,确认是否是它们想要的。以更快的速度,更小的成本收集到程序的反馈,并生成一个更准确的版本。
方法论
- 在需求评审和开发阶段,寻找需要的核心部分,那些定义了系统的部分,有重大风险的地方。然后对这部分排列优先级,优先从这里处理。
- 注意曳光弹的场景是快速构建一个完整系统的雏形,并非是制作原型
与原型的区别
原型制作生成的是一次性代码,曳光弹虽然简单,但它是完整的,是最终框架的组成部分。原型是用来验证方案的可行性,而曳光弹是用来验证系统的可行性。
原型制作
原型制作是为了学习经验。它的价值不在于产生的代码,而在于吸取的教训。这正式原型的意义所在
原型制作比完整的产品制作要便宜得多。因此我们可以通过制作原型来分析和暴露风险,以一种大幅减低成本的方式获得修正的机会。原型被设计出来,只是为了回答问题的答案,因此可以忽略很多不重要的细节。但如果你发现自己处在于一个不能放弃细节的环境中。那么可能曳光弹式开发更适合你。
当你制作原型时,哪些细节可以忽略:
- 正确性——你可以在适当的时候替代掉数据
- 完整性——原型只需要满足优先的功能
- 健壮性——错误和边界条件并不是必须的,你只需要验证特定的航线
- 格 式—— 并不需要太多的注释和文档
制作原型时,尽量推迟思考细节,你要确定的是,系统的各个部分是怎么结合形成一个整体的。
领域语言
语言之界限,即是一个人世界之界限
——路德维希·维特根斯坦
计算机语言会影响你怎样思考问题,影响你怎样看待信息的传播。每一门语言都有一个特性列表——静态类型、动态类型、mixin、函数式还是面向对象——所有这些对问题的解决方案,既可能是提供建议也可能扰乱视听。在一些案例中,高阶的程序员能跨越到下一个层级,不是用词汇表来编写代码,而直接用该领域的语言编程,直接使用该领域的词汇、语法和语义(DSL)
对于传统的语言,例如 Java 这种,我们可能使用外部语言例如 XML 或者 JSON 需要编写额外的解析器,但是向现代语言例如 Kotlin 这种,它们对于 DSL 的支持是极为优秀的。我们可以通过现有的词汇表来轻松扩展出 DSL 语言来。
从另一个角度思考,ChatGPT 以及 Copilot 的兴起和趋势。将编写业务代码的大部分工作交由 AI 来完成会提高我们的工作效率,理想情况下我们只需要提供边界完善和剪枝的操作。但是如何将业务需求转换为语言模型能够明确理解的文字,这也同样考验证的我们通过母语的编程能力,可以较为清晰的通过母语将需求与环境描述清楚,语言模型才能根据我们需要生成所需要的代码。我们在以后的需求场景可能要锻炼我们通过母语来编写伪代码的能力,以此来应对日新月异的 AI 发展,和提高自己的工作效率
估算
在接收到一个问题或是需求时,我们通过对估算,可以快速判断该事件的可行性。而估算的前提是——对问题建模。当我们理解问题或是需求时,就开始为之建立一个粗略的思维框架模型。在建模的过程中,你可以发现一些表面上看不出来的潜在模式和过程。在得到模型后,就可以将其分解成组件,你需要发掘出这些组件如何交互的数学规则。
如何估算项目进度
- RERT( Program Evaluation Review Techningue )每个 PERT任务都有一个乐观的、一个可能的和一个悲观的估算,像这种带着范围值的估算,他能避免最常见的估算错误因素。
- 增量开发:将任务分为不同阶段,在每次迭代后,提炼其中的经验,完善对进度的控制。
问题建模也是之后让 AI 编程的核心方法论,只有通过建模的形式,我们才可以将需求或问题清晰表达,以及将各个节点拆分成组件,再讲这些组件的模型在未实现实际代码时就明确的划分其职责和功能
基础工具
纯文本
作为程序员,我们基础材料不是木头或铁块,而是知识。我们把需求以知识的形式收集起来,然后在设计、实现出、测试和文档中表达这些知识。纯文本,则是我们认为是将知识持久地存储下来的最佳格式。
所谓的纯文本并不是但只是指 txt 格式的文件,而是可以任何可以被人类直接阅读,编辑器可以直接解析的文本数据。纯文本的优势是
- 防备老化的保险 —— 如今的很多知识软件的使用和更新都依赖于网络和维护,,当存储数据的应用程序生命周期结束后,如果它不支持导出纯文本数据,那么你在应用程序中存储的知识想要重新使用起来就极为困难了。
- 易于检索——使用纯文本记录,我们可以无缝的使用版本管理来管理修改记录,通过 Shell 功能来任意搜索和匹配你的知识,也可以扔到一些文本编辑器中快速查找。而不需要依赖于特定的软件,和它们垃圾的检索功能(例如微信)
- 熟练使用编辑器以及 Shell —— 图形工具的好处自安于 WYSIWYG (所见即可得);弱势之处是 WYSIAYG(所见即全部),如果图形工具的设计者没有为你的额外的需求设计钩子,那你就是做不到。而借助编辑器和 Shell 强大的生态和泛用性,你可以组合它们获得十分强大的能力,且这份知识还是通用的,它并不局限于某个 IDE 中。
- 充当配置项 —— 配置项可以让你迫使你解除你的设计的耦合,从而带来更灵活可适应性更好的程序
工程日记
日记的好处
- 它比记忆更加可靠
- 它为提供了一个地方,用来保存于当前任务无关的想法。这样你就可以继续专注正在做的事情,并指导这个伟大的想法不会被遗忘
- 它就像一个橡皮鸭。当你停下来,把东西写上去的时候,大脑可能会换挡,几乎就像在和某个人说话一样——这是一个反思的好机会。你可以在开始做笔记的时候,突然意识到刚刚做的事情,也就是笔记的主题是完全错误的。
还有一个额外的好处,你能时不时的想起很多年前你在做什么,会想起那些人、那些事,以及那些糟糕的衣服和发型。
务实的偏执
自责中往往有种奢侈。我们自责时,总觉得别人无权再则被我们
—— 奥斯卡·王尔德《道林·格雷的画像》
BDC 契约式编程
TDD 测试驱动开发
作者讲述了 BDC 和 TDD 的开发模式,主要是针对防御式编程和断言式编程的理念的推崇 —— 及在代码设计阶段把边界条件思考充分,并进行 Check . 在出现问题时,及早将异常抛出,避免进一步危害整个系统。这是一个合格程序员的基本功,也没有什么特别的方法论。其中值得称道就是断言编程的开关性,我们可以像一个 Google 源码一样,只在 debug 模式中开启断言,这样可以日常开发和测试流程时就把一些问题明显的暴露出来。而在生产模式中,秉承着用户优先和性能损耗的理念。我们可以关掉断言,补充日志。
很多时候,明天看起来会和今天差不多,但不要指望一定会这样
- 在做设计与维护的时候,对超过可见范围的模块的维护和设计,并不一定需要浪费精力为不确定的未来做设计(徒增复杂性,像另一本书降到,Android 0.x 版本时,硬件是不支持 GPU 的,而负责开发的 Google 工程师在制作图形渲染这一块时,并没有设计整个系统,而是设计了一块虚拟的 GPU,当 Android 后续的版本支持 GPU 后,只需要将虚拟 GPU 的模块替换掉,系统的设计仍然保留),还不如将代码设计成可替换的。使代码可替换,还有助于提高内内聚性、解耦和 DRY,从而实现更好的设计。
- 软件开发中,总是采取经过深思熟虑的小步骤,同时检查反馈,并在推进前不断调整。把反馈的速率当做速度限制,永远不要进行 “太大” 的任务步骤
并发
并发性是指两个或更多个代码在执行过程中表现得像是在同时运行一样。并行性则值得是它们的确是在同一时刻运行。
像获得并发性,需要在一个特殊的环境下运行下运行。当代码运行时,这个环境可以在其不同部分之间切换执行过程。这样的环境通常基于线程、进程、携程来实现
像获得并行性,则需要又可以同时做两件事情的硬件,通常是同一 CPU 上的多核心,同一机器上的多个 CPU 或连接在一起的多台计算机。
编码
- 时刻注意你在做什么,事情往往是慢慢失去控制的
- 你能向一个更初级的程序员详细解释一下代码么?如果做不到,也许可能正在依赖某个巧合
- 要按计划推行,无论这个计划是以什么形式存在
- 不要只测试代码,还要测试猜测。去实际试一下一些假设以及边界条件,确定你的代码在你所能预知的场景都能正常工作
- 为你的精力投放安排一个优先级。要把时间花在重要的方面。
重构
软件开发最常见的隐喻是建筑的构建。商务人士对建筑的隐喻感到很舒服:它是科学的,可重复的,管理上有严格的汇报层次结构,等等。但实际上园艺的隐喻更接近于现实的软件开发:它更像一个有机体而非钻石堆砌,你需要不断观测花园的健康状态,根据需要对(土壤、职务、布局)做出调整。
如何重构
- 不要试图让重构和添加功能同时进行
- 在开始重构之前,确保有良好的拥有良好的测试环境。尽可能多地运行测试。这样,如果变更破坏了任何东西,都将尽快得知。
- 采取简短慎重的步骤:将字段从一个类移动到另一个类,拆分方法,重命名变量。重构通常涉及对许多全局变量的修改,这些局部变量会导致更大范围的修改,若果保持小步骤,并在每个步骤之后进行测试,就能避免冗长的调试。
测试
测试的好处主要发生在你考虑测试以及编写测试的时候,而不是在运行测试的时候。
你编写的所有软件都将进行测试 —— 如果不是你和你的团队测试,那就要由最终用户测试 —— 所以你最好计划对其进行彻底的测试。 一点预先的准备可以大大降低维护费用,减少客诉。
项目
完美,不是在没有什么需要增加,而是在没有什么需要去掉时达到的
需求
挖掘需求
找出用户为何要做特定事情的原因,而不只是他们目前做这件事的方式,这很重要。到最后,你的开发必须解决他们的商业问题,而不是满足他们陈述的需求。用户文档记载需求背后的原因将在每天进行实现决策时给你的团队带来无价的信息。
明确需求
用户陈述的的要求并不一定是真正的需求。例如 “只有员工的上级和人事部门此案可以查看员工的档案” 这个陈述今天看来也许是个需求,但是由于它在陈述中嵌入了商业政策,而政策会经常改变,改变可能只是一些相对微妙的区别,但是对开发者来说就却有深远的影响。如果需求被陈述为 “只有人事部门才能查看员工档案” —— 开发者最后就可能编写在每次客户端访问时,进行明确的检查。但是如果陈述是 “只有授权的用户才可以访问员工档案”,开发者就可能设计一套完整的访问控制系统,当政策改变时,只有该系统的元数据需要修改。
所以我们可能不并不想把他硬性的写入我们的需求中。最好的是把政策和需求文档分开,需求作为研发目标输出给开发者,而政策则是他们需要在实现中支持的事物类型的例子。最后,政策可以成为应用中的元数据。
需求文档
制作需求文档时的一大危险是太过具体。好的需求文档会保持抽象。在涉及需求的地方,最简单的准确地反映商业需求要的陈述是最好的。这并非意味着你可以含糊不清 —— 你必须把底层的语义不变项当作需求进行捕捉,并把具体的或当前的工作实践当作政策进入文档。
—— 需求不是架构。需求不是设计,也不是用户界面。需求是需要。
问题
- 对谜题的所有解决方案进行陈列,分解。并解释为何不能采用这个方案,确定么,是否能证明?
- 自我提问,确定问题的核心以及当前的疑惑的是否是真正的问题还是外围的技术问题转移了注意力?
傲慢与偏见
注重实效的程序员不会逃避责任。相反,我们乐于接受挑战,乐于使我们的专业知识广为认知。如果我们在负责一项设计,或是一段代码,我们是在做可以引以为豪的工作。