《程序员修炼之道(第2版)》
# 《程序员修炼之道:通向务实的最高境界(第2版)》
作者 (美)David Thomas(大卫·托马斯),Andrew Hunt(安德鲁·亨特)
# 序
人生是你的
编程社区与我曾经身处的其他社区非常不同。其独特之处在于,人们无不醉心于学习和实践,这既令人生畏,又让人耳目一新。
改变习惯、行为和期望
# 新版前言
为了节省与新客户打交道的时间,我们开始做笔记。这些笔记最终变成了《程序员修炼之道》这本书
为了让你理解我们的思维方式
# 第1章 务实的哲学
第1章 务实的哲学 这是一本和你有关的书。 毫无疑问,你的事业是你自己的,更重要的是,你的人生是你的——都是你自己所拥有的。之所以有必要读这本书,是因为我们相信自己可以成为一个更好的开发者,并能帮助其他人变得更好——也就是说,可以成为一个务实的程序员。 务实的程序员的特质是什么?是他们面临问题时,在解决方案中透出的态度、风格及理念。他们总是越过问题的表面,试着将问题放在更宽泛的上下文中综合考虑,从大局着想。毕竟,若不去了解来龙去脉,结合实际如何谈起?又怎能做出明智的妥协和合理的决策? 他们的另一个成功点是他们为所做的一切负责。关于这一点,我们会在我的源码被猫吃了中讨论。责任感驱使务实派程序员不会在他们的项目分崩离析时坐视不管。在软件的熵中,我们将告诉你如何让项目保持清爽。 大多数人很难接受改变,这或许有充分的理由,也有可能仅仅是旧有的惰性使然。在石头做的汤和煮熟的青蛙中,我们会看到一个推动变革的策略(考虑利益平衡),了解一个两栖动物忽视了渐进变化中风险的寓言故事。 了解所做工作的来龙去脉有一个好处,那就是更容易把握软件必须做到多好。接近完美往往才是唯一的选项,这通常需要做许多折衷方案。我们将在够好即可的软件中展开。 当然,你需要大量的基础知识和经验,才可能实现以上这些。学习是一个持续不断的过程。在知识组合中,我们会讨论一些保持学习劲头的方法。 最后,没有与世隔绝的工作。我们总是在花大量的时间和其他人打交道。交流!列出了让我们做得更好的一系列方法。 务实的编程源于务实思考的哲学。这一章描述了这种哲学的基调。
你的事业是你自己的,更重要的是,你的人生是你的——都是你自己所拥有的。之所以有必要读这本书,是因为我们相信自己可以成为一个更好的开发者,并能帮助其他人变得更好——也就是说,可以成为一个务实的程序员。
务实的程序员的特质是什么?是他们面临问题时,在解决方案中透出的态度、风格及理念。他们总是越过问题的表面,试着将问题放在更宽泛的上下文中综合考虑,从大局着想。
他们的另一个成功点是他们为所做的一切负责。
# 1 人生是你的
人生是你自己的,是你在拥有、经营和创造。
总有一些原因导致开发者拒绝改变。他们缩在那里,期盼着事情会自己变好。他们眼睁睁地看着自己的技能过时,却抱怨公司没有给予培训。他们乘着公交车,路过广告林立的街道,顶着凄雨寒风,钻入写字楼工作。
这个行业给了你一系列非凡的机遇。积极主动点,掌控这些机遇。
# 2 我的源码被猫吃了
在你的职业发展、学习教育,以及你的项目、每天的工作等各方面对你自己负责,对你的行为负责,这是务实哲学的基石之一
一个务实的程序员能完全掌握自己的职业生涯,从不害怕承认无知和错误。
除了个人尽力做好,你必须分析超出你控制范围的风险情况。
跟老板说“我的源码被猫吃了”解决不了问题
除了个人尽力做好,你必须分析超出你控制范围的风险情况
你必须根据自己的价值观和判断做出决定。 当你决定对一个结果承担责任时,要明白这意味着你将承接相关的义务。当你犯了错误(就像我们所有人一样),或是做出了错误的判断时,诚实地承认它,并尝试给出选择。 不要把问题归咎于别人或其他什么事情上,也不要寻找借口。不要把所有问题都归咎于供应商、编程语言、管理或是同事。这些因素都可能是问题的一部分。它们的确会对解决方案造成影响,但不是给你的借口。 如果你面临供应商帮不上忙这样的风险,就应该制订一个应急方案。如果磁盘挂起——你所有的源码都在里面——而你没有备份,这就是你的错。跟你的老板说“我的源码被猫吃了”解决不了问题。 提示4 提供选择,别找借口 如果你打算跟别人解释为什么做不完、为什么延期、为什么搞砸,在此之前先等等,听一下自己的内心。讲给你显示器上的橡皮鸭听听,或是先对着猫说一遍。你的那些借口听起来合理吗?还是很愚蠢?你的老板听到会怎样? 把谈话在心里过一遍。其他人可能说什么?他们会问,“你试过这样做吗……”“为什么你不考虑一下那样做?”而你怎么回答?在你跑去告诉他们坏消息前,还有什么你可以再试试的?有时,你已经知道他们会说什么,那么就直接帮他们搞定。 给出选择,而不是找借口。不要说搞不定;解释一下要做些什么才能挽回这个局面。是否必须扔掉这些代码呢?给他们讲讲重构的价值(参见第216页的话题40:重构)。你是否需要一点时间来做原型?因为只有这样才能决定后面怎么做最好(参见第57页的话题13:原型与便签)。为了防止错误再次发生,你是否需要引入更好的测试(参见第220页的话题41:为编码测试和第288页的无情的持续测试)或增加自动化流程? 也许你需要额外的资源才能完成这项任务。或许你需要花更多的时间和用户打交道?也可能仅仅是你自己需要时间:你需要学习一些技能吗?
如果你打算跟别人解释为什么做不完、为什么延期、为什么搞砸,在此之前先等等,听一下自己的内心。讲给你显示器上的橡皮鸭听听,或是先对着猫说一遍。你的那些借口听起来合理吗?还是很愚蠢?你的老板听到会怎样?把谈话在心里过一遍。其他人可能说什么?他们会问,“你试过这样做吗……”“为什么你不考虑一下那样做?”而你怎么回答?在你跑去告诉他们坏消息前,还有什么你可以再试试的?有时,你已经知道他们会说什么,那么就直接帮他们搞定。给出选择,而不是找借口。不要说搞不定;解释一下要做些什么才能挽回这个局面。是否必须扔掉这些代码呢?给他们讲讲重构的价值(参见第216页的话题40:重构)。你是否需要一点时间来做原型?因为只有这样才能决定后面怎么做最好(参见第57页的话题13:原型与便签)。为了防止错误再次发生,你是否需要引入更好的测试(参见第220页的话题41:为编码测试和第288页的无情的持续测试)或增加自动化流程?
当你意识到自己在说“我不知道”时,一定要接着说“——但是我会去搞清楚”
# 3 软件的熵
漠视会加速腐烂的过程。
要体会体会
不要只是因为一些东西非常危急,就去造成附带损害。破窗一扇都嫌太多
不要只是因为一些东西非常危急,就去造成附带损害。破窗一扇都嫌太多
一定要告诉自己,“不要打破窗户。”
# 4 石头做的汤和煮熟的青蛙
永远留意着大局,持续不断地审视你身边发生的事情,而不要只专注于你个人在做的事情
# 5 够好即可的软件
对用户、未来的维护者来说够好即可,只要好的程度能让你自己内心平静就可以
# 6 知识组合
当你的知识价值下降时,你对于公司或客户的价值也在下降。我们想阻止这一切的发生。 学习新事物的能力是你最重要的战略资产。但是如何获取学习方法,又如何知道该学什么呢? 知识组合 我们可以将程序员所了解的一切有关计算过程的事实、工作的应用领域,以及所有经验,视为他们拥有的知识组合。管理知识组合和管理金融投资组合非常的类似: 1.正规投资者有定期投资的习惯。 2.多样化是长线成功的关键。 3.聪明的投资者会平衡保守型和高风险高回报型投资的组合。 4.投资者用低买高卖来获得最大的回报。
学习新事物的能力是你最重要的战略资产
如果你无法自己找到答案,去寻觅有能力找到答案的人,而不要让问题沉寂下去。和其他人交谈有助于构建你的人际网络,而且你还会惊奇地发现,在这个过程中你会找到一些其他不相关问题的解决方案——旧有的知识组合会不断地扩大…… 所有阅读和研究都需要时间,而时间总是不够用的。所以你需要提前准备好,确保在无聊的时候有东西可读。在医院排队往往是把书读完的好机会——不过一定要记得带上自己的电子阅读器。不然可能只好去翻医院里的旧年刊,里面折起的那页讲的是1973年的巴布亚新几内亚。 批判性思维 最后一个要点是要批判性地思考读到的和听到的东西。你需要确保组合中的知识是精准的,未受供应商或媒体炒作的影响。当心坚持教条的狂热者,他们将其视为唯一答案——而那些教条未必适合你和项目。 永远不要低估商业主义的力量。网络搜索引擎有时仅仅是把热门的东西列在最前面而已,并不能说明这是你的最佳选择,而且内容提供商也可以花钱把它们的东西排到前列。书店有时仅仅是把一本书摆在显著的位置而已,并不能说明这是一本好书,甚至不能说明这本书很流行,可能只是有人花钱把它摆在了那里。 提示10 批判性地分析你读到和听到的东西 批判性思维本身就是一门完整的学科,我们鼓励你仔细研究和学习这门学科。现在先在这里起个头,问几个值得思考的问题。 问“五个为什么” 我最喜欢的咨询技巧是:至少问五次“为什么”。就是说,每当有了答案后,还要追问“为什么”。像个烦人的四岁小孩那样经常性重复提问,不过记得要比小朋友更有礼貌。这样做可以让你更接近本源。 谁从中受益
# 7 交流!
深以为然,心有戚戚焉 与所有的沟通形式一样,这里的窍门是收集反馈。不要只是等待问题的出现:把它问出来
与所有的沟通形式一样,这里的窍门是收集反馈。不要只是等待问题的出现:把它问出来
理解听众想听什么的一个角度,就是搞清楚他们的优先事项是什么
有时只需要问一个简单的问题“现在是讨论……的好时机吗
们在这个领域的技能水平和经验如何?是专家还是新手?他们需要手把手教,还是只想要一个“太长就不看”版的简介?如果有疑虑,开口问。
如果你正在使用字处理软件,请使用它的样式表以保持一致性。(你的公司应该已经准备好了样式表,你可以直接使用。)学习如何设置页眉和页脚。
随时知会别人,能让人更容易原谅你偶然的疏忽,让人觉得你并没有忘记他们。
用源码中的注释生成好看的文档非常容易,建议给模块和导出函数都加上注释,这能在其他开发者使用的时候,给他们很大的助力。 不过,有人说必须给每个函数、数据结构、类型声明等都分别加上注释,我们并不赞同这种做法。这种机械的注释方式实际上会导致代码更难维护:一旦你想改点什么,就需要改变两个东西。因此,将非API的注释限制在只用来讨论其为何存在及其意图、目标。当代码已经展示了事情怎样完成时,注释是多余的——因为这违反了DRY原则。 注释源码是一个绝佳的机会,可以用来记录那些在其他地方无法记录的项目细节:工程上的权衡,为什么要做决定,放弃了哪些替代方案,等等。 总结 · 明白自己想说什么。 · 了解听众。 · 选择时机。 · 挑选风格。 · 让它看起来不错。 · 让听众参与。 · 做倾听者。 · 回应别人。 · 把代码和文档绑在一起。 相关部分包括: · 话题15:估算,位于第67页 · 话题18:加强编辑能力,位于第82页
将非API的注释限制在只用来讨论其为何存在及其意图、目标。当代码已经展示了事情怎样完成时,注释是多余的——因为这违反了DRY原则
注释源码是一个绝佳的机会,可以用来记录那些在其他地方无法记录的项目细节:工程上的权衡,为什么要做决定,放弃了哪些替代方案,等等。
# 8 优秀设计的精髓
当你在软件领域思考时,ETC 是个向导,它能帮助你在不同的路线中选出一条。
ETC里有一个隐含的前提。多条路线中的哪一条更易于将来的变更,ETC假定我们有能力辨别。
# 9 DRY——邪恶的重复
在一个系统中,每一处知识都必须单一、明确、权威地表达
这不是你能不能记住的问题,而是什么时候会忘记的问题。
DRY 针对的是你对知识和意图的复制。它强调的是,在两个地方表达的东西其实是相同的,只是表达方式有可能完全不同。接下来是一个严峻的考验:当代码的某个单一方面必须改变时,你是否发现自己在多个地方以多种不同的格式进行了变更?有没有同时修改代码和文档,或是同时变更数据库Schema和代码中相关的数据结构,亦或……?如果这类情况发生,你的代码并不满足DRY。
并非所有的代码重复都是知识的重复
代码的确相同,但代码所表达的知识是不同的。这两个函数校验了两个不相干的东西,只是恰巧使用了相同的规则。这是一个巧合,而非重复。
# 10 正交性
重点在解耦,基于模块化、组件、分层,基于外部配置,将第三方api藏在抽象之后 正交性
“正交性”是从几何学中借用来的术语。
在计算科学中,这个术语象征着独立性或解耦性。对于两个或多个事物,其中一个的改变不影响其他任何一个,则这些事物是正交的。
模块化、基于组件和分层
当你规划好组件后,问问自己:如一个特别功能背后的需求发生显著改变,有多少模块会受影响?对于一个正交系统,答案应该是“一个”
Enterprise Java Bean(EJB)系统是一个关于正交性的有趣例子。在大多数面向事务的系统中,应用程序代码必须描述每件事务的开始和结束。在EJB下,此信息以注解声明的方式表达出来,放在实际工作的方法之外。同样的应用程序代码可以运行在不同的EJB事务环境中,而不需要做任何变更。 EJB的这个方式是修饰模式的一个范例:给一个事物添加功能却不需要修改事物本身。这种编程风格可以用于各种编程语言,无须要求特别的框架或是库。唯一需要的是在编程时遵守一点纪律。 编码 每当你写下代码时,就有降低软件的正交性的风险。你不仅需要盯着正在做的事情,还要监控软件的大环境。如果不这样,其他模块中的功能就很可能无意间重复了,或是把已有的知识表达了两次。 有几种技术可以用来保持正交性: 保持代码解耦 编写害羞的代码——模块不会向其他模块透露任何不必要的信息,也不依赖于其他模块的实现。试试最少知识原则,会在第131页的话题28:解耦中加以讨论。如果你需要改变一个对象的状态,让该对象替你来完成。这样做能让你的代码和其他代码实现隔离,更有可能保持正交性。 避免全局数据 只要代码引用全局数据,就会将自己绑定到共享该数据的其他组件上。即使只打算对全局数据进行读操作,也可能引发问题(例如突然需要将代码改为多线程的情形)。一般来说,如果总是显式地将任何需要的上下文传递给模块,那么代码会更容易理解和维护。在面向对象的应用程序中,上下文通常作为参数传给对象的构造函数。在其他代码中,也可以创建一个包含上下文的数据结构,并将结构的引用传出去。 《设计模式:可复用面向对象软件的基础》 [GHJV95] 一书介绍了单例模式。该模式提供了一种方法,可确保一个特定的类只有唯一实例。
养成不断质疑代码的习惯。只要有机会就重新组织、改善其结构和正交性。这个过程被称为重构,它非常重要
采用正交性原则,并与DRY原则紧密结合,的确可以让系统变得更灵活、更容易理解,并且更容易调试、测试和维护
# 11 可逆性
关键的决定不易逆转
一旦决定使用某个供应商的数据库,或是某个架构模式,抑或是特定的部署模型,就是在采取一系列无法回退的行动,除非付出巨大的代价。
这本书中的许多主题都面向有弹性、适应性强的软件生产过程。只要坚持其中推荐的做法,尤其是第131页的解耦,以及使用第170页的外部配置这些原则,我们就不必做太多不可逆转的关键决定。
具体实践中,数据库有各自的方言跟关键字
如果你真的将数据库的概念抽象出去——让它只是以服务形式提供持久化——现在你就可以灵活地中途换马了
认为任何决定都是板上钉钉的——而没有为可能出现的意外做好准备。与其认为决定是被刻在石头上的,还不如把它们想象成写在海滩的沙子上。一个大浪随时都可能袭来,卷走一切
将第三方API隐藏在自己的抽象层之后。将代码分解为多个组件:即使最终会把它们部署到单个大型服务器上,这种方法也比一开始做成庞然大物,然后再切分要容易得多。
# 12 曳光弹
典型的反应是要把系统定死;把各种需求逐条列出来,制成大量的文件;约束好每一项未知的东西,并且限定环境;开火时采用航迹推算法。总之,前面先做大量的计算,然后开枪,希望能命中。
它只是功能还不完整。但是,只要在各个组件间,从一头到另一头全部打通,就可以检查出离目标有多么接近,并在必要时做出调整。一旦抵达目标,再添加功能就很容易了。
曳光弹式开发和项目不会结束这种理念是一致的:总有东西需要改,总有新功能需要加。这是一个逐步递增的方法
传统的做法是使用一种笨重的工程方法:代码被分成模块,在为这些模块编码时不用考虑外部环境。模块被组合成组件,然后集成这些组件,直到有一天它变成一个完整的应用程序。只有这些完成后,才能将应用程序作为一个整体交付给用户进行测试。
曳光弹会告诉你击中了什么。可能它并不能每次都击中目标,你需要不断地调整瞄准,直到击中目标。这正是问题的关键。
你可能认为曳光代码这个概念与原型制作无异,只不过换了个咄咄逼人的名字。但它们是有区别的。当使用原型时,你的目标是探索最终系统的特定方面。如果有了一个真正的原型,最终你将扔掉验证构思时捆绑在一起的所有东西,并总结经验教训,最后正确地重新编码。
曳光代码方法解决的是一个不同的问题。你需要知道,应用程序作为一个整体,是如何整合在一起的,需要向用户展示实践中交互如何工作,而且你想要给开发人员一个框架,用来挂接代码。
可以展示给用户和开发人员。随着时间的推移,你会把新的功能添加到这个框架中,把留白的程序补完。但是框架保持不变,并且你知道系统将继续这样运作,一直和一开始完成曳光代码时一致。
这一区别非常重要,值得我们反复强调。原型生成的是一次性代码;曳光代码虽然简单但是完整,它是最终系统框架的组成部分
# 15 估算
15 估算 位于华盛顿特区的国会图书馆目前有大约75 Tb的在线数字信息。请马上回答!通过1Gbps的网络发送出所有这些信息需要多长时间?一百万个名字和地址需要多大的磁盘空间?压缩100Mb的文本需要多长时间?完成你的项目需要几个月? 在某种程度上,这些都是毫无意义的问题——因为缺少必要信息。然而,只要你愿意估算,这些问题都是可以回答的。并且,在估算的过程中,你将会加深对程序所处世界的理解。 通过学习估算,把这项技能发展为对事物的数量级产生直觉,你将能展现出一种魔法般的能力,这种能力可以判别事情的可行性。当有人说“我们要通过网络将备份上传到亚马逊S3上”时,你直觉上就能判断这是否可行。在你编码的时候,能知道哪个系统需要优化,哪些放在那里就够了。 提示23 通过估算来避免意外 无论何时,当有人要你做预估时,有一个答复是万能的。对于这个小福利,我们将在本部分的末尾揭晓答案。 多精确才够 在某种程度上,所有的答案都是估算,区别仅在于一些比另一些更精确。所以当有人让你估算的时候,你要问自己的第一个问题是,答案会用在什么场合下。对方需要很高的精度,还是只要一个大约的估计? 有一件关于估算的有趣的事——你使用的单位会对结果的解释产生影响。如果你说某件事需要130个工作日完成,那么听的人往往觉得实际要的时间会很接近这个数字。然而,如果你说的是“哦,大约6个月吧”,他们就会认为还需要5到7个月不等。两个数字表示的时间周期是一致的,但是“130天”却可能暗示了比你想象得更高的精度级别。我们建议你采用下面的时间尺度做估算:
所以当有人让你估算的时候,你要问自己的第一个问题是,答案会用在什么场合下。对方需要很高的精度,还是只要一个大约的估计?
所有的估算都是基于对问题的建模。但是在我们深入建模技术之前,必须提到一个基本的估算技巧,用它总能给出不错的答案:问问已经做过的人。
所有的评估工作的首要部分都是建立对所问内容的理解。除了上面已经讨论过的精确度问题,你还需要掌握问题域的范围。范围通常是问题的隐含前提,你只是需要养成习惯,在开始猜测之前就加以考虑。很多时候,你选择的范围会成为给出的答案的一部分:“假设没有交通事故,车里也有汽油,那么我会在 20 分钟内抵达那里。”
这就是人们在现实世界中所做的估算。它不会仅有一个数字(除非你强迫他们给你一个数字),而是由一系列的预案构成。
每个 PERT 任务都有一个乐观的、一个最有可能的和一个悲观的估算。任务被排列进一个相互依赖的网络中,然后使用一些简单的统计方法来确定整个项目可能的最佳和最差时间。
# 第3章 基础工具
工具会放大你的才能
让需求来驱使你不断选购新的工具
你必须在调试方面做到非常熟练,才可能成为一个伟大的程序员
# 18 加强编辑能力
18 加强编辑能力 我们在前面提到过,工具是手的延伸。嗯,把它用在编辑器上,要比用在任何其他软件工具上更贴切——你需要尽可能毫不费力地操作文本,因为文本是编程的原始材料。 在本书的第一版中,我们建议只用一个编辑器来完成所有的事情:编码、文档、备忘录、系统管理,等等。现在我们的立场软化了一些——你爱用多少个编辑器就用多少个,不过最好每一个用起来都能游刃有余。 提示27 游刃有余地使用编辑器 有那么夸张吗?我是不是还没有说过这能节省大量时间?千真万确:以一整年为跨度,即使编辑效率只提高了4%,只要每周花在编辑上的时间有 20小时,你每年就能凭空多出一周时间。 但这还不是真正的好处——真不算是。如果你操作编辑器游刃有余,最主要的收益来自于变得“顺畅”——不必再把心思花在编辑技巧上面。从思考到将想到的东西呈现在编辑器中的整个过程,没有阻塞,一气呵成。思考变流畅,编程就会受益。(如果你教过新手开车,就能理解他们和老司机的确不一样。新手一定会去仔细考虑要做的每个动作,而老司机则是靠本能在开车。) 游刃有余意味着什么 怎么才算游刃有余。这里有一个挑战列表: · 当编辑文本时,以字符、单词、行、段落为单位移动光标及进行选择。 · 当编辑代码时,在各种语法单元(配对的分隔符、函数、模块……)之间移动。 · 做完修改后,重新缩进代码。 · 用单个指令完成代码块的注释或取消注释。
编辑时要自省。每次发现自己又在重复做某件事情的时候,要习惯性地想到“或许有更好的方法”,然后找到这个方法。
# 19 版本控制
这已经是他们使用 VCS的极限。而他们错过的是一个更广阔的天地,一个充满协作、部署流水线、问题追踪、团队交流的完整世界。
# 20 调试
慌人们很容易陷入恐慌,尤其是当最后期限逼近,或是在老板或客户站在背后紧张凝视之下,拼命找出问题原因的时候。然而,这时非常重要的是要退后一步冷静思考。对于那些你觉得是 Bug引起的症状,认真想想,到底什么会导致它们那个样子
。我们通常将编译器警告级别设置得尽可能高。浪费时间去找一个计算机可以帮你找到的问题,是没有意义的
你可能需要观察提交错误报告的用户,通过其操作来获得足够详细的信息。
你可能需要拜访报告 Bug的用户,这样才能收集到比最初提供的更多的数据
人为的测试(例如程序员从下到上画线)对应用程序的测试而言还不够。你必须粗暴地测试所有边界条件,并且复原实际的最终用户使用模式。你需要有系统地做这些事情
要知道,把纸笔放在旁边会很有帮助,这样可以随时做笔记。特别是,当无意中发现一个线索,一番顺藤摸瓜后却发现问题不在这里时,如果之前没有记下从哪里开始的,可能会在找回源头上浪费很多时间。 有时你看到的堆栈跟踪信息无比之深。在这种情况下,通常有一种比逐个检查每个栈帧更快的方法来发现问题:使用二分法。但是在讨论二分法之前,让我们先看看另外两个常见的 Bug场景。 输入值的敏感度 这种情况你肯定遇到过:程序可以很好地处理所有的测试数据,并在第一周的生产中保持良好状态。之后,当输入特定数据集时,它会突然崩溃。 你可以着眼于它崩溃的地方,试着由此回溯。不过有时从数据着手更容易一些。获取数据集的副本,并用数据测试本地运行的应用程序副本,确保其在本地也能崩溃。然后将数据用二分法分解,直到能准确地分离出导致崩溃的输入值。 版本间回退 一个很棒的团队,将软件好好地发布到生产环境中,某一天代码却出了 Bug,而软件在一周前还能正常工作。有什么方法可以很好地帮你找出,是哪一步的变更引起的Bug?你猜怎么着,二分法要登场了。 二分法 每个 CS本科生都被要求写过基于二分法的代码(有时也叫二分查找)。想法很简单,你需要在一个有序的数组里找到特定的值。当然可以一次一个地依次查找,但是这样一来,找到该值平均需要的次数,多达数组元素个数的一半。这样做有时找到的是一个更大一点的值,那有可能意味着你要的值不一定在数组里面。 分而治之就快多了。先在数组中间选择一个值。如果是你要找的就停下来;如果不是,就可以把数组切成两半。若找到的值大于目标值,那么就知道它一定位于数组的前半部分,否则就位于数组的后半部分。在适当的子集中重复这个过程,很快就能得到结果。
有时你看到的堆栈跟踪信息无比之深。在这种情况下,通常有一种比逐个检查每个栈帧更快的方法来发现问题:使用二分法。但是在讨论二分法之前,让我们先看看另外两个常见的 Bug场景。
# 第4章 务实的偏执
每个人都觉得,地球上只有自己一个好司机。全世界都在闯红灯、实线变道、不打灯就转弯、开车发短信,总之都不符合我们的标准。所以我们需要防御性驾驶。在麻烦发生之前就做好准备,预料到意料之外的事情,永远不要把自己置于无法自拔的境地
要防御式编程
——有任何疑问,都要去验证我们得到的一切信息;使用断言来检测错误数据,不信任任何可能来自潜在攻击者或有恶意者的数据;做一致性检查,对数据库的列设置约束
务实的程序员则更进一步,他们连自己也不相信。既然没人能写出完美的代码,那么也包括自己在内。务实的程序员会为自己的错误建立防御机制
只要我们总是坚持走小步,按照不要冲出前灯范围中描述的那样,就不会从悬崖边掉下去
# 24 死掉的程序不会说谎
崩溃的程序不会说谎, 死掉的程序不会说谎
防御式编程是在浪费时间,让它崩溃
一个死掉的程序,通常比一个瘫痪的程序,造成的损害要小得多。
# 26 如何保持资源的平衡
在代码的不同位置,如果都会分配同一组资源,就始终以相同的顺序分配它们。这将减少死锁的可能性。
事务、网络连接、内存、文件、线程、窗口,基本的模式都是适用的:分配资源的人应该负责释放它。
# 27 不要冲出前灯范围
当你不得不做下面这些事情的时候,可能已经陷入了占卜的境地:· 估计未来几个月之后的完成日期· 为将来的维护或可扩展性预设方案· 猜测用户将来的需求· 猜测将来有什么技术可用
与其浪费精力为不确定的未来做设计,还不如将代码设计成可替换的。当你想要丢弃你的代码,或将其换成更合适的时,要让这一切无比容易。使代码可替换,还有助于提高内聚性、解耦和DRY,从而实现更好的总体设计。
# 第5章 宁弯不折
我们将告诉你如何做出可逆的决策,这样代码在面对不确定的世界时可以保持灵活性和适应性。
# 28 解耦
产生耦合的场景:一连串的方法调用,继承,全局化 那么对代码进行“解耦”到底是指什么呢?在本部分中,我们将讨论: · 铁道事故——一连串的方法调用 · 全局化——静态事物的风险 · 继承——为什么子类很危险
那么对代码进行“解耦”到底是指什么呢?在本部分中,我们将讨论:· 铁道事故——一连串的方法调用· 全局化——静态事物的风险· 继承——为什么子类很危险
耦合的“症状
不相关的模块或库之间古怪的依赖关系· 对一个模块进行的“简单”修改,会传播到系统中不相关的模块里,或是破坏了系统中的其他部分· 开发人员害怕修改代码,因为他们不确定会造成什么影响· 会议要求每个人都必须参加,因为没有人能确定谁会受到变化的影响
TDA(tell-don't-ask,只管命令不要询问)
重用不是主要考虑的问题,可替换才是。这个概念是第一次知道。
重用可能不是创建代码时主要考虑的问题,但是探求代码的可重用性应该作为例行编码过程的一部分。当你使代码可重用时,就给了它干净的接口,将其与其他代码解耦。这允许你在提取一个方法或模块时,不需要同时牵扯出其他东西。如果代码使用了全局数据,则很难将其与其他部分分离。
重用可能不是创建代码时主要考虑的问题,但是探求代码的可重用性应该作为例行编码过程的一部分。当你使代码可重用时,就给了它干净的接口,将其与其他代码解耦。这允许你在提取一个方法或模块时,不需要同时牵扯出其他东西。如果代码使用了全局数据,则很难将其与其他部分分离。
解决方案是确保始终将这些资源包装在你所控制的代码之后
再强调一次,一切都是为了变更
# 29 在现实世界中抛球杂耍
29 在现实世界中抛球杂耍 事情不会随随便便发生,它们是注定要发生的。 ——约翰·肯尼迪 在笔者还稚气未脱的旧日时光里,计算机还不是特别灵活。我们通常会根据计算机的局限性来组织与它们的交互方式。 今天,我们要求更多:计算机必须融入我们的世界,而不是我们去融入计算机的世界。我们的世界是混乱的:事情在不断发生,东西在四处移动,我们的想法在变化……我们所写的应用程序必须以某种方式确定要做什么。 本部分主要讨论如何编写这些响应式应用程序。 我们将从事件这个概念开始。 事件 事件表达出信息的可用性。它可能来自外部世界:用户点击了按钮,或是股票报价更新了。它也可能是内部的:计算的结果已经准备好了,搜索完成了。它甚至可以是像获取列表中的下一个元素这样简单的事情。 不管来源是什么,如果我们编写响应事件的应用程序,并根据这些事件调整程序的行为,那么这些应用程序将在现实世界中更好地工作。用户会发现它们更具交互性,应用程序本身也会更好地利用资源。 但我们如何编写这类应用程序呢?如果没有某种策略,我们很快就会陷入困惑,应用程序将是一堆紧密耦合的代码。 下面看看能帮助我们的四个策略。 1.有限状态机
状态机基本上就是怎样处理事件的一份规范。它由一组状态组成,其中一个是当前状态。对于每个状态,我们列出对该状态有意义的事件。对于每个事件,我们定义出系统新的当前状态。
看看时间戳:这三个请求,或者说是三个单独的流,是并行处理的,第一个返回的是id 2,用了 82ms,后面两个分别在 50ms和 51ms后返回。 事件流是异步集合 在前面的例子中,我们的用户 ID列表(在被观测对象 users中)是静态的。但不一定非得如此。也许我们想在人们登录网站时收集这些信息。我们所要做的就是在创建会话时生成一个包含用户ID 的被观测事件,并用这个被观测对象来取代那个静态对象。这样,我们就可以在收到这些ID 的同时,获取用户的详细信息,大概还能将信息存储到某个位置去。 这是一个非常强大的抽象方法:我们不再须要将时间视为必须管理的东西。事件流将同步和异步处理统一到一个通用的、方便的 API之后。
# 30 变换式编程
我们需要重新把程序视为从输入到输出的一个变换
有时候,找到变换的最简单方法是,从需求开始并确定它的输入和输出。现在你已经将整个程序表示为函数
# 31 继承税
这么斩钉截铁的说停下来还是第一次听说 你用面向对象的语言编程吗?使用继承吗? 如果是这样,那么停下来!继承可能做不到你想做的事情。
这是在反讽
那些不喜欢拍键盘的人,通过继承将基类的公共功能添加到子类中,来保护他们的手指
继承就是耦合
# 32 配置
虽然静态配置很常见,但目前我们倾向于另一种做法。我们仍然希望配置数据保持在应用程序外部,但不直接放在文件中,也不放在数据库里;而是储存在一个服务 API之后。这样做有很多好处:· 在身份认证和访问权限控制将多个应用程序的可见内容阻隔开的情况下,让多个应用程序可以共享配置信息· 配置的变更可以在任何地方进行· 配置数据可以通过专有 UI 维护· 配置数据变得动态最后一点,配置应该是动态的,这在我们转向高可用性应用程序时至关重要。为了改变单个参数就必须停下来重启应用程序,这样的想法已完全脱离当前的现实。使用配置服务,应用程序的组件可以注册所使用参数的更新通知,服务可以在配置被更改时发送包含了新值的消息。
不要因为懒,就放弃做决策,而把分歧做成配置。如果对某个特性应该这样工作还是那样工作真的存在争议,或者不知道是否应该让用户选择,那么就去找个方法试试,通过反馈来决定哪一种更好
# 第6章 并发
这个定义比较清晰 并发性指的是两个或更多个代码段在执行过程中表现得像是在同时运行一样。并行性是指它们的确是在同一时刻一起运行。想获得并发性,需要在一个特殊的环境下运行代码。当代码运行时,这个环境可以在其不同部分之间切换执行过程。这样的环境通常基于纤程、线程、进程来实现。想获得并行性,则需要有可以同时做两件事情的硬件,通常是同一 CPU上的多个核心、同一机器上的多个 CPU,或是连接在一起的多台计算机。
并发性指的是两个或更多个代码段在执行过程中表现得像是在同时运行一样。并行性是指它们的确是在同一时刻一起运行。想获得并发性,需要在一个特殊的环境下运行代码。当代码运行时,这个环境可以在其不同部分之间切换执行过程。这样的环境通常基于纤程、线程、进程来实现。想获得并行性,则需要有可以同时做两件事情的硬件,通常是同一 CPU上的多个核心、同一机器上的多个 CPU,或是连接在一起的多台计算机。
开发人员经常讨论代码块之间的耦合。他们指涉的是依赖关系,以及这些依赖关系是如何让事物变得难以变更的。但还有另一种形式的耦合。如果代码给几件事情强加一个顺序进来,而这个顺序对解决手头问题而言并非必需,就会发生时域耦合。
# 33 打破时域耦合
“时域耦合[1]到底是指什么?”它指的是时间
时间是软件架构中经常被忽略的一个方面
时间对我们来说有两个重要的方面:并发性(在同一时刻发生的多件事情)以及次序(事情在时间轴上的相对位置)
这是之前没有优先考虑过的,只有出了问题才会考虑这个问题,之后需要有这么一根弦
我们通常不会在编程时考虑这两个方面。当人们刚开始坐下来设计架构或编写程序时,倾向于将事情线性化。这符合绝大多数人的思考方式——先做这个,再做那个。但这样的思考方式会导向时域耦合——在时间范畴上产生耦合:A 方法必须在 B 方法之前调用;一次只能运行一个报告;在按钮按下前必须等屏幕先重绘;“嘀嘀”一定在“嗒嗒”之前发生。
精彩,那句“系统更容易推理,潜在的响应更快、更可靠”
我们需要考虑并发性,并且考虑对时间依赖或顺序依赖解耦。通过这样做,我们可以在开发的许多领域中获得灵活性并减少任何基于时间的依赖:工作流分析、体系结构、设计和部署。结果将是系统更容易推理,潜在的响应更快、更可靠。
我们需要考虑并发性,并且考虑对时间依赖或顺序依赖解耦。通过这样做,我们可以在开发的许多领域中获得灵活性并减少任何基于时间的依赖:工作流分析、体系结构、设计和部署。结果将是系统更容易推理,潜在的响应更快、更可靠。
这就是我们在针对并发性做设计时所追求的东西。我们希望找到那些耗时活动中,无须把时间花在代码上的活动,例如查询数据库、访问外部服务、等待用户输入——所有这些事情时常会让我们的程序卡住,直到操作完成。这些等待的间隙,都有机会去做一些更有意义事情,总比让 CPU闲得无聊要好。
并发性是一种软件机制,而并行性则和硬件相关。
# 34 共享状态是不正确的状态
随机故障通常是并发问题
# 35 角色与进程
基于角色的模型还是第一次听说,java里面可以用akka框架,为什么国内大厂不用呢,都在死磕spring · 你当前是否还在写用互斥量来保护共享数据的代码。为什么不试试使用相同的代码基于角色模型来做一个原型呢?
# 第7章 当你编码时
测试不是关于找Bug的工作,而是一个从代码中获取反馈的过程,涉及设计的方方面面,以及API、耦合度等。这意味着,测试的主要收益来自于你思考和编写测试期间,而不是运行测试那一刻。
我们大多数人都能在很大程度上以自动辅助驾驶的心态驾驶汽车:不会明确地命令自己的脚去踩踏板,也不会明确地命令自己的手臂去转动方向盘——我们仅仅在想着“慢一点,向右转”。然而,可靠的、安全的驾驶员会不断地复查各种情况,检查潜在的问题,并且让自己处于良好的位置,以防意外发生。同样的道理也适用于编写代码——编码可能在很大程度上只是例行公事,但保持头脑清醒可以很好地防止灾难的发生。
# 37 听从蜥蜴脑
有时候有某种感觉 诀窍首先是注意到它正在发生,然后找出原因。
诀窍首先是注意到它正在发生,然后找出原因。
你可能只是担心自己会犯错。
说到底是把代码作为自身能力的反应
我们这些开发者在代码中投入了大量精力,因而会把代码中的错误看作是自身能力的反映
我们这些开发者在代码中投入了大量精力,因而会把代码中的错误看作是自身能力的反映
也许你只是创造了一个只会招来Bug的蚂蚁农场
我们发现了一种有效的脑力突破的方法——告诉自己该做原型了
当你发现事情以一种奇怪的方式完成时,把它记下来。持续这样做,试着寻找模式
学会在编码时听从直觉是一项需要培养的重要技能
听从你的直觉,在问题跳出来之前加以避免
# 38 巧合式编程
我们应该避免通过巧合编程,因为靠运气和意外来获得成功是行不通的,编程应该深思熟虑
人们很容易被这种想法愚弄。为什么要冒把能工作的东西搞砸的风险呢?我们可以想到几个原因:· 它可能并非真的在工作——或许只是看起来是这样。· 你所依赖的边界条件可能只是一个偶然现象。在不同的环境中(不同的屏幕分辨率,或更多的CPU内核),它的行为可能不同。· 未计入文档的行为可能会随着库的下一个版本而改变。· 额外的不必要的调用会使代码变慢。· 额外的调用增加了引入新 Bug 的风险。
对于你所编写的由其他人调用的代码,遵守一些基本原则总是有用的,比如良好的模块化,以及将实现隐藏在短小且文档清晰的接口后面。一份详细说明的契约
和结果接近是不够的
在某些时候“只是”偏差了1,是一个巧合,它掩盖了一个深层次的更为根本的缺陷。如果没有恰当的时间处理模型,整个庞大的代码库就会随着时间的推移而退化,直到难以维持大量的 +1和-1语句。最终,没有一处是正确的,这个项目被废弃了。
不要假设,要证明。
如果希望花费更少的时间来编写代码,就要在开发周期中尽可能早地捕获并修复错误,这样可以一开始就少犯错。
时刻注意你在做什么。Fred是慢慢让事情失去控制的,直到最后,像第9页的青蛙一样被煮熟。
是这样的。
你能向一个更初级的程序员详细解释一下代码吗?如果做不到,也许正在依赖某个巧合。
你能向一个更初级的程序员详细解释一下代码吗?如果做不到,也许正在依赖某个巧合。
不要在黑暗中编码。构建一个没有完全掌握的应用程序,或者使用一个并不理解的技术,就很可能会被巧合咬伤。如果不确定它为什么能用,就不会知道它为什么出错
要按计划推进,不管这个计划是在脑子里,还是在鸡尾酒餐巾纸的背面,或是在白板上
对,将假设文档化,方便跟人沟通
将假设文档化。第104的话题23:契约式设计,可以帮助你在心中澄清设想,也可以帮助你与他人沟通。
将假设文档化。第104的话题23:契约式设计,可以帮助你在心中澄清设想,也可以帮助你与他人沟通。
· 不要只测试代码,还要测试假设。不要猜,去实际试一下。写一个断言来测试假设(参见第115页的话题25:断言式编程)。如果断言是正确的,那么说明你已经改进了代码中的文档。如果发现假设是错误的,那么你应该感到幸运
为你的精力投放排一个优先级。要把时间花在重要的方面(事实上,这往往正是比较困难的部分)。如果根本原理或基础设施都会出问题,花哨的外表则更是不堪一击
不要成为历史的奴隶。不要让现有的代码去支配未来的代码。如果不再合适,所有代码都可以替换。即使一个程序正在进展中,也不要让已经做完的事情限制下一步要做的事情——准备好重构(参见第216页的话题40:重构)。这个决定可能会影响项目进度。这里的假设是影响小于不进行更改造成的开销
振聋发聩,这篇全程高能
所以下次碰到有什么事情看起来可行,但你不知道为什么,确保它不是一个巧合。
所以下次碰到有什么事情看起来可行,但你不知道为什么,确保它不是一个巧合。
# 40 重构
软件更像是园艺而非建筑——它更像一个有机体而非砖石堆砌
为了保证外部行为没有改变,你需要良好的自动化单元测试来验证代码的行为
随着时间的推移,代码中的附带损害有可能致命
重构,和大多数事情一样,在问题很小的时候做起来更容易,要把它当作编码日常活动
重构的核心是重新设计
如果你执拗地非要将海量的代码统统撕毁,可能会发现,自己所处的境地,比开始时更加糟糕
和我认识的不一样,应该读读重构那本书
简单技巧,可以用来确保进行重构不至于弊大于利:[8] 1.不要试图让重构和添加功能同时进行。 2.在开始重构之前,确保有良好的测试。尽可能多地运行测试。这样,如果变更破坏了任何东西,都将很快得知。 3.采取简短而慎重的步骤:将字段从一个类移动到另一个类,拆分方法,重命名变量
简单技巧,可以用来确保进行重构不至于弊大于利:[8]1.不要试图让重构和添加功能同时进行。2.在开始重构之前,确保有良好的测试。尽可能多地运行测试。这样,如果变更破坏了任何东西,都将很快得知。3.采取简短而慎重的步骤:将字段从一个类移动到另一个类,拆分方法,重命名变量
保持小步骤,并在每个步骤之后进行测试,就能避免冗长的调试。
维护一个良好的回归测试,这才是安全重构的关键
如果不得不进行超过重构范围的工作,而且会以改变外部行为或接口收场,那么通过刻意破坏构建,让代码过去的客户无法通过编译,可能会有所帮助。这样做可以让你知道什么需要更新。下一次看到一段代码与它应该有的样子不符时,要把它修好。这其实是在控制疼痛——尽管现在很痛,但以后会痛得更厉害,那么就忍痛赶紧干完
# 41 为编码测试
醍醐灌顶
我们相信,测试获得的主要好处发生在你考虑测试及编写测试的时候,而不是在运行测试的时候
我们相信,测试获得的主要好处发生在你考虑测试及编写测试的时候,而不是在运行测试的时候
测试驱动编码
为方法写一个测试的考虑过程,使我们得以从外部看待这个方法,这让我们看起来是代码的客户,而不是代码的作者
测试所提供的反馈至关重要,可以指导编码过程。与其他代码紧密耦合的函数或方法很难进行测试,因为你必须在运行方法之前设置好所有环境。所以,让你的东西可测试也减少了它的耦合
确实也在践行
如果你在开始编写代码之前,就考虑过测试边界条件及其工作方式,那么就很可能会发现简化函数的逻辑模式。如果你考虑过需要测试的错误条件,那么将会相应地去构造这个函数
如果你在开始编写代码之前,就考虑过测试边界条件及其工作方式,那么就很可能会发现简化函数的逻辑模式。如果你考虑过需要测试的错误条件,那么将会相应地去构造这个函数
TDD 的基本循环是:1.决定要添加一小部分功能。2.编写一个测试。等相应功能实现后,该测试会通过。3.运行所有测试。验证一下,是否只有刚刚编写的那个测试失败了。4.尽量少写代码,只需保证测试通过即可。验证一下,测试现在是否可以干净地运行。5.重构代码:看看是否有办法改进刚刚编写的代码(测试或函数)。确保完成时测试仍然通过
务必实践一下 TDD。但真这样做时,不要忘记时不时停下来看看大局。人们很容易被“测试通过”的绿色消息所诱惑,从而编写大量的代码,但实际上这些代码并不能让你离解决方案更近
可以看到一个聪明的人是如何被通过测试的喜悦套牢,开始被琐事分心
我们坚信,构建软件的唯一方法是增量式的。构建端到端功能的小块,一边工作一边了解问题。应用学到的知识持续充实代码,让客户参与每一个步骤并让他们指导这个过程
每次认准一个小功能,这个功能得到客户认可,有了目标,再让测试驱动
测试对开发的驱动绝对能有帮助。但是,就像每次驱动汽车一样,除非心里有一个目的地,否则就可能会兜圈子
测试对开发的驱动绝对能有帮助。但是,就像每次驱动汽车一样,除非心里有一个目的地,否则就可能会兜圈子
需要从一开始就在软件中构建可测试性,并在尝试将每个部分连接在一起之前,对它们进行彻底的测试
错误的做法
需要决定在单元这个级别上测试什么。由来已久的做法是,程序员抛给代码一些随机的数据,再查看一下打印语句,随即就声称测试已通过
我们想要编写测试用例来确保指定的单元遵守了契约。这样做能告诉我们两件事:代码是否符合契约,以及契约是否具有我们所认为的含义
有意思的比喻
在生产环境那潮湿、温暖的条件下,木器更容易长虫
你编写的所有软件最终都将被测试——如果不是由你和你的团队做测试,那么就将由最终的用户去测试
——所以不妨计划好进行彻底的测试。稍微提前考虑一下,就能大大降低维护成本,减少求助电话
最糟糕的做法基本可以统称为“以后再测”。开什么玩笑,“以后再测”实际上意味着“永不测试”
测试的好处更主要来自于思考测试,以及思考测试会对代码造成怎样的影响。在长时间坚持这样做之后,我写不写测试都会这样思考。代码仍然是可测试的,只是无须真的写出测试而已
# 42 基于特性测试
考虑测试的最大好处之一就是它能指导你写代码
一旦实现了契约和不变式(我们将把它们放在一起并称为特性),就可以使用特性来自动化我们的测试
自己编写测试,用自动化工具进行测试
使用基于特性的测试来校验假设
想到一个问题,写这些测试,是需要一个体系的,比如编辑器要调试好、测试框架要选好等等,要不然会拖慢开发。
这就是基于特性的测试既强大又给人挫折之处。说它强大是因为,只要建立一些规则来生成输入,设定好断言来验证输出,就可以任其发展。至于会发生什么,你并不完全知道——测试可能通过,断言也可能失败,又或者代码可能因无法处理所给定输入而完全失败。 挫折之处在于,确定失败的原因可能很棘手。 我们的建议是,当基于特性的测试失败时,找出传递给测试函数的参数,然后使用这些值创建一个单独的、常规的单元测试。单元测试为你做了两件事。第一,它使你可以将注意力集中在问题上,而避开所有那些基于特性的测试框架产生的对代码的额外调用。第二,单元测试可充当回归测试。因为基于特性的测试所传递的参数是随机生成的,不能保证在下一次运行测试时使用相同的值;而单元测试则能强制使用这些值,确保 Bug 不会通过。
单元测试、特性测试需要有完整的体系(比如编辑器要设置好,自动测试工具设置好等等),要不会拖慢开发进度,但是这个却是一劳永逸的事情,值得去做
基于特性的测试对设计也有帮助 当我们在谈单元测试时说过,其主要好处之一是,它能强制你以特定方式思考代码:单元测试是 API 的第一个客户。 这一点对基于特性的测试也同样成立,只是方式略有不同。基于特性的测试让你从不变式和契约的角度来考虑代码;你会思考什么不能改变,什么必须是真实的。这种额外的洞察力会对代码产生神奇的影响,可以消除边界情况,并突显使数据处于不一致状态的函数。 我们相信基于特性的测试是对单元测试的补充:二者处理不同的关注点,并且都能带来各自的好处。如果你现在还没有使用,那就试试吧。
# 43 出门在外注意安全
接下来要做的是分析代码中那些可能出错的路径,并将其添加到测试套件中。你要考虑传入错误的参数、泄漏的资源或资源不存在等此类事情
务实的程序员有相当多的偏执。我们知道自己有缺陷和限制,外部攻击者会抓住每个我们留下的漏洞去破坏系统。
1.将攻击面的面积最小化2.最小特权原则3.安全的默认值4.敏感数据要加密5.维护安全更新
可以将复杂的代码视作元凶,是它使攻击面脆弱多孔、易受感染。同样,代码越简单越好。更少的代码意味着更少的 Bug,更少机会出现严重安全漏洞。更简单、更紧凑、复杂度更小的代码更好推理,更容易发现潜在的弱点
永远不要信任来自外部实体的数据,在将其传递到数据库、呈现视图或其他处理过程之前,一定要对其进行消毒
# 44 事物命名
名不正,则言不顺;言不顺,则事不成
事物应该根据它们在代码中扮演的角色来命名。这意味着,无论何时,只要你有所创造,就需要停下来思考“我这一创造的动机是什么?”
只是换个名字,就能通过代码不断地提醒我们,这个人打算做什么,做的事情对我们意味着什么
# 第8章 项目启动之前
读一下需求之坑,学习如何避免常见的陷阱
# 45 需求之坑
需求很少停留在表面
心有戚戚焉
通常情况下,它们被埋在层层的假设、误解和政治之下。更糟糕的是,需求通常根本不存在
通常情况下,它们被埋在层层的假设、误解和政治之下。更糟糕的是,需求通常根本不存在
程序员的用武之地。我们的工作是帮助人们了解他们想要什么。事实上,这可能是我们最有价值的属性,因而值得一再重申
最初对需求的声明,往往并非绝对化的要求。客户可能没有意识到这一点,但一定希望你能一起去探索。
当某些事情看起来很简单的时候,我们却会去寻找那些边缘情况,并就其不胜其烦地问人。
寻找边界情况
当某些事情看起来很简单的时候,我们却会去寻找那些边缘情况,并就其不胜其烦地问人
以前的毛病是照单全收
你充当的角色是解释客户所说的话,并向他们反馈其中的含义。这既是一个演绎性过程,又是一个创造性过程:你会灵机一动,为一个更好的解决方案添砖加瓦,最终方案会比你或客户单独提出的方案更好
你充当的角色是解释客户所说的话,并向他们反馈其中的含义。这既是一个演绎性过程,又是一个创造性过程:你会灵机一动,为一个更好的解决方案添砖加瓦,最终方案会比你或客户单独提出的方案更好
开发者获取需求并将结果反馈给客户。这开启了探索之旅。在探索过程中,随着客户尝试不同的解决方案,你可能会得到更多的反馈。这是所有需求采集过程的现实情况
务实的程序员藉由“你是不是这个意思”这样的客户访谈来得到反馈
该我们对客户反馈的“这不是我的意思”做出回应了,就用这一句——“是不是更像这样”
即使是在项目结束时,我们仍在解释客户的需求。事实上,到那个时候,我们可能会有更多的客户:QA 人员、运营人员、市场人员,甚至可能是消费者组成的测试群体
心有戚戚焉
针对更普遍的情况做实现,至于系统需要支持的那种特定类型的东西,只是通用实现在加入策略信息后的示例
针对更普遍的情况做实现,至于系统需要支持的那种特定类型的东西,只是通用实现在加入策略信息后的示例
成功的工具会让用的人觉得称手。成功的需求采集也会将其考虑在内。
工具会让用的人觉得称手。成功的需求采集也会将其考虑在内。这也就是为什么使用原型或曳光弹做早期
正如我们所讨论的,客户并不确切知道自己想要什么。所以,当我们需要把他们所说的东西,扩展为几近一份法律文件时,就好像是在流沙上建造一座难以置信的复杂城堡
客户从不阅读规范。客户之所以请程序员来,是因为程序员会对所有的细节和细微之处感兴趣,尽管客户的动机只是解决一个高阶的、有些模糊的问题。需求文档是为开发人员编写的,其中包含的信息和细微之处有时难以理解,并且常常让客户感到乏味
不是贬低客户,但给他们一份庞大的技术文档,就像是给普通开发者一份荷马时代希腊语的《伊利亚特》,并让他们依此编写出一个视频游戏
通过保持需求的简短陈述,鼓励开发人员去澄清问题。这样可以在创建每段代码之前,以及在创建期间,对客户和程序员之间的反馈过程进行强化
我们可以做些什么来防止需求蔓延开呢?答案(又一次)是反馈。如果你在与客户一起工作时,一直通过持续反馈来进行迭代,那么客户对“特性又多了一个”产生的影响,将有切身体会。他们将看到另一张故事卡出现在面板上,继而帮你另选一张卡,以为进入下次迭代腾出空间。反馈是双向的
创建并维护一张项目术语表,在上面记录项目中所有特定术语和词汇的定义。
非常有必要,可以用腾讯文档
创建并维护一张项目术语表,在上面记录项目中所有特定术语和词汇的定义。
项目所有的参与者,包括最终用户和支持员工,都应该使用同一张术语表来确保一致性。
# 46 处理无法解决的难题
找到约束条件 有句流行语“跳出框框思考”,鼓励我们认识到可能不适用的约束,并忽略它们。但这句话并不完全准确。如果“框框”是约束和条件的边界,那么诀窍就是找到框框,它可能比你想象的要大得多
有句流行语“跳出框框思考”,鼓励我们认识到可能不适用的约束,并忽略它们。但这句话并不完全准确。如果“框框”是约束和条件的边界,那么诀窍就是找到框框,它可能比你想象的要大得多
解决谜题的关键是,认识到你所受到的约束和你所拥有的自由度,因为认识到这些就会找到答案。这就是为什么有些谜题如此有效——我们太容易忽视潜在的解决方案。
你必须挑战任何先入之见,并评估它们是否是真实的、硬性的约束
问题不在于你是在框框里思考还是在框框外思考,问题在于找到框框——识别真正的约束条件
这是暂时做点别的事情的理想时间。做点不同的事情,比如遛遛狗;先让自己放空一下,晚一点再继续
也许你已经在原计划上延误很久,或者已感到绝望,觉得系统不可能再正常工作,因为解决这个特殊的问题是“无法做到的
如果你就是不愿意让这个问题搁置一段时间,那么最好的办法就是找个人去解释一下这个问题。通常情况下,围绕它的简单谈论就可以让你分心,从而得到启迪
让他们问你一些问题,比如:· 为什么你在解决这个问题?· 解决这个问题有什么收益?· 你遇到的问题是否与边界情况有关?你能消除这些边界情况吗?· 有没有一个更简单的相关问题,是你能解决的?这是另一个实践橡皮鸭的例子。
在观察的领域里,命运总是垂青有准备的人
日常工作中,将什么行得通什么行不通反馈给大脑,是供养大脑的最好方法
就是记录工程日记
# 47 携手共建
与用户密切合作的建议贯穿本书;用户是你团队的一部分。
这就是我们所说的“一起工作”的真正含义:不仅仅是提问、讨论、做笔记,还要在真正编码的同一时刻提问和讨论
不要一个人埋头钻进代码中
# 48 敏捷的本质
第一次知道他真正的含义,可笑天天把这个词挂在嘴边的人做的事情跟它相反 我们一直在实践中探寻更好的软件开发方法,身体力行的同时也帮助他人。由此我们建立了如下价值观: · 个体和互动高于流程和工具 · 工作的软件高于详尽的文档 · 客户合作高于合同谈判 · 响应变化高于遵循计划
这些价值观,是由不断发现更好软件生产方法的行为,所激发和显露出来的。这不是一份静态的文档,它是对生产过程的建议
这些价值观不会告诉你该做什么。当你自己决定要做点什么的时候,它们会告诉你要去追寻什么
敏捷方式工作的秘诀:1.弄清楚你在哪里。2.朝想去的方向迈出有意义的最小一步。3.评估在哪里终结,把弄坏的东西修好。重复这些步骤,直到完成。在每一件事的每个层面上递归地使用这些步骤。
# 第9章 务实的项目
魔法三连”:版本控制、测试和自动化组成的务实的入门套件
在傲慢与偏见中,我们要求你在作品上签名,并为自己的工作感到自豪
# 49 务实的团队
程序员有点像猫:聪明、意志坚强、固执己见、独立,并且经常引起网络崇拜
成为一个务实的人,有很多优势,但是如果这样的个体能在一个务实的团队中工作,优势会成倍增加
团队作为一个整体,不应该容忍破碎的窗户——那些没人去修的小问题
非常认可,如果存在一个“质检员”,很快,开发人员会放松自己对质量的要求,让质量问题突破防线去进攻“质检员”
在一些团队方法中,团队会设一个“质量官”——由这个人对交付产品的质量负责。这显然是荒谬的:质量只能来自团队每个成员的独立贡献。质量是内在的,无法额外保证。
在一些团队方法中,团队会设一个“质量官”——由这个人对交付产品的质量负责。这显然是荒谬的:质量只能来自团队每个成员的独立贡献。质量是内在的,无法额外保证。
整个团队甚至更容易被一锅炖熟。每个人都觉得有别人在处理问题,或是觉得领导一定已经同意用户请求的变更。即使是最用心的团队,也可能对项目中的重大变化浑然不觉
要和这些现象做斗争。鼓励每个人积极监控环境的变化。保持清醒,对项目范围扩大、时间缩短、额外特性、新的环境——任何在最初的理解中没有的东西,都要留心。对新的需求要保持度量[2]。团队不必对变化导致的失控心存抗拒——只需要知道变化正在发生就可以。否则,置身沸水的人就会是你
只有在你花时间观察周围,找出什么是有效的,什么是无效的,然后做出改变的时候,持续的改进才有可能发生。(参见第267页的话题48:敏捷的本质)。太多的团队忙于排水,而没有时间修补漏洞。把这件事排入日程表并加以解决。
在团队中要实现这一点,意味着你需要拥有所有技能:前端、UI/UX、服务器、DBA、QA,等等,而且这些技术彼此之间也需要协调、融洽
自动化是每个项目团队的基本组成部分。确保团队拥有构建工具的技能,以便可以构建和部署工具,用其来将项目开发和生产部署自动化
# 50 椰子派不上用场
模仿的是形式,不是内容。人类学家称之为货物崇拜
我们的目标当然不是“使用 Scrum”、“进行敏捷”、“做到精益”或诸如此类的事情。我们的目标是交付可以工作的软件,让用户马上能获得新的功能。不是几周、几个月或几年以后,而是现在。对于许多团队和组织来说,持续交付感觉像是一个崇高的、无法实现的目标,特别是当你背负着一个将交付时间限制在几个月甚至几周的流程时。但和任何目标一样,最关键的是要保持瞄着正确的方向。
试试
如果交付周期是几年,试着把周期缩短到几个月。如果是几个月,就减少到几周。如果是一个四周的冲刺,那么试一下两周。如果是两周的冲刺,试试一周。然后到每天。最后是即期交付。请注意,能够即期交付并不意味着你必须每天每分钟都交付。只有当这样做在业务上有意义时,才有必要在用户需要时即期交付。
椰子是指形式的没有效果的东西
结果和用椰子差不多
# 51 务实的入门套件
心有戚戚焉 无论是构建和发布过程、测试、项目文书工作,还是项目上的任何其他重复任务,都必须是自动的,并且在任何能力足够的机器上都是可重复的
无论是构建和发布过程、测试、项目文书工作,还是项目上的任何其他重复任务,都必须是自动的,并且在任何能力足够的机器上都是可重复的
首先,它允许构建的机器朝生暮死。
版本控制在项目级别驱动构建和发布流程
对追求效率的公司非常有用
构建、测试和部署通过提交或推送给版本控制来触发,并在云容器中完成创建。发布到交付阶段,还是生产阶段,可以通过在版本控制系统中打标记来指定。这样,发布就不再有那么强的仪式感,变成了日常生活中的一部分——这是真正的持续交付,没有绑定到任何一台构建机器或开发人员的机器上
构建、测试和部署通过提交或推送给版本控制来触发,并在云容器中完成创建。发布到交付阶段,还是生产阶段,可以通过在版本控制系统中打标记来指定。这样,发布就不再有那么强的仪式感,变成了日常生活中的一部分——这是真正的持续交付,没有绑定到任何一台构建机器或开发人员的机器上
大写加粗
尽早测试,经常测试,自动测试
尽早测试,经常测试,自动测试
测试状态覆盖率,而非代码覆盖率
有了这三件套:版本控制、无情的测试和完全自动化,项目就有了你所需要的坚实基础,这样就可以将精力集中在困难的部分:取悦用户。
# 52 取悦用户
当你吸引别人的时候,目标不是从他们身上赚钱或者让他们做你想做的事,而是让他们充满快乐
作为开发者,我们的目标是取悦用户。这就是我们身在其位的原因。不要为了数据就从他们身上深挖,不要数他们的眼球,不要掏空他们的钱包。撇开邪恶的目标不谈,只是及时交付能工作的软件,还远远不够。这本身无法取悦他们。用户真正要的不是代码,他们只是遇到某个业务问题,需要在目标和预算范围内解决。他们的信念是,通过与你的团队合作,能够做到这一点。
这个项目在完成一个月(或是一年,不管多久)之后,你根据什么来判断自己已经取得成功?
根据用户期望分析用户需求
确保团队中的每个人都清楚这些期望。 · 在做决定的时候,想想哪条路更接近这些期望。 · 根据期望严格分析用户需求。在许多项目中,我们已经发现,所陈述的“需求”实际上只是对用技术可以完成哪些工作的猜测——它实际上是一个业余的实现计划,只是伪装成需求文档。如果你能证明有方法会使项目更接近目标,那么就不要害怕,大胆提出改变需求的建议。 · 随着项目的进展,继续考虑这些期望。
确保团队中的每个人都清楚这些期望。· 在做决定的时候,想想哪条路更接近这些期望。· 根据期望严格分析用户需求。在许多项目中,我们已经发现,所陈述的“需求”实际上只是对用技术可以完成哪些工作的猜测——它实际上是一个业余的实现计划,只是伪装成需求文档。如果你能证明有方法会使项目更接近目标,那么就不要害怕,大胆提出改变需求的建议。· 随着项目的进展,继续考虑这些期望。
全书的精髓
如果你想取悦客户,就和他们建立起某种关系,这样即可积极地帮助他们解决问题。或许你的头衔只是“软件开发者”或“软件工程师”的某种变体,而事实上这个头衔应该是“解决问题的人”。这就是我们所做的,也是一个务实的程序员的本质。 我们在解决问题。
如果你想取悦客户,就和他们建立起某种关系,这样即可积极地帮助他们解决问题。或许你的头衔只是“软件开发者”或“软件工程师”的某种变体,而事实上这个头衔应该是“解决问题的人”。这就是我们所做的,也是一个务实的程序员的本质。我们在解决问题。
# 53 傲慢与偏见
保持匿名会滋生粗心、错误、懒惰和糟糕的代码,特别是在大型项目中——很容易把自己看成只是大齿轮上的一个小齿,在无休止的工作汇报中制造蹩脚的借口,而不是写出好的代码
人们应该在一段代码上看到你的名字,并对它是可靠的、编写良好的、经过测试的、文档化的充满期许
# 跋
看到这里有点失落感(应该是不舍),它解答了我很多的疑问,也让我很多的想法得到了印证,之后应该会不断对这本书“反刍” 跋 长远来说,我们塑造生活,塑造自己。直至死亡为止,这过程永不会完结。最终我们总要为所做的抉择负上责任。 ——埃莉诺·罗斯福 在第一版问世前的20年里,计算机从一种非主流的新奇事物,进化为当今时代的企业必备,而我们有幸成为这一进化的一部分。从那以后的二十年间,软件快速发展,不仅突破了单纯商务机器的范畴,甚至可以说接管了世界。但这对我们来说意味着什么呢? 在《人月神话:软件项目管理之道》[Bro96]中,弗雷德里克·布鲁克斯说过:“程序员,就像诗人一样,几乎仅仅工作在单纯的思考中。他们运用自己的想象,来建造自己的城堡。”我们从一张白纸开始,几乎可以创造任何我们能想象到的东西。我们创造的东西可以改变世界。 从帮助人们规划变革运动的 Twitter,到用来防止汽车打滑的处理器,再到让我们不必记住日常烦琐细节的智能手机,我们的程序无处不在,我们的想象力无处不在。 我们这些开发者,拥有令人难以置信的特权——我们真正在建设未来,这是一股巨大的力量。伴随着这种力量而来的,是一种非同寻常的责任。 我们隔多久会停下来思考这个问题?对于这一责任究竟意味着什么,我们在自己人之间,以及和更广泛的读者一起,隔多久会讨论一次? 比起笔记本电脑、台式机和数据中心,嵌入式设备用到的计算机要多一个数量级。这些嵌入式计算机通常控制着生命攸关的系统,从发电厂到汽车再到医疗设备。即使是简单的中央供暖控制系统或家用电器,如果设计或实施不当,也会致人死亡。当你为这些设备做开发时,所承担的责任是很惊人的。 许多非嵌入式系统同时具备好的一面和坏的一面。社交媒体可以促进和平改革,也能煽动丑陋的仇恨。
把这个写到周报里面吧
对于我们交付的每一段代码,我们有义务问自己两个问题: 1.我已经保护好用户了吗? 2.我自己会用它吗?
对于我们交付的每一段代码,我们有义务问自己两个问题:1.我已经保护好用户了吗?2.我自己会用它吗?
使用 小悦记 导出 | 2023年11月25日