├── .gitignore ├── Chapter1 ├── 你的知识组合.md ├── 务实主义哲学.md ├── 沟通.md ├── 猫吃了我的源代码.md ├── 石汤和煮青蛙.md ├── 足够好的软件.md ├── 软件熵.md └── 这是你的人生.md ├── Chapter10 └── 刊后语.md ├── Chapter2 ├── 务实的方法.md ├── 原型和便签.md ├── 可逆性.md ├── 域语言.md ├── 好设计的本质.md ├── 正交性.md ├── 示踪子弹.md ├── 评估.md └── 重复的恶魔.md ├── Chapter3 ├── shell.md ├── 基本工具.md ├── 工程日记.md ├── 强大的编辑.md ├── 文本处理.md ├── 版本控制.md ├── 纯文本的力量.md └── 调试.md ├── Chapter4 ├── 别开过头了.md ├── 契约设计.md ├── 如何平衡资源.md ├── 断言式编程.md ├── 死程序不说谎.md └── 程序性妄想症.md ├── Chapter5 ├── 弯曲或折断.md ├── 杂耍现实世界.md ├── 继承税.md ├── 解耦.md ├── 转换编程.md └── 配置.md ├── Chapter6 ├── actors和进程.md ├── 共享状态不正确.md ├── 并发.md ├── 断开时间耦合.md └── 黑板.md ├── Chapter7 ├── 代码测试.md ├── 命名.md ├── 在某处保持安全.md ├── 基于属性的测试.md ├── 巧合编程.md ├── 当你编程时.md ├── 算法速度.md ├── 聆听你的蜥蜴脑.md └── 重构.md ├── Chapter8 ├── 敏捷的本质.md ├── 解决不可能的难题.md ├── 需求坑.md └── 项目之前.md ├── Chapter9 ├── 傲慢与偏见.md ├── 务实的团队.md ├── 务实的项目.md ├── 实用入门套件.md ├── 椰子不要切碎.md └── 让用户满意.md ├── README.md └── assets ├── Orthogonality.png ├── bullet.png ├── layering.png ├── topic28_1.png ├── topic28_2.png ├── topic29_1.png ├── topic29_2.png ├── topic29_3.png ├── topic29_4.png ├── topic30_1.png ├── topic31_1.png ├── topic31_2.png ├── topic33_1.png ├── topic34_1.png ├── topic36_1.png ├── topic39_1.png ├── topic44_1.png └── topic49_1.png /.gitignore: -------------------------------------------------------------------------------- 1 | _book 2 | node_modules 3 | -------------------------------------------------------------------------------- /Chapter1/你的知识组合.md: -------------------------------------------------------------------------------- 1 | # 你的知识组合 2 | 3 | 4 | > _知识的投资永远会带来最大的收益。_ 5 | > 6 | > _-- 本杰明 富兰克林_ 7 | 8 | 对于一个像老本·富兰克林精辟的讲道者来说永远不会手足无措。 为什么,如果我们可以早睡早起,那么我们将成为优秀的程序员,对吗? 早起的鸟儿有虫吃,但是早起的虫子会怎样? 9 | 10 | 不过,在这种情况下,本真是一针见血。你的知识和经验是你最重要的日常职业资产, 11 | 12 | 不幸的是,随着新技术、语言和环境的发展,你的知识会变得过时。不断变化的市场力量可能会使你的经验过时或无关紧要。鉴于我们的科技社会日新月异的变化速度,这种情况很快就会发生。 13 | 14 | 随着知识价值的下降,你对公司或客户的价值也在下降。我们要防止这种情况发生。 15 | 16 | 你学习新事物的能力是你最重要的战略资产。但是,你怎么学会学习,你怎么知道该学什么? 17 | 18 | ## 你的知识组合 19 | 20 | 我们喜欢把程序员所知道的关于计算的所有方面、他们所工作的应用程序领域以及他们的所有经验作为他们的知识组合。管理知识组合与管理金融组合非常相似: 21 | 22 | 1. 认真的投资者定期投资是--作为一种习惯。 23 | 2. 多样化是长期成功的关键。 24 | 3. 聪明的投资者在保守投资和高风险、高回报投资之间保持平衡。 25 | 4. 投资者试图低买高卖以获得最大回报。 26 | 5. 投资组合应定期审查和重新平衡。 27 | 28 | 为了在事业上取得成功,你必须用同样的准则投资于你的知识组合。 29 | 30 | 好消息是,做这项投资是一项技能,就像其他任何可以学习的技能一样。诀窍是让你自己一开始就这么做。形成惯例,然后遵循这个模式,直到你的大脑将它内化。到那时,你会发现自己会自动吸收新知识。 31 | 32 | ## 建立你的知识组合 33 | 34 | - ### _定期投资_ 35 | 36 | 就像金融投资一样,你必须定期投资于你的知识组合。即使只是一小部分,习惯本身也和总数一样重要。它可以有助于将目标对准时间和地点,远离常见的干扰。下一节将列出几个目标示例。 37 | 38 | - ### _多样化_ 39 | 40 | 你知道不一样的东西越多,你就越有价值。作为基线,您需要了解当前使用的特定技术的细节。但不要停在那里。面对快速变化的计算技术,今天的热门技术很可能会在明天变得毫无用处(或者至少不需要)。你适应的技术越多,你就越能适应变化。别忘了你需要的其他技能,包括非技术领域的技能。 41 | 42 | - ### _风险管理_ 43 | 44 | 技术存在从高风险、潜在的高回报到低风险、低回报的范围标准。把所有的钱都投资在可能突然崩盘的高风险股票上不是个好主意,也不应该保守地投资所有的钱,错过可能的机会。不要把你所有的技术鸡蛋放在一个篮子里。 45 | 46 | - ### _低买高卖_ 47 | 在一项新兴技术流行之前学习它和发现一只被低估的股票一样困难,但回报也同样值得。在Java刚被引入和未知的时候学习它可能是有风险的,但是当它后来成为一个行业支柱时,则为早期采用者带来了丰厚的回报。 48 | 49 | - ### _审查并重新平衡_ 50 | 51 | 这是一个非常有活力的行业。你上个月开始调查的热门技术现在可能已经彻底过时了。也许你需要重新学习一下你很久没用过的数据库技术。或者,如果你尝试其他语言,也许你能更好地适应新的工作岗位… 52 | 53 | 在所有这些准则中,最重要的一条也是最简单的: 54 | 55 | --- 56 | ## 技巧 9 定期投资你的知识组合 57 | --- 58 | 59 | ## 目标 60 | 61 | 既然你已经有了一些关于添加什么以及何时到你的知识投资组合的指导方针,那么什么是获得知识的最佳方式来为你的投资组合提供资金呢?这里有一些建议。 62 | 63 | - ### _每年至少学习一门新语言_ 64 | 65 | 不同的语言解决相同的问题会有不同的方式。通过学习几种不同的方法,可以帮助你拓宽你的思路,避免陷入陈规。此外,由于有大量的免费软件,学习许多语言是很容易的。 66 | 67 | - ### _每个月看一本技术书_ 68 | 69 | 虽然网上有大量的短文,偶尔也有可靠的答案,但要深入理解,你需要长文书。浏览书商,寻找与当前项目相关的有趣主题的技术书籍。一旦养成习惯,每月读一本书。在你掌握了你目前正在使用的技术之后,分门别类地研究一些与你的项目无关的技术。 70 | 71 | - ### _阅读非技术类的书_ 72 | 73 | 重要的是记住,计算机是由那些你试图满足其需求的人使用的。你和别人一起工作,被别人雇佣,被别人攻击。别忘了人性里平等的一面,因为这需要完全不同的技能集(我们讽刺地称之为软技能,但它们实际上很难掌握)。 74 | 75 | - ### _上课_ 76 | 77 | 在当地大学或在线学院,或在附近的贸易展览或会议上寻找有趣的课程 78 | 79 | - ### _参加本地用户组和聚会_ 80 | 81 | 不要只是去听,而是积极参与。与世隔绝对你的职业生涯是致命的;找出你公司以外的人在做什么。 82 | 83 | - ### _试验不同环境_ 84 | 85 | 如果你只在 Windows 系统工作,花点时间在 Linux 上,如果您只使用 makefile 和编辑器,请尝试使用具有高端功能的复杂IDE,反之亦然。 86 | 87 | - ### _保持最新_ 88 | 89 | 在网上阅读与当前项目不同的技术新闻和帖子。这是一个很好的方法,可以发现其他人对它有什么体验,他们使用的特定术语等等 90 | 91 | 继续投资很重要。一旦你对新语言或新技术感到满意,就继续吧。再学一个。 92 | 93 | 不管你是否在一个项目中使用过这些技术,甚至是否你把它们写在简历上。学习的过程会扩展你的思维,为你打开新的可能性和新的做事方式。异花授粉的想法是很重要的;尝试把你学到的经验应用到你当前的项目中。即使你的项目不使用这种技术,也许你可以借用一些想法。例如,熟悉对象定向,您将以不同的方式编写过程程序。理解函数式编程范式,您将以不同的方式编写面向对象的代码,依此类推。 94 | 95 | ## 学习机会 96 | 97 | 你在贪婪地阅读,你在你的领域里的所有最新的突破性发展处在顶端(不是一件容易做的事情),有人问你一个问题。你一点也不知道答案是什么,你可以随便承认。 98 | 99 | _不要让问题停在那儿。_ 把它当作个人的挑战来寻找答案。四处请教。在网络上搜索学术部分,而不仅仅是把它当做一部分消费掉。 100 | 101 | 如果你自己找不到答案,就找谁能找到答案的那个人。别让问题停下来。与他人交谈将有助于建立你的个人关系网,你可能会惊讶地发现,在这一过程中,其他不相关的问题的解决方案。旧的投资组合也越来越大…。 102 | 103 | 所有这些阅读和研究都需要时间,而且时间已经短缺。所以你得提前计划。总有一些东西要在死气沉沉的时候读。花在等待医生和牙医的时间可以是一个很好的机会来赶上你的阅读,但一定要带上你自己的电子阅读器,否则你可能会发现自己在翻阅1973年一篇关于巴布亚新几内亚的狗耳文章。 104 | 105 | ## 批判性思考 106 | 107 | 最后一点重要的是批判性地思考你所读和听到的东西。你需要确保你的投资组合中的知识是准确的,没有任何一个供应商或媒体炒作。当心那些坚持他们的教条提供唯一答案的狂热者,它可能适用于也可能不适用于你和你的项目。 108 | 109 | 永远不要低估商业主义的力量。仅仅因为一个网络搜索引擎先列出一个热门内容并不意味着它是最好的匹配;内容提供商可以付费获得最高的账单。仅仅因为书店把一本书放在显眼的地方并不意味着它是一本好书,甚至是一本受欢迎的书;他们可能已经付钱把它放在那里了。 110 | 111 | 批判性思维本身就是一门完整的学科,我们鼓励你们阅读并研究它。在此期间,我们先来问几个问题并思考一下。 112 | 113 | - ### _问“五个为什么”_ 114 | 115 | 一个最喜欢的咨询技巧:问“为什么?”?“至少五次。也就是说,给出一个答案,问“为什么?“说到这里。像一个脾气暴躁的四岁孩子一样经常重复,但更礼貌一些。你也许可以通过这种方式接近根本原因。 116 | 117 | - ### _谁能从中受益_ 118 | 119 | 这听起来可能有些愤世嫉俗,但跟着钱走是一条非常有帮助的分析之路。对其他人或其他组织的好处可能与您自己的利益一致,也可能与您自己的利益不一致。 120 | 121 | - ### _上下文是什么_ 122 | 123 | 每件事都是在自己的背景下发生的,这就是为什么“一刀切”的解决方案往往不会。考虑一篇宣扬“最佳实践”的文章或一本书。要考虑的问题是“最适合谁”?先决条件是什么,短期和长期后果是什么? 124 | 125 | - ### _何时或在哪里起作用_ 126 | 127 | 在什么情况下?太迟了吗?太早了?不要停止一阶思考(接下来会发生什么),而是使用二阶思考:之后会发生什么? 128 | 129 | - ### _为什么这是个问题_ 130 | 131 | 有没有一个潜在的模式?基础模型是如何工作的? 132 | 133 | --- 134 | ## 技巧 10 批判性地分析你的所听所见 135 | --- 136 | 137 | 不幸的是,现在已经很少有简单的答案了。但是,有了你广泛的知识组合,通过对你将要阅读的大量技术出版物进行批判性分析,你就能理解复杂的答案。 138 | 139 | ## 相关内容包括 140 | 141 | - 话题 22 [_工程日刊_](../Chapter3/工程日记.md) 142 | - 话题 1 [_这是你的人生_](./这是你的人生.md) 143 | 144 | ## 挑战 145 | 146 | - 本周开始学习一门新的语言。总是用同样的老语言编程?试试Clojure,Elixir,Elm,F#,Go,Haskell,Python,R,ReasonML,Ruby,Rust,Scala,Swift,TypeScript,或者任何你喜欢的东西。 147 | 148 | - 开始读一本新书(但先读完这本!)。如果您正在做非常详细的实现和编码,请阅读一本关于设计和架构的书。如果你在做高级设计,读一本关于编码技术的书。 149 | 150 | - 走出去和那些不参与你当前项目的人,或者那些不在同一家公司工作的人谈谈技术。在你公司的自助餐厅建立关系网,或者在当地的聚会上寻找其他的爱好者。 151 | -------------------------------------------------------------------------------- /Chapter1/务实主义哲学.md: -------------------------------------------------------------------------------- 1 | # 务实主义哲学 2 | 3 | 这本书是关于你的 4 | 5 | 我们可以花很大的篇幅详细介绍我们的经验、见解和观察,但那都是关于我们的,说实话,我们相当无聊。相反,我们只从过往几十年的经验中挑选了一些亮点:我们希望对你和你的职业生涯有帮助的一些最好的、最相关的课程。这要从我们的理念,我们对职业的态度开启和你的职业。 6 | 7 | 别搞错,这是你的职业,更重要的是, 话题 1,[_这是你的人生_](./这是你的人生.md)。你拥有它。你来这里是因为你知道你可以成为一个更好的开发人员,也可以帮助其他人变得更好, 你可以成为我们口中所说的务实的程序员。 8 | 9 | 务实的程序员有什么区别? 我们觉得这是一种态度,一种风格,一种解决问题及其解决方案的哲学。 他们思考的问题超出了眼前的问题,总是试图将其放在更大的范围内,总是试图意识到更大的局面。 毕竟,如果没有这种更大的环境,您怎么能务实呢? 您如何做出明智的妥协和明智的决定? 10 | 11 | 另外一个让务实的程序员能够成功重要的一点就是他们对自己所做的一切都负责,这个我们会在话题 2,[_猫吃了我的源代码_](./猫吃了我的源代码.md) 里面讨论。有责任心,务实的程序员不会袖手旁观,不会看着他们的项目因疏忽而崩溃。 在话题 3 [_软件熵_](./软件熵.md) 中,我们告诉您如何保持项目一尘不染,始终是崭新状态。 12 | 13 | 大多数人发现改变难以接受,有时是出于充分的理由,有时是由于过去惯常的惯性导致。在话题 4 [_石汤和煮青蛙_](./石汤和煮青蛙.md) 里面,我们研究一种促进变化的策略,并(为了平衡)提出了一个两栖动物的警示性故事,该故事忽略了逐渐变化的危险。 14 | 15 | 了解您工作环境的好处之一是: 您可以更轻松地知道软件的质量。 有时候,唯一的选择是接近完美,但通常需要权衡取舍。 我们将在话题 5 [_足够好的软件_](./足够好的软件.md) 中对此进行探讨。 16 | 17 | 当然,您需要拥有广泛的基础知识和经验才能实现所有这些目标。 学习是一个持续不断的过程。 在话题 6 [_你的知识组合_](./你的知识组合.md) 中,我们讨论了一些保持增长势头的策略。 18 | 19 | 最后,没有一个人是在真空环境下工作,我们都花费很多时间跟别人交流,话题 7,[_沟通_](./沟通.md),罗列出一些更好的方法 20 | 21 | 实用编程源于实用思维哲学。 本章为这种哲学奠定了基础。 22 | -------------------------------------------------------------------------------- /Chapter1/沟通.md: -------------------------------------------------------------------------------- 1 | # 沟通 2 | 3 | 4 | > _我相信,被人审视总比被人忽视好。_ 5 | > 6 | > _--- 梅韦斯特《九十岁的美女》1934年_ 7 | 8 | 也许我们可以向韦斯特女士学习。你拥有什么不重要,重要的是你如何包装它。除非你能与他人沟通,否则拥有最好的想法、最好的代码或最务实的想法最终都是徒劳的。一个好的想法,没有有效沟通的话,那它就像个孤儿。 9 | 10 | 作为开发人员,我们必须在多个层面上进行沟通。我们花几个小时开会,聆听和讨论。最终我们与用户合作,试图了解他们的需求。我们编写代码,它将我们的意图传达给一台机器,并将我们的想法记录下来,供未来的开发人员使用。我们撰写提案和备忘录,请求和证明资源的合理性,报告我们的现状,并提出新的方法。我们每天都在团队内工作,宣传我们的想法,修改现有的实践,并提出新的建议。我们一天的大部分时间都花在沟通上,所以我们需要把它做好。 11 | 12 | 把英语(或者你的母语)当作另一种编程语言。像编写代码一样编写自然语言:遵守 DRY 的原则,ETC,自动化等等。(我们将在下一章讨论 DRY 和 ETC 的设计原则。) 13 | 14 | --- 15 | ## 技巧 11 英语只是另一种编程语言 16 | --- 17 | 18 | 我们列出了一些我们认为有用的额外想法。 19 | 20 | ### 了解你的听众 21 | 22 | 你只有在传达信息的时候才在交流。要做到这一点,你需要了解受众的需求、兴趣和能力。我们都参加过这样的会议:一个开发极客用一段关于某些神秘技术优点的长篇独白,让营销副总裁眼前一亮。这不是交流:只是聊天,而且很烦人。 23 | 24 | 假设你想建议一个基于web的系统允许你的终端用户提交 bug 报告。你可以用很多不同的方式来呈现这个系统,这取决于你的听众。终端用户会很感激,因为他们全天 24 小时都可以提交错误报告而不用在电话里等待。你的市场部门将能够利用这个点来促进销售。支持部门的经理们会有两个理由感到高兴:这将会需要更少的员工,问题报告将自动化。最后,开发人员可能喜欢体验基于web的客户端-服务器技术和新数据库引擎。通过对每个小组进行适当的宣传,你会让他们都对你的项目感到兴奋。 25 | 26 | ### 知道你想说什么 27 | 28 | 在商业里使用的正式沟通方式中,最困难的部分可能就是弄清楚你到底想说什么。小说作者通常在开始之前就把书详细地排版出来,但写技术文档的人通常乐于坐在键盘前输入: 29 | 30 | 1 介绍 31 | 32 | 然后开始输入他们脑子里的东西。 33 | 34 | 计划你想说的话。写一个提纲。然后扪心自问,“这是否传达了我想表达给我的观众的方式,会有效果吗?“精炼它直到它变好。 35 | 这种方法不仅适用于编写文档。当你面对一个重要的会议或者一个重要客户的电话时,记下你想交流的想法,并计划一些策略来让他们明白。 36 | 37 | 现在你知道了听众的诉求,是时候计划该如何做了。 38 | 39 | ### 选择你的时刻 40 | 41 | 现在是星期五下午六点,审计人员已经来了一周了。你老板最小的儿子在医院,外面下着倾盆大雨,上下班回家肯定是个噩梦。这可能不是让她为你的笔记本电脑升级内存的好时机。 42 | 43 | 作为理解你听众需要听到什么的一部分,你需要弄清楚他们的优先级是什么。如果一个经理的老板给了她一段艰难的时间,因为有些源代码丢失了,那么你就会有一个更容易接受的听众来倾听你对源代码仓库的想法。使你所说的在时间上和内容上都相关。有时只需要一个简单的问题“现在是不是讨论…?” 44 | 45 | ### 选择一种方式 46 | 47 | 调整你的演讲风格以适合你的听众。有些人想要一份正式的“事实”简报。其他人喜欢在谈正事之前进行长时间、广泛的交谈。他们在这方面的技术水平和经验如何?他们是专家吗?新手?医生,他们需要详细的手册还是仅仅快速的浏览?如果有疑问,请询问。 48 | 49 | 不过,请记住,你是沟通事务的一方。如果有人说他们需要一段文字来描述某件事,而你在不到几页的篇幅里看不到任何方法,告诉他们。记住,这种反馈也是一种交流。 50 | 51 | ### 使它看起来不错 52 | 53 | 你的想法很重要。这些想法应该有一个好看的工具来传达给你的观众。 54 | 55 | 太多的开发人员(及其管理人员)在生成书面文档时只关注内容。我们认为这是个错误。任何一位厨师(或食物网络的观察者)都会告诉你,你可以在厨房里苦干几个小时,结果却因为表现不佳而毁了你的努力。 56 | 57 | 今天没有任何理由生产外观差的印刷文件。现代软件可以产生惊人的输出,无论你是使用 Markdown 还是使用文字处理器。你只需要学习一些基本的命令。如果您使用的是文字处理器,请使用其样式表以保持一致性。(您的公司可能已经定义了可以使用的样式表。)了解如何设置页眉和页脚。查看包中包含的示例文档,了解有关样式和布局的想法。先自动检查拼写,然后用手检查。毕尽,他们拼写的错五检察程序没法发线(原文是: After awl, their are spelling miss steaks that the chequer can knot ketch.) 58 | 59 | ### 吸引你的听众 60 | 61 | 我们经常发现,我们制作的文档最终不如我们制作它们的过程重要。如果可能,请让读者参与文档的早期草稿。得到他们的反馈,并选择他们的脑洞。您将建立一个良好的工作关系,并可能在这个过程中产生更好的文档。 62 | 63 | ### 成为一名聆听者 64 | 65 | 如果你想让别人听你的话,你必须使用一种技巧:听他们讲。即使这是一个你拥有所有信息的情况,即使这是一个正式会议,你站在20个西装革履的人前。如果你不听他们的话,他们也不会听你的。 66 | 67 | 鼓励人们通过提问来交谈,或者让他们总结你告诉他们的内容。把会议变成一个对话,你就能更有效地表达你的观点。谁知道呢,你甚至可能学到一些东西。 68 | 69 | ### 回应他们 70 | 71 | 如果你问某人一个问题,假如他们不回答,你会觉得他们不礼貌。但是,当人们给你发邮件或备忘录询问信息或要求采取行动时,你有多少次没有回复他们?在日常生活的匆忙中,很容易忘记。总是回复邮件和语音邮件,即使回复只是简单的“我稍后再给你回复。”“让人们了解情况会让他们更宽容偶尔的失误,让他们觉得你没有忘记他们。 72 | 73 | --- 74 | ## 技巧 12 说什么和怎么说 75 | --- 76 | 77 | 除非你在真空中工作,否则你需要能够沟通。沟通越有效,你就越有影响力。 78 | 79 | --- 80 | ### 在线沟通 81 | 82 | 我们所说的关于书面交流的一切都同样适用于电子邮件、社交媒体帖子、博客等。尤其是电子邮件,它已经发展到成为公司通信的一个主要部分;它被用来讨论合同,解决纠纷,并在法庭上作为证据。但出于某种原因,那些从不发送破旧的纸质文件的人,很乐意在全世界范围内发送那些看起来恶心、语无伦次的电子邮件。 83 | 84 | 我们的提示很简单: 85 | 86 | - 点击发送前校对 87 | 88 | - 检查拼写并查找任何自动检察的错误 89 | 90 | - 保持格式简单。并不是所有的电子邮件客户端都能像现代浏览器一样呈现,所以你漂亮的布局可能会崩溃。其他在线媒体中的许多评论或回复不允许使用 HTML 标记。 91 | 92 | - 尽量少引用。没有人喜欢收到带有“我同意”字样的自己的100行电子邮件。 93 | 94 | - 如果你引用的是别人的邮件,一定要把它归为属性,并内联引用(而不是作为附件)。在社交媒体平台上引用时也是如此。 95 | 96 | - 不要玩火或是表现的很坏除非你想让它回来缠着你。如果你不想当着某人的面说,那就不要在线上说 97 | 98 | - 发送前请检查收件人列表。在没有意识到老板在抄送名单上的情况下,通过部门邮件批评老板已经成为一种陈词滥调。更好的是,不要在电子邮件上批评老板。 99 | 100 | 正如无数大公司和政界人士所发现的,电子邮件和社交媒体帖子永远存在。试着像对待任何书面备忘录或报告一样关注和关心电子邮件。 101 | 102 | --- 103 | 104 | ## 文件资料 105 | 106 | 最后,还有通过文档进行交流的问题。 通常,开发人员不会过多考虑文档。 充其量是不幸的必需品。 在最坏的情况下,它被视为低优先级任务,希望管理层在项目结束时将其忽略。 107 | 108 | 务实的程序员将文档视为整个开发过程的组成部分。 通过避免重复工作或浪费时间,并通过紧接文档本身(在代码本身中),可以使编写文档更加容易。 实际上,我们希望将所有务实的原则应用于文档和代码 109 | 110 | --- 111 | ## 技巧 13 内置文档,不要闩死 112 | --- 113 | 114 | 从源代码中的注释中生成美观的文档很容易,我们建议将注释添加到模块和导出的函数中,以使其他开发人员在使用它们时有所助益。 115 | 116 | 但是,这并不意味着我们同意那些说每个函数,数据结构,类型声明等都需要注释的人。 这种机械的编写注释实际上使维护代码更加困难:现在,进行更改时有两件事需要更新。 因此,将您的非 api 评论限制为讨论执行某项操作的原因,目的和目标。 该代码已经显示了它是如何完成的,因此对此进行注释是多余的,并且违反了 DRY 原则。 117 | 118 | 注释源代码为您提供了一个完美的机会来记录项目中那些在其他地方无法记录的难以捉摸的部分:工程权衡、为什么做出决策、放弃了哪些其他替代方案等等。 119 | 120 | ## 总结 121 | 122 | - 知道你想说什么 123 | - 了解你的听众 124 | - 选择你的时机 125 | - 选择一种方式 126 | - 让它看起来不错 127 | - 吸引你的听众 128 | - 成为一名聆听者 129 | - 回应他们 130 | - 代码和文档保持在一起 131 | 132 | ## 相关内容包括 133 | 134 | - 话题 18 [_强大的编辑_](../Chapter3/强大的编辑.md) 135 | - 话题 15 [_评估_](../Chapter2/评估.md) 136 | - 话题 45 [_需求坑_](../Chapter8/需求坑.md) 137 | - 话题 48 [_务实的团队_](../Chapter9/务实的团队.md) 138 | 139 | ## 挑战 140 | 141 | - 有好几本好书介绍了开发团队之间的交流,其中包括《神话般的一个月:软件工程随笔》和《人件:生产性项目和团队》。 重点尝试在接下来的18个月中阅读这些内容。 此外,《恐龙大脑:与所有不可能的人打交道》讨论了我们带给工作环境的情感包。 142 | 143 | - 下次您必须进行演示或写备忘录以提倡某个职位时,请在开始之前尝试阅读本节中的建议。 明确识别受众以及您需要传达的内容。 如果合适的话,之后与您的听众交谈,看看您对他们的需求的评估有多准确。 144 | -------------------------------------------------------------------------------- /Chapter1/猫吃了我的源代码.md: -------------------------------------------------------------------------------- 1 | # 猫吃了我的源代码 2 | 3 | > 所有弱点中最大的弱点就是害怕出现弱点。 4 | > 5 | > J.B. Bossuet, Politics from Holy Writ, 1709 6 | 7 | 务实哲学的基石之一是在自己的职业发展,学习和教育,项目以及日常工作方面对自己以及自己的行为负责。 务实的程序员负责自己的职业,不惧怕无知或错误。 当然,这并不是编程中最令人愉快的方面,但是即使在最好的项目中,它也会发生。 尽管进行了全面的测试,良好的文档记录和可靠的自动化功能,但还是出了问题。 发布延迟。 出现了不可预见的技术问题。 8 | 9 | 当发生这些事的时候,我们尽自己最大的专业解决这些问题。这意味着诚实并且直接。我们可以以自己的能力为骄傲,但是对待我们的缺点,无知和错误,我们必须保持诚实。 10 | 11 | ## 团队信任 12 | 13 | 最重要的是,您的团队需要能够信任并依赖您-并且您也需要放心地依赖他们每个人。 根据研究文献,对团队的信任对于创造力和协作至关重要。在基于信任的健康环境中,您可以放心地说出自己的想法,表达您的想法,并依靠可以反过来依赖您的团队成员。 没有信任,那好吧…… 14 | 15 | 想象一下一支高科技的隐身忍者团队正在渗透大恶魔的巢穴。 经过数月的计划和精细的执行,您已经在网站上实现了它。 现在轮到您设置激光引导网格了:“对不起,伙计们,我没有激光。 我把它落在家中,我家的猫在玩那个红点。 16 | 这种违反信任的行为可能很难修复。 17 | 18 | ## 负起责任 19 | 20 | 责任是您肯定同意的。 您承诺要确保正确地完成某件事,但不一定对它的每个方面都有直接的控制权。 除了尽自己最大的努力,您还必须分析情况以发现无法控制的风险。 您有权对一些不可能的情形,风险太大或道德影响过于粗略的情况不承担责任。 您必须根据自己的价值观和判断来拨打电话。 21 | 22 | 当您接受一个结果带来的责任时,您就应该为此承担责任。 当您犯了一个错误(我们都一样)或判断有误时,请诚实地接受它并尝试提供选择。 23 | 24 | 不要甩锅,也不要编造借口。 不要将所有问题归咎于供应商,编程语言,管理人员或您的同事。 所有这些都可能发挥作用,但是您需要提供解决方案,而不是借口。 25 | 26 | 如果存在供应商无法为您解决的风险,则您应该制定应急计划。 如果您的大容量存储崩溃了—随身带走所有源代码而且您没有备份,这是您的错。 告诉老板 “猫吃了我的源代码” 不会有帮助。 27 | 28 | ## 技巧 4 提供选择 不要找蹩脚的接口 29 | 30 | 在你接近某个人并且告诉他们为什么某些事无法完成,已经晚了,或者出问题了,请停下来倾听自己的声音。 跟你显示器上的橡皮鸭或猫说话。 您的借口听起来是合理还是愚蠢?当你说这些的时候你的老板感觉如何? 31 | 32 | 在你的脑海中构建这样的对话。另一个人可能会说什么?他们会不会问,“你有没有试过这个......” 或者 “你考虑那个了吗?” 你怎么回答呢?在你去跟他们说这些坏消息之前,想想你是不是可以做点别的尝试 ?有时候,你知道他们要说什么,所以为他们省去了麻烦。 33 | 34 | 提供选择而不是借口。不要说这不可能完成;说明一下可以采取什么措施挽救局势。 是否必须扔掉代码? 教育他们重构的价值(参考 话题 40 [_重构_](../Chapter7/重构.md)),您是否需要花时间进行原型设计以确定最佳的推进方式(参考话题 13 [_原型和便笺_](../Chapter2/原型和便笺.md))?你需要引入更好的测试(参考话题 41 [_代码测试_](../Chapter7/代码测试.md),无情又连续的测试)或者自动化以防止再次发生? 35 | 36 | 或许你需要额外的资源去完成这项任务。或者你可能需要花更多的时间在用户身上?或许就是你:您是否需要更深入地学习一些技术?书或是课程有帮助?别害怕提问或者承认你需要帮助。 37 | 38 | 在大声说出蹩脚的借口之前,请先将它们冲掉。 如果你必须要说的话,请先告诉您的猫。 毕竟,如果小提德尔(Tiddles)要承担责任...... 39 | 40 | ## 相关部分包括 41 | 42 | - 话题 48 [_务实的团队_](../Chapter9/务实的团队.md) 43 | 44 | ## 挑战 45 | 46 | - 你是怎么回应当有一些人 - 比如银行柜员,或者汽车修理工或业务员 - 用蹩脚的借口来找你?您如何看待他们以及他们的公司。 47 | 48 | - 当你发现自己在说:“我不知道”,确定这句话后面会跟一句“但是我会想办法解决”。这是一个很好的方式去承认自己不知道什么,但是却像专业人士一样愿意负起责任。 49 | -------------------------------------------------------------------------------- /Chapter1/石汤和煮青蛙.md: -------------------------------------------------------------------------------- 1 | # 石汤和煮青蛙 2 | 3 | _从战争中返回家园的三名士兵饿了。当他们看到前方的村庄时,他们的精神振作起来-他们确定村民会给他们饭吃。 但是当他们到达那里时,他们发现大门上锁,窗户紧闭。经过多年的战争,村民们缺乏食物,把他们拥有的一切都贮藏起来。_ 4 | 5 | _士兵们并没有气馁,他们烧了一锅水,并小心地将三块石头放进去。 惊奇的村民出来观看。_ 6 | 7 | _“这个叫石汤”,士兵这样解释道。“你就只放这些东西?” 村民问道。“对啊 - 虽然有人说加点胡萝卜可能味道更好...” 一个村民跑开了,立刻从他的屋子里拿出一篮子胡萝卜。_ 8 | 9 | _又过了一会儿,那个村民又问“这样就好了?”_ 10 | 11 | _士兵们说:“好吧,有几个土豆的话汤就有了灵魂。” 另一个村民又跑开了。_ 12 | 13 | 在接下来的一个小时中,士兵们列出了更多可以让汤变得更美味的食材:牛肉,韭菜,盐和香料。 每次都会有不一样的村民跑开然后带来他们个人贮藏的东西。 14 | 15 | 最终他们做了一大锅热气腾腾的汤。士兵们把石头取出来,然后与整个村庄的村民坐下来,享受他们几个月来任何人都没有吃到过的美味。 16 | 17 | 石汤的故事有一些道德问题。村民们被士兵利用其好奇心骗走了食物。但更重要的是,士兵们起到了催化剂的作用,将村民聚集在一起,使他们能够一起做到自己之前无法完成的事情-这就是协同的结果。 最终每个人都赢了。 18 | 19 | 时不时地,您可能想效仿士兵。 20 | 21 | 您可能处在一种确切知道需要做什么以及如何做的情况下。 整个系统只是出现在您的眼前-您知道这是对的。 但是,如果允许您解决整个问题,就会遇到延误以及一脸茫然。 人们建立委员会,预算也需要批准,而且事情将会变得复杂。 每个人都将保护自己的资源。 有时这称为“启动疲劳”。 22 | 23 | 是时候把石头拿出来了。制定合理的需求。 认真开发。 一旦完成,向人们展示并且让他们惊叹不已。 然后说“当然,如果我们添加……会更好。”假装这并不重要。 坐下来等待他们开始要求您添加你最初想要的功能。 人们发现更容易加入持续的成功。 向他们展示未来,您将使他们团结起来。 24 | 25 | --- 26 | ## 技巧 6 成为改变的催化剂 27 | --- 28 | 29 | ## 村民的角度 30 | 31 | 另一方面,石汤的故事也是关于温柔的陷阱和渐进的欺骗。 这是因为过分专注。 村民们思考着石头,却忘记了世界其他地方。 摔倒这样的事情在我们身上每一天都在发生。 32 | 33 | 我们都看到了现象。项目进展缓慢,势不可挡地发展到不可控制的地步。 多数软件灾难开始时很少引起注意,并且大多数项目都会出现超过一次。 系统会逐个功能地偏离其规范,而一个补丁又一个补丁地添加到一段代码中,直到没有剩余的原始代码为止。 通常是小事情的积累破坏了士气和团队。 34 | 35 | --- 36 | ## 技巧 7 记住大局 37 | --- 38 | 39 | 我们从来没有尝试过这个-诚实。 但是“他们”说,如果您将一只青蛙扔到沸水中,它会立马跳出来。 但是,如果将青蛙放在一锅冷水中,然后慢慢加热,青蛙不会注意到温度在缓慢升高,并且会一直待到煮熟。 40 | 41 | 注意青蛙的问题跟我们之前在话题 3 [_软件熵_](./软件熵.md) 里面提到的破窗问题是不一样的。在破窗理论中,人们失去了对抗熵的意愿,因为他们认为没有人在乎。而青蛙只是没有意识到变化。 42 | 43 | 不要做寓言故事里的青蛙。关注大局。不断地检查周围的事情,而不仅仅是您个人正在做的事情。 44 | 45 | ## 相关内容包括 46 | 47 | - 话题 38 [_巧合编程_](../Chapter7/巧合编程.md) 48 | - 话题 1 [这是你的人生](./这是你的人生.md) 49 | 50 | ## 挑战 51 | 52 | - 约翰·拉科斯(John Lakos)在审查第一版的草稿时提出了以下问题:士兵逐渐欺骗村民,但是他们催化的变化对彼此都有益。然而,通过逐渐欺骗青蛙,您对青蛙造成了伤害。那么尝试促进改变时,您可以确定是在煮石汤还是青蛙汤?决定是主观的还是客观的? 53 | 54 | - 快速的,不用看,您头顶的天花板上有几盏灯? 房间里有几个出口? 多少人? 是否有上下文无关的内容? 这是一种态势感知练习,是从侦察兵到海军海豹突击队队员都练习的一种技巧。 养成真正观察并注意周围环境的习惯。 然后对您的项目执行相同的操作。 55 | -------------------------------------------------------------------------------- /Chapter1/足够好的软件.md: -------------------------------------------------------------------------------- 1 | # 足够好的软件 2 | 3 | 4 | > _追求更好,我们将不断前进。_ 5 | > 6 | > _--- 莎士比亚,李尔王 1.4_ 7 | 8 | 有一个古老的笑话是关于美国公司的,该公司向日本制造商订购了 100000 个集成电路板。 说明里面有一部分是缺陷率:每 10000 个芯片中就有一个。几周后接到订单:一个装有数千个 IC 的大盒子,一个仅有十个 IC 的小盒子。 小盒子上贴着一个标签,上面写着:“这些是有问题的。” 9 | 10 | 如果我们真的对质量有这种控制的话。 但是现实世界不会让我们生产出真正完美的东西,特别是没有错误的软件。 时间,技术和气氛都对我们不利。 11 | 12 | 但是,这不必令人沮丧。 正如 Ed Yourdon 在 IEEE Software 上的一篇文章中所描述的那样,什么时候足够好的软件是最好的,您可以训练自己编写足够好的软件-对用户友好,对未来的维护者和您自己的内心都足够好。 您会发现自己的工作效率更高,用户也更快乐。 而且您可能会发现您的程序实际上对于较短的孵化时间来说更好。 13 | 14 | 我们继续之前,我们需要确定我们接下来要说的话。 “足够好”一词并不表示草率的代码或随意产出的代码。 所有系统都必须满足用户要求才能成功,并满足基本性能,隐私和安全性标准。 我们只是在提倡让用户在适当的时候有机会参与决定您开发的产品足以满足其需求的过程。 15 | 16 | ## 让用户参与权衡 17 | 18 | 通常您是在为他人编写软件。 通常您也会记得从他们那里获得需求。 但是,您多久问他们一次,他们希望他们的软件有多好? 有时别无选择。 如果您正在研究起搏器,航天飞机或将被广泛传播的低层图书馆,那么需求将会更加严格,您的选择也会受到限制。 19 | 20 | 但是,如果您在开发全新的产品,则会遇到不同的限制。 营销人员将遵守诺言,最终的终端用户可能会根据发布时间表制定计划,并且您的公司肯定会受到现金流的限制。 仅仅为了向程序中添加新功能,或者仅仅再完善一次代码而忽略这些用户的要求,会显得很不专业。 我们并不是在主张恐慌:承诺不可能的时间比例并削减基本的工程技术来满足最后期限同样是不专业的。 21 | 22 | 您开发的系统范围和质量应作为系统要求的一部分进行讨论。 23 | 24 | --- 25 | ## 技巧 8 使质量成为需求问题 26 | --- 27 | 28 | 通常,您会遇到需要权衡的情况。 出乎意料的是,许多用户宁愿使用今天有些粗糙的软件,也不愿等待一年才能获得闪亮的“吹牛”的版本(实际上,从现在起一年后他们将需要的内容可能完全不同)。 许多预算紧张的IT部门都会同意。 如今,出色的软件通常胜过明天的完美软件。 如果您尽早给用户展示一些东西,他们的反馈通常会带您找到更好的最终解决方案(请参阅话题 12,[_示踪子弹_](../Chapter2/示踪子弹.md))。 29 | 30 | ## 知道何时停止 31 | 32 | 在某些方面,编程就像绘画。 您从一块空白画布和一些基本原材料开始。 您可以结合科学,艺术和手工艺来确定如何处理它们。 您勾勒出整体形状,绘制基础的环境,然后填入细节。 您会退后一步不断地用批判的眼光,查看您所做的事情。 有时您会丢掉一块画布,然后重新开始。 33 | 34 | 但是艺术家会告诉您,如果您不知道什么时候停止,那么所有辛苦的工作都将荡然无存。如果您逐层添加,细节覆盖细节,那么绘画也将丢失在绘画中。 35 | 不要过分夸大和过度精炼来破坏一个完美的程序。 继续前进,让您的代码保持一段时间。 这可能不太完美。 不用担心:它永远不可能完美。(在第 7 章 [_当您编程时_](../Chapter7/当您编程时.md) 里面,我们将讨论在不完善的世界中开发代码的哲学。) 36 | 37 | ## 相关内容包括 38 | - 话题 46 [_解决不可能的难题_](../Chapter8/解决不可能的难题.md) 39 | - 话题 45 [_需求坑_](../Chapter8/需求坑.md) 40 | 41 | ## 挑战 42 | - 查看您经常使用的软件工具和操作系统。 您是否可以找到任何证据证明这些组织和/或开发人员可以轻松地列举他们所知并不完美的软件? 作为用户,您是否愿意 43 | 44 | 1. 等待他们找出所有错误, 45 | 2. 具有复杂的软件并接受某些错误,或者 46 | 3. 选择缺陷少的简单软件? 47 | 48 | - 考虑模块化对软件交付的影响。 与设计非常松散且耦合的模块或微服务的系统相比,获得紧耦合的单个软件模块所需质量是否需要花费更多或更少的时间? 每种方法的优点和缺点是什么? 49 | 50 | - 您能想到功能膨胀的流行软件吗? 也就是说,软件所包含的功能远远超过您每次使用的功能,每种功能都带来了更多bug和安全漏洞,并使您难以使用和使用的功能更加难以找到和管理。 您自己是否有陷入陷阱的危险? 51 | -------------------------------------------------------------------------------- /Chapter1/软件熵.md: -------------------------------------------------------------------------------- 1 | # 软件熵 2 | 3 | 尽管软件开发几乎不受所有物理定律的束缚,但是熵不可阻挡的增长却给我们带来了沉重打击。 熵是物理学中的术语,指的是系统中的“无序”程度。 不幸的是,热力学定律保证了宇宙中的熵趋于最大。 当软件混乱增加时,我们称之为“软件腐烂”。 有些人可能用更乐观的术语称其为“技术债务”,这暗示着有一天他们会偿还。 但他们不会。 4 | 5 | 不过,不管叫什么,技术债务和腐烂都可以传播到无法控制的地步。 6 | 7 | 有许多因素可能导致软件腐烂。 一个项目中最重要的似乎是心理学或(团队)文化。 即使您一个人就是一个团队,您的项目心态也可能是一件非常微妙的事情。 尽管制定了最好的计划,拥有最好的人员,但是项目在其生命周期中仍然会遭受破坏和衰败。 然而,其他项目尽管也会遇到了巨大的困难和持续不断的挫折,但仍成功地克服了自然界的无序倾向,并取得了不错的成绩。 8 | 9 | 有什么不同吗? 10 | 11 | 在城市里面,有些建筑漂亮干净,而另一些则腐烂。 为什么? 研究犯罪和城市腐烂领域的人员发现了一种引人入胜的触发机制,该机制可以迅速将干净,完整,有人居住的建筑物变成被砸坏并废弃的废弃建筑。 12 | 一个破了的窗户。 13 | 14 | 一个破碎的窗户,在长时间内都没有修复,给建筑物的居民灌输一种被遗弃的感觉,一种被认为是无关紧要的力量的感觉。 所以另一个窗口坏了。 人们开始乱扔垃圾。 随之而来涂鸦也出现了。开始严重的结构损坏。 在相对较短的时间内,该建筑物受到损坏,超出了业主对其进行修复的期望,并且这种废弃感成为了现实。 15 | 16 | 为什么会这样?心理学家进行的研究表明,绝望可能会传染。 想想在近一个季度的流感病毒。无视明显破裂的情况,强化了这样的观念:也许什么也无法解决,也没人在乎,所有人注定要失败,一切终将毁灭;所有可能在团队成员中传播的负面思想,造成了这样的恶性循环。 17 | 18 | --- 19 | ## 技巧 5 不要生活在窗户破了的地方 20 | --- 21 | 22 | 不要让“破窗”(错误的设计,错误的决定或不良的代码)得不到修复。发现每个问题后立即修复。如果没有足够的时间正确地修复它,则将其安装起来。 也许您可以注释掉有问题的代码,或显示“未实现”消息,或用虚拟数据替代。 采取一些措施来防止进一步的损害,并表明您处于领先地位。 23 | 24 | 我们发现,一旦窗户破损,那么干净,功能正常的系统就会迅速出现问题。 还有其他因素可能导致软件腐烂,我们将在其他地方涉及其中的一些因素,但是忽视可比其他任何因素都更快地加快腐烂的速度。 25 | 26 | 您可能会认为没有人有时间清理项目里所有的碎玻璃。 如果您继续这样思考,则最好计划购买垃圾箱或搬到另一个社区。不要让熵赢。 27 | 28 | ## 首先,不要伤害 29 | 30 | 许多年前,安迪(Andy)的一个熟人非常富裕。 他的房子完美无暇,美丽,到处都是无价的古董,艺术品等。 一天,挂在客厅壁炉旁的挂毯着火了。消防部门赶紧救了他和他的房子。 但是,在他们将又大又脏的水管拖进房屋之前,他们先停下来,在熊熊大火燃烧的情况下,在前门和火源之间铺上了垫子。 31 | 32 | 他们不想弄乱地毯。 33 | 34 | 现在听起来很极端。当然,消防部门的当务之急是扑灭大火,这肯定会附带损害。 但是他们显然已经评估了情况,对自己有能力管理火灾充满信心,并非常小心不对财产造成不必要的损害。 这就是软件必须采用的方式:不要仅仅因为存在某种危机而造成附带损害。 一扇破窗户也是。 35 | 36 | 一个破烂的窗口(一个设计不良的代码片段,一个团队在项目期间必须接受的糟糕的管理决策)才是一切下滑的开始。如果您发现自己的项目有很多破损的窗口,那么很容易陷入“以下所有代码都是胡扯,我照着做”的想法。到目前为止,项目是否完善都没关系。 在导致“破窗理论”的原始实验中,一辆废弃的汽车原封不动地在原地一周。 但是只要一个窗户被打破,汽车就会在几小时内被剥离并倒置。 37 | 38 | 同样,如果您发现自己的团队和项目中的代码非常优雅,编写简洁,设计良好且美观大方,那么您可能需要格外小心,不要像消防员那样将其弄乱。 即使发生了大火(最后期限,发布日期,商业展览演示等),您也不想成为第一个弄乱并造成额外损失的人。 39 | 40 | 就告诉自己,“不要有破损的窗户” 41 | 42 | ## 相关内容包括 43 | 44 | - 话题 10 [_正交性_](../Chapter2/正交性.md) 45 | - 话题 40 [_重构_](../Chapter7/重构.md) 46 | - 话题 44 [_命名_](../Chapter7/命名.md) 47 | 48 | ## 挑战 49 | 50 | - 通过调查项目邻域来帮助增强团队实力。选择2-3个破了的窗户和你的同事就项目中有哪些问题及如何解决展开讨论 51 | 52 | - 你能说出来一个窗户是什么时候破的吗?你对此有何反应?如果这是某个人的决定或是管理层的命令导致的结果,你将如何处理? 53 | -------------------------------------------------------------------------------- /Chapter1/这是你的人生.md: -------------------------------------------------------------------------------- 1 | # 这是你的人生 2 | 3 | > _我在这个世界上不是为了实现您的期望,并且您也不是为了达到我的期望。_ 4 | > 5 | > _-- 李小龙_ 6 | 7 | 8 | 这是你的人生,你拥有它,你驱动它,你创造着它。 9 | 10 | 与我们交谈的许多开发人员都感到沮丧。他们的担忧是多种多样的。有些人觉得他们的工作停滞不前了。其他人则认为技术已经把他们甩开了。人们感到自己受到赞赏,报酬不足或团队有毒。 也许他们想搬到亚洲或欧洲,或者在家工作。 11 | 12 | 不过我们给出来的答案总是一样的: 13 | 14 | “为什么你不能改变它呢?” 15 | 16 | 软件开发必须出现在您可以控制的所有职业清单的顶部。我们需要技能,我们的知识跨越地域界限,我们可以远程工作。我们的薪水还不错。我们真的可以做任何我们想做的事情。 17 | 18 | 但是,因为某些原因, 开发者看上去好像比较抵抗改变。他们渴望并且希望情况会好起来。他们看着自己的技能过时,并抱怨自己的公司没有培训他们。 他们看着公交车上异国情调的广告,然后踏入寒冷的雨水,步履沉重地去上班。 19 | 20 | 所以这是书里最重要的提示。 21 | 22 | --- 23 | ## 技巧 3 你有代理 24 | --- 25 | 26 | 您的工作环境糟透了吗? 你的工作很无聊吗? 尝试修复它。 但是,请不要永远尝试。 正如马丁·福勒(Martin Fowler)所说,“您可以改变你的组织或改变你的组织。” 27 | 28 | 如果技术似乎正在让您无法追赶,请花时间(您自己的时间)研究看起来很有趣的新事物。 您是在投资自己,所以在下班时这样做才是合理的。 29 | 30 | 想要远程办公?你问了吗?如果他们回答说不可以,那么找一个说可以远程办公的公司。 31 | 32 | 这个行业为您提供了绝佳的机会。 保持积极主动,并接受他们。 33 | 34 | ## 相关相关章节包括 35 | 36 | - 话题 6 [_你的知识组合_](./你的知识组合.md) 37 | - 话题 4 [_石汤和煮青蛙_](./石汤和煮青蛙.md) 38 | -------------------------------------------------------------------------------- /Chapter10/刊后语.md: -------------------------------------------------------------------------------- 1 | # 刊后语 2 | 3 | > _从长远来看,我们塑造了我们的人生,也塑造了我们自己。这个过程永远不会结束,直到我们死去。而我们所做的选择,最终是我们自己的责任。_ 4 | > 5 | > _-- 埃莉诺-罗斯福_ 6 | 7 | 在第一版出版之前的二十年里,我们参与了计算机从业余爱好者的好奇心到现代企业的需求的演变。在这二十年里,软件已经超越了单纯的商业机器,真正的占领了世界。但是这对我们来说到底意味着什么? 8 | 9 | 在《[_The Mythical Man-Month: 关于软件工程_](https://www.amazon.com/Mythical-Man-Month-Software-Engineering-Anniversary/dp/0201835959) 》的论文中,弗雷德-布鲁克斯说:"程序员,就像诗人一样,工作时只稍稍脱离了纯粹的思想范畴。他在空气中建造他的城堡,从空气中通过想象力的发挥来创造。" 我们从一张白纸开始,就可以创造出我们能想象的任何东西。而我们所创造的东西也会改变世界。 10 | 11 | 从 Twitter 帮助人们计划革命,到汽车上的处理器可以阻止你打滑,再到智能手机意味着我们不再需要记住讨厌的日常细节,我们的程序无处不在。我们的想象力无处不在。 12 | 13 | 我们这些开发者是非常有特权的。我们真正在打造未来。这是一种非凡的力量。而伴随着这种力量的是非凡的责任。 14 | 15 | 我们有多少时候会停下来思考这个问题?我们有多少时候会在自己和更多的观众中讨论这意味着什么? 16 | 17 | 嵌入式设备中的计算机比笔记本电脑、台式机和数据中心中的计算机数量要多得多。这些嵌入式计算机通常控制着从发电厂到汽车到医疗设备等关键生命系统。即使是一个简单的中央供暖控制系统或家用电器,如果设计或实施不当,也可能会造成人员死亡。当你为这些设备开发时,你就承担了一个惊人的责任。 18 | 19 | 许多非嵌入式系统也会带来巨大的好处和巨大的伤害。社交媒体可以促进和平革命,也可以煽动丑陋的仇恨。大数据可以让购物变得更容易,它可以摧毁你认为你所拥有的任何一丝隐私。银行系统做出的贷款决定改变了人们的生活。 20 | 21 | 我们已经看到了关于乌托邦未来的可能性的暗示,也看到了导致噩梦般的乌托邦的意外后果的例子。这两种结果之间的差别可能比你想象的更微妙。而这一切都掌握在你的手中。 22 | 23 | ## 道德罗盘 24 | 我们所拥有的意外力量的代价就是警惕。我们的行为直接影响着人们。我们的软件不再是车库里 8 位 CPU 上的业余程序,也不再是地下室主机上的孤立的批处理业务流程,甚至不再是桌面电脑上的孤立的业务流程;我们的软件编织着现代人的日常生活。 25 | 26 | 我们有责任对我们交付的每一段代码都要问自己两个问题。 27 | 28 | 1. 我是否保护了用户? 29 | 30 | 2. 我自己会使用吗? 31 | 32 | 首先,你应该问 "我是否尽了最大的努力,保护了这段代码的用户不受伤害?" 我是否已经为那个简单的婴儿监视器打上了持续的安全补丁?我是否确保无论自动中央供暖恒温器发生故障,客户仍然可以手动控制?我是否只存储了我需要的数据,并对任何个人数据进行了加密? 33 | 34 | 没有人是完美的,每个人都会时不时地漏掉一些东西。但是,如果你不能如实说,你试图把所有的后果都列举出来,并确保保护用户不受影响,那么当事情出了问题时,你也要承担一定的责任。 35 | 36 | --- 37 | ## 提示 98 首先,不要做坏事 38 | --- 39 | 40 | 第二,有一个与黄金法则相关的判断:我愿意成为这个软件的用户吗?我希望我的信息被共享吗?我希望我的行踪被交给零售店吗?我是否愿意被这个自主驾驶的车辆驱动?我愿意这样做吗? 41 | 42 | 有些创造性的想法开始绕过了道德行为的界限,如果你参与了这个项目,你和赞助者一样有责任。无论你如何合理化,有一条规则仍然是正确的。 43 | 44 | --- 45 | ## 提示 99 如果你纵容了一个卑鄙小人,你就是一个卑鄙小人。 46 | --- 47 | 48 | ## 想象你想要的未来 49 | 这取决于你。是你的想象力,你的希望,你的关注,提供了纯正的思想基础,构建了未来 20 年及以后的未来。 50 | 你们正在为自己和你们的后代建设未来。你们的责任是让它成为一个我们都想居住的未来。当你们做的事情与这个理想相悖的时候,要有勇气说 "不!" 设想我们可以拥有的未来,并有勇气去创造它。每天在空中建造城堡。 51 | 52 | 我们都有一个很棒的人生。分享它。庆祝它。构建它。 53 | 54 | 享受生活的乐趣! 55 | -------------------------------------------------------------------------------- /Chapter2/务实的方法.md: -------------------------------------------------------------------------------- 1 | # 务实的方法 2 | 3 | 4 | 有一些技巧和窍门适用于软件开发的各个级别,几乎是公理的想法,以及实际上是通用的过程。 然而,这些方法很少被记录下来。 在设计,项目管理或编码的讨论中,您通常会发现它们是用奇数句写下来的。 但是为了您的方便,我们将在这里将这些想法和过程结合在一起。 5 | 6 | 成为软件开发的第一个也是最重要的话题的核心:话题 8:[_好设计的本质_](./好设计的本质.md)。 一切都从此开始。 7 | 8 | 接下来的两部分,话题 9 [_重复的恶魔_](./重复的恶魔.md) 和话题 10 [_正交性_](./正交性.md),联系非常紧密。第一个警告您不要在整个系统中重复知识,第二个警告您不要在多个系统组件之间分配任何一项知识。 9 | 10 | 随着变化的步伐加快,保持我们的应用程序相关性变得越来越难。 在话题 11 [_可逆性_](./可逆性.md) 中,我们将介绍一些有助于使您的项目与不断变化的环境隔离的技术。 11 | 12 | 接下来两部分也同样联系紧密。在话题 12 [_示踪剂子弹_](./示踪剂子弹.md) 我们谈论的是一种开发风格,它允许您同时收集需求,测试设计和实现代码。 这是跟上现代生活步伐的唯一途径。 13 | 14 | 话题 13 [_原型和便笺_](./原型和便笺.md) 向您展示了如何使用原型测试架构,算法,接口和想法。 在现代世界中,在全心全意致力于创意之前,测试创意并获得反馈至关重要。 15 | 16 | 随着计算机科学的慢慢成熟,设计师们正在生产越来越多的高级语言。虽然接受 “make it so” 的编译器还没有发明出来,但在话题 14 [_域语言_](./域语言.md) 中,我们提出了一些更温和的建议,您可以自己实现。 17 | 18 | 最后,我们所有人都在时间和资源有限的世界中工作。 如果您善于弄清事情要花多长时间,那么您可以更好地在这两种稀缺的环境中生存下来(并使老板或客户更快乐),这在话题 15 [_估算_](./估算.md) 中涵盖了。 19 | 20 | 通过在开发过程中牢记这些基本原则,您可以编写更好,更快,更强大的代码。 您甚至可以使它看起来容易。 21 | -------------------------------------------------------------------------------- /Chapter2/原型和便签.md: -------------------------------------------------------------------------------- 1 | # 原型和便签 2 | 3 | 4 | 许多不同的行业都使用原型来尝试特定的想法。 原型制作比全面生产便宜得多。 例如,汽车制造商可能会为新汽车设计制造许多不同的原型。 每款产品都旨在测试汽车的特定方面-空气动力学,造型,结构特征等。 守旧派的人可能会使用黏土模型进行风洞测试,也许轻木和胶布模型会用于艺术部门,等等。 不太浪漫的人会在计算机屏幕或虚拟现实中进行建模,从而进一步降低成本。 这样,可以尝试冒险或不确定的元素,而不必致力于构建真实项目。 5 | 6 | 我们以同样的方式构建软件原型,出于同样的原因来分析和暴露风险,并以大大降低的成本提供纠正的机会。像汽车制造商一样,我们可以针对一个原型来测试一个项目的一个或多个特定方面。 7 | 8 | 我们倾向于将原型视为基于代码的,但不一定总是如此。像汽车制造商一样,我们可以用不同的材料制造原型。便利贴非常适合制作动态事物的原型,例如工作流和应用程序逻辑。用户界面可以原型化为白板上的图形,也可以原型为使用绘画程序或界面构建器绘制的非功能性模型。 9 | 10 | 原型旨在回答仅几个问题,因此与投入生产的应用程序相比,它们便宜得多且开发速度更快。该代码可以忽略不重要的细节-目前对您而言并不重要,但稍后对用户可能非常重要。例如,如果要制作UI原型,则可以避免使用错误的结果或数据。另一方面,如果您只是研究计算或性能方面的问题,则可以摆脱糟糕的UI,甚至根本没有UI。 11 | 12 | 但是,如果您发现自己无法放弃细节,那么就需要问自己是否真的在建造原型。在这种情况下,也许采用示踪子弹式开发会更合适(请参阅话题 12 [_示踪子弹_](./示踪子弹.md) ) 13 | 14 | ## 原型化的东西 15 | 16 | 您可以选择使用原型进行什么样的调查? 任何带有风险的东西。 以前没有尝试过的任何东西,或对最终系统绝对重要的任何东西。 未经证实,实验或可疑的任何内容。 您不满意的任何地方。 你可以原型 17 | 18 | - 架构 19 | - 现有系统中的新功能 20 | - 外部数据的结构或内容 21 | - 第三方工具或组件 22 | - 性能问题 23 | - 用户界面设计 24 | 25 | 原型制作是一种学习体验。 它的价值不在于所产生的代码,而在于汲取的教训。 这就是原型制作的重点。 26 | 27 | 28 | --- 29 | ## 提示 21 学习原型 30 | --- 31 | 32 | ## 如何使用原型 33 | 34 | 在构建原型时,您可以忽略哪些细节? 35 | 36 | _正确性_ 37 | 38 | 您也许可以在适当的地方使用伪数据。 39 | 40 | _完整性_ 41 | 42 | 该原型只能在非常有限的意义上起作用,也许只有一个预选的输入数据和一个菜单项。 43 | 44 | _坚固性_ 45 | 46 | 错误检查可能不完整或完全丢失。 如果您偏离预定义的路径,原型可能会在光辉的烟火表演中崩溃并燃烧。 没关系。 47 | 48 | _样式_ 49 | 50 | 承认这一点很痛苦,但是原型代码可能没有太多注释或文档的方式。 您可能会由于对原型的经验而产生大量的文档,但是相对而言原型系统本身很少。 51 | 52 | 由于原型应该掩盖细节,并专注于所考虑的系统的特定方面,因此您可能希望使用高级脚本语言来实现原型,该脚本语言要比项目的其余部分更高(可能是Python或Ruby之类的语言) ),因为这些语言可能会妨碍您。 您可以选择继续使用原型所使用的语言进行开发,也可以进行切换。 毕竟,您还是要扔掉原型。 53 | 54 | 如果您需要对用户界面进行原型设计,则可以使用许多工具来关注外观和/或交互,而不必担心代码或标记。 55 | 56 | 脚本语言也可以很好地与“胶水”结合使用,将低级的部分组合成新的组合。 使用这种方法,您可以将现有组件快速组装成新配置,以查看其工作方式。 57 | 58 | ## 原型架构 59 | 60 | 构建了许多原型来对正在考虑的整个系统进行建模。 与示踪子弹相反,原型系统中的各个模块都不需要特别起作用。 实际上,您甚至不需要编写代码即可对体系结构进行原型设计—您可以使用便利贴或索引卡在白板上进行原型设计。 您正在寻找的是系统如何整体挂在一起,再次推迟细节。 以下是您可能需要在体系结构原型中寻找的一些特定领域: 61 | 62 | - 主要组成部分的职责是否明确定义并适当? 63 | - 主要组件之间的协作是否定义明确? 64 | - 耦合最小化了吗? 65 | - 您能确定潜在的重复来源吗? 66 | - 接口定义和约束是否可以接受? 67 | - 每个模块是否都有执行过程中所需数据的访问路径? 需要时是否具有访问权限? 68 | 69 | 最后一项往往会从原型制作经验中产生最大的惊喜和最有价值的结果。 70 | 71 | ## 如何不使用原型 72 | 73 | 在着手任何基于代码的原型之前,请确保每个人都了解您正在编写一次性代码。对于不知道原型只是原型的人,原型可能具有欺骗性的吸引力。您必须非常清楚此代码是一次性的,不完整的,并且无法完成。 74 | 75 | 容易被示范原型的外观完整性所误导,如果您未设定正确的期望,项目发起人或管理层可能会坚持部署原型(或其后代)。提醒他们,您可以用轻木和胶带制成一台出色的新车原型,但您不会在交通高峰时驾驶它! 76 | 77 | 如果您认为您的环境或文化中很可能会误解原型代码的目的,那么使用示踪符号方法可能会更好。您将最终获得一个可靠的框架,该框架可作为未来发展的基础。 78 | 79 | 如果使用得当,通过在开发周期的早期识别并纠正潜在的问题点,原型可以为您节省大量时间,金钱,痛苦-修复错误既便宜又容易。 80 | 81 | ## 相关内容包括 82 | 83 | - 话题 14 [_域语言_](./域语言.md) 84 | - 话题 37 [_聆听你的蜥蜴脑_](../Chapter7/聆听你的蜥蜴脑.md) 85 | - 话题 27 [_别开过头了_](../Chapter4/别开过头了.md) 86 | - 话题 17 [_Shell 游戏_](../Chapter3/shell.md) 87 | - 话题 45 [_需求坑_](../Chapter8/需求坑.md) 88 | - 话题 12 [_示踪子弹_](./示踪子弹.md) 89 | - 话题 51 [_让用户满意_](../Chapter9/让用户满意.md) 90 | 91 | ## 练习 92 | ### 练习 3 (尽可能回答) 93 | 94 | 市场营销人员希望与您坐下来集思广益。 他们正在考虑使用可点击的图片映射将您带到其他页面,依此类推。 但是他们无法确定图片的模型-可能是汽车,电话或房屋。 您有目标页面和内容的列表; 他们希望看到一些原型。 哦,对了,您还有15分钟的时间。 您可能会使用哪些工具? 95 | -------------------------------------------------------------------------------- /Chapter2/可逆性.md: -------------------------------------------------------------------------------- 1 | # 可逆性 2 | 3 | 4 | > _如果这是您唯一的想法,没有什么比这更危险了。_ 5 | > 6 | > --- _埃米尔·奥古斯特·沙蒂埃(阿兰),《宗教提案》,1938年_ 7 | 8 | 工程师更喜欢简单,单一的解决方案。 数学测试可以使您满怀信心地宣称 x = 2 比关于法国大革命的各种模糊热情原因的论文要舒适得多。 管理层倾向于与工程师达成一致:单个简单的答案非常适合电子表格和项目计划。 9 | 10 | 如果只有现实世界能够合作! 不幸的是,虽然 x 今天是 2,但可能明天需要它是 5,下周可能是 3。 没有什么是永远的 --- 如果您严重依赖某个事实,则几乎可以保证它会改变。 11 | 12 | 总是有不止一种方法来实现某些功能,而且通常有不止一家供应商可以提供第三方产品。 如果您进入的项目受到近视概念(只有一种方法)的束缚,那么您可能会感到不快。 随着未来的发展,许多项目团队被迫睁大了眼睛: 13 | 14 | “但是您说过我们要使用数据库 XYZ! 我们已经完成了项目编码的 85%,我们现在不能更改!” 程序员抗议。 “抱歉,但是我们公司决定改为对所有项目使用数据库 PDQ 进行标准化。 所有项目。这是我无法控制的。我们只需要重新编码。你们所有人都将在周末工作,直到另行通知” 15 | 16 | 更改不必那么严厉,甚至不必那么紧迫。但随着时间的推移,你的项目进展,你可能会发现自己陷入了一个站不住脚的位置。在每一个关键的决策中,项目团队都致力于一个更小的目标,一个拥有更少选择现实的窄版本。 17 | 18 | 一旦做出许多关键决定,目标就会变得很小,以至于它移动,风向改变,东京的蝴蝶拍打翅膀,你都会错过。 你可能会错过很多。 19 | 20 | 问题在于关键的决定不容易逆转。 21 | 22 | 一旦你决定使用该供应商的数据库,该体系结构模式或某个部署模型,您便会采取行动,除非付出很大的代价,否则无法撤消。 23 | 24 | ## 可逆性 25 | 26 | 本书中的许多话题都旨在生产灵活,适应性强的软件。 通过坚持他们的推荐,尤其是 [_DRY 原则_](./重复的恶魔.md),[_解耦_](../Chapter5/解耦.md) 和使用[_外部配置_](../Chapter5/配置.md),我们不必做出许多关键的,不可逆的决定。 这是一件好事,因为我们并不总是在第一次就做出最佳决定。 我们致力于某种技术,只是发现我们无法雇用足够的具有必要技能的人员。 我们会在竞争对手将其收购之前锁定某些第三方供应商。 需求,用户和硬件变更的速度快于我们开发软件的速度。 27 | 28 | 假设您在项目早期决定使用供应商 A 的关系数据库。后来,在性能测试期间,您发现该数据库太慢了,但是供应商B的文档数据库却很快。 对于大多数常规项目,您会很不幸。 在大多数情况下,在整个代码中都纠缠了对第三方产品的调用。 但是,如果您真的将数据库的概念抽象出来(仅提供持久性即服务的程度),这样你就有了在中途换马的灵活性。 29 | 30 | 同样,假设该项目从一个基于浏览器的应用程序开始,但是游戏,市场决定他们真正想要的是一个移动应用程序。 那对你有多难? 在理想的情况下,它不会对您造成太大的影响,至少在服务器方面不会造成太大影响。 您将剥离一些 HTML 呈现,并用 API 替换它。 31 | 32 | 错误在于假设任何决定都是一成不变的,并且没有为可能发生的突发事件做准备。 与其将决策刻在石头上,不如将它们想象成是写在沙滩上的沙子上。 随时可能发生大浪,将其消灭。 33 | 34 | --- 35 | ## 提示 18 没有最终决定 36 | --- 37 | 38 | ## 灵活的架构 39 | 40 | 尽管许多人试图保持其代码的灵活性,但是您还需要考虑在架构,部署和供应商集成方面保持灵活性。 41 | 我们在 2019 年编写这本书。自世纪之交以来,我们已经看到以下 “最佳实践” 服务器端架构: 42 | 43 | - 大块的铁 44 | - 大铁联合会 45 | - 负载均衡的商品硬件集群 46 | - 运行应用程序的基于云的虚拟机 47 | - 基于云的虚拟机运行服务 48 | - 上面的容器化版本 49 | - 云支持的无服务器应用程序 50 | - 不可避免地,显然有些任务又回到了大块的铁板上。 51 | 52 | 继续把最新最伟大的时尚加入到这个列表中,然后敬畏地看一眼:这是一个奇迹,任何事情都能奏效。 53 | 54 | 您如何计划这种架构的波动? 你不能。 55 | 56 | 您可以做的就是使更改变得容易。 将第三方 API 隐藏在您自己的抽象层后面。 将代码分解为组件:即使最终将它们部署在单个大型服务器上,这种方法也比采用整体应用程序并将其拆分要容易得多。(我们有很多证据可以证明这一点。) 57 | 58 | 而且,尽管这不是特别的可逆性问题,但最后一条建议是。 59 | 60 | --- 61 | ## 提示 19 不再追随时尚 62 | --- 63 | 64 | 没人知道未来会怎样,尤其是我们! 因此,使您的代码可以滚动:在可能的情况下“继续运行”,在必要的情况下进行滚动。 65 | 66 | ## 相关内容包括 67 | 68 | - 话题 28 [_解耦_](../Chapter5/解耦.md) 69 | - 话题 8 [_好设计的本质_](./好设计的本质.md) 70 | - 话题 10 [_正交性_](./正交性.md) 71 | - 话题 45 [_需求坑_](../Chapter8/需求坑.md) 72 | - 话题 19 [_版本控制_](../Chapter3/版本控制.md) 73 | - 话题 50 [_实用入门套件_](../Chapter9/实用入门套件.md) 74 | 75 | ## 挑战 76 | - 是时候和薛定谔的猫讨论一下量子力学了。 77 | 78 | 假设你有一只猫和一个放射性粒子在一个封闭的盒子里。这个粒子有 50% 的几率裂变成两个。如果是的话,猫会被杀死的。如果不行,猫就没事了。那么,猫是死了还是活了?根据薛定谔的说法,正确的答案是两者都是(至少在盒子保持关闭的情况下)。每次发生有两种可能结果的亚核反应,宇宙就被克隆。在一个宇宙中,这件事发生了,而在另一个宇宙中却没有发生。猫活在一个宇宙中,死在另一个宇宙中。只有当你打开盒子你才知道你在哪个宇宙里 79 | 80 | 难怪为未来编码很难。 81 | 82 | 但是想想代码的进化,就像一个装满了薛定谔猫的盒子:每个决定都会导致不同版本的未来。代码可以支持多少种可能的未来?哪个可能性更大?到时候支持他们有多难? 83 | 84 | 你敢打开盒子吗? 85 | -------------------------------------------------------------------------------- /Chapter2/域语言.md: -------------------------------------------------------------------------------- 1 | # 域语言 2 | 3 | 4 | > _语言的极限是人的世界的极限。_ 5 | > 6 | > -- _路德维格·维特根斯坦_ 7 | 8 | 计算机语言会影响您对问题的看法以及对交流的看法。 每种语言都具有一系列功能-诸如静态与动态类型,早期与后期绑定,函数式与OO,继承模型,mixins,宏之类的流行语-所有这些都可能建议或掩盖某些解决方案。 与基于Haskell风格的思想的解决方案相比,设计考虑到C ++的解决方案将产生不同的结果,反之亦然。 相反,我们认为更重要的是,问题域的语言也可能提出编程解决方案。 9 | 10 | 我们总是尝试使用应用程序领域的词汇来编写代码(请参阅 维护词汇表[TODO],在此建议使用项目词汇表)。 在某些情况下,务实的程序员可以进入下一个级别,并使用领域的词汇,语法和语义(语言)来进行实际编程。 11 | 12 | --- 13 | ## 提示 22 程序靠近问题域 14 | --- 15 | 16 | ## 一些现实世界的领域语言 17 | 18 | 让我们看几个例子,人们已经这样做了。 19 | 20 | ## RSpec 21 | RSpec 是 Ruby 的测试库。 它启发了其他大多数现代语言的版本。 RSpec 中的测试旨在反映您期望的代码行为。 22 | 23 | ```ruby 24 | describe BowlingScore do 25 | it "totals 12 if you score 3 four times" do 26 | score = BowlingScore.new 27 | 4.times { score.add_pins(3) } 28 | expect(score.total).to eq(12) 29 | end 30 | end 31 | ``` 32 | 33 | ## Cucumber 34 | Cucumber 是指定测试的编程语言中立方式。 您使用适合您所使用语言的 Cucumber 版本运行测试。 为了支持类似自然语言的语法,您还必须编写特定的匹配器,以识别短语并提取测试参数。 35 | 36 | ```java 37 | Feature: Scoring 38 | Background: 39 | Given an empty scorecard 40 | Scenario: bowling a lot of 3s 41 | Given I throw a 3 42 | And I throw a 3 43 | And I throw a 3 44 | And I throw a 3 45 | Then the score should be 12 46 | ``` 47 | 48 | Cucumber 测试旨在由软件的客户阅读(尽管实际上很少发生;为什么没有那么多企业用户阅读 Cucumber 功能 ?为什么会这样呢?) 49 | 50 | --- 51 | ### 为什么没有那么多企业用户阅读 Cucumber 功能 52 | 传统的收集需求,设计,代码和运送方法不起作用的原因之一是,它以我们知道需求是什么的概念为基础。 但是我们很少这样做。 您的业务用户对他们想要实现的目标含糊其词,但是他们既不知道也不关心细节。 这就是我们价值的一部分:我们了解意图并将其转换为代码。 53 | 54 | 因此,当您强迫业务人员签署需求文档或让他们同意一组Cucumber功能时,您所做的等同于让他们检查以Sumerian撰写的文章中的拼写。 他们会进行一些随机更改以保存面子并签名,以使您离开他们的办公室。 55 | 56 | 给他们运行的代码,但是,他们可以使用它。 那才是他们真正需求的体现。 57 | 58 | --- 59 | 60 | ## Phoenix Routes 61 | 62 | 许多 Web 框架都有路由功能,可以将传入的 HTTP 请求映射到代码中的处理程序函数上。 这是 Phoenix 的一个例子。 63 | 64 | ```elixir 65 | scope "/", HelloPhoenix do 66 | pipe_through :browser # Use the default browser stack 67 | 68 | get "/", PageController, :index 69 | resources "/users", UserController 70 | end 71 | ``` 72 | 73 | 这表示以 “/” 开头的请求将通过一系列适用于浏览器的过滤器运行。 对“/”本身的请求将由 PagewController 模块中的 index 函数处理。 UsersController 实现了管理可通过 url /users 访问的资源所需的功能。 74 | 75 | ## Ansible 76 | 77 | Ansible 是一种通常在一堆远程服务器上配置软件的工具。 它是通过阅读您提供的规范,然后在服务器上进行使其与该规范相对应的任何操作来完成的。 该规范可以用YAML编写, 这种语言可以根据文字描述构建数据结构: 78 | 79 | ```YAML 80 | --- 81 | - name: install nginx 82 | apt: name=nginx state=latest 83 | - name: ensure nginx is running (and enable it at boot) 84 | service: name=nginx state=started enabled=yes 85 | 86 | - name: write the nginx config file 87 | template: src=template/nginx.conf.j2 dest=/etc/nginx/nginx.conf 88 | notify: 89 | - restart nginx 90 | ``` 91 | 92 | 此示例可确保在我的服务器上安装了最新版本的nginx,默认情况下已启动该版本,并使用您提供的配置文件。 93 | 94 | ## 领域语言的特征 95 | 96 | 让我们更仔细地看这些例子。 97 | 98 | RSpec 和 Phoenix 路由以其宿主语言(Ruby 和 Elixir)编写。 它们使用一些相当曲折的代码,包括元编程和宏,但是最终它们被编译并作为常规代码运行。 99 | 100 | Cucumber 测试和 Ansible 配置以其自己的专用语言编写。 Cucumber 测试将转换为要运行的代码或数据结构,而Ansible规范始终会转换为由 Ansible 本身运行的数据结构。 101 | 102 | 结果,RSpec 和路由代码被嵌入到您运行的代码中:它们是代码词汇的真正扩展。 Cucumber 和 Ansible 通过代码读取,并转换为代码可以使用的某种形式。 103 | 104 | 我们称 RSpec 和内部域语言的路由示例,而 Cucumber 和 Ansible 使用外部语言。 105 | 106 | ## 内部和外部语言之间的权衡 107 | 108 | 通常,内部领域语言可以利用其宿主语言的功能:您创建的领域语言功能更强大,而这种功能是免费提供的。 例如,您可以使用一些 Ruby 代码自动创建一堆 RSpec 测试。 在这种情况下,我们可以测试没有备件或罢工的分数: 109 | 110 | ```ruby 111 | describe BowlingScore do 112 | (0..4).each do |pings| 113 | (1..20).each do |throws| 114 | target = pins * throws 115 | 116 | it "totals #{target} if you score #{pins} #{throws} times" do 117 | score = BowlingScore.new 118 | throws.times { score.add_pins(pins) } 119 | expect(score.total).to eq(target) 120 | end 121 | end 122 | end 123 | end 124 | ``` 125 | 126 | 您刚编写了100项测试。 休息一天。 127 | 128 | 内部域语言的缺点是,您受该语言的语法和语义的束缚。 尽管某些语言在这方面非常灵活,但是您仍然不得不在所需的语言和可以实现的语言之间进行折衷。 129 | 130 | 最终,无论您想出什么,仍然必须是目标语言中的有效语法。 带有宏的语言(例如 Elixir,Clojure 和 Crystal)为您提供了更多的灵活性,但最终语法是语法。 131 | 132 | 外部语言没有这种限制。只要您可以为该语言编写解析器,就可以轻松进行。有时,您可以使用其他人的解析器(就像 Ansible 使用 YAML 所做的那样),但是您又回到了妥协的位置。 133 | 134 | 编写解析器可能意味着向您的应用程序添加新的库和工具。 编写好的解析器并不是一件容易的事。 但是,如果您发自内心,可以考虑使用解析器生成器(例如 bison 或 ANTLR)以及解析框架(例如许多 PEG 解析器)。 135 | 136 | 我们的建议很简单:不要花费比您多的努力。 编写域语言会给您的项目增加一些成本,并且您需要确信节省的资金(可能是长期的)可以抵消。 137 | 138 | 通常,如果可以的话,请使用现成的外部语言(例如 YAML,JSON 或 CSV)。 如果没有,请查看内部语言。 我们建议仅在应用程序的用户使用您的语言编写时使用外部语言。 139 | 140 | ## 廉价的内部领域语言 141 | 142 | 最后,如果您不介意宿主语言语法泄漏,则可以创建内部域语言。 不要做大量的元编程。 相反,只需编写函数即可完成工作。 实际上,这几乎是 RSpec 的作用: 143 | 144 | ```ruby 145 | describe BowlingScore do 146 | it "totals 12 if you score 3 four times" do 147 | score = BowlingScore.new 148 | 4.times { score.add_pins(3) } 149 | expect(score.total).to eq(12) 150 | end 151 | end 152 | ``` 153 | 154 | 在这段代码中,describe, it, expect,to 和 eq 都是 Ruby 的方法。 关于如何传递对象,幕后有一些线索,但这全都是代码。 155 | 156 | 我们将在练习中对此进行一些探讨。 157 | 158 | ## 相关内容包括 159 | 160 | - 话题 32 [配置](../Chapter5/配置.md) 161 | - 话题 8 [好设计的本质](./好设计的本质.md) 162 | - 话题 13 [原型和便签](./原型和便签.md) 163 | 164 | ## 挑战 165 | 166 | - 您当前项目的某些要求可以用特定领域的语言来表达吗? 是否有可能编写出可以生成大多数所需代码的编译器或翻译器? 167 | 168 | - 如果您决定采用迷你语言作为更接近问题域的一种编程方式,那么您将接受为实现它们而付出的努力。 您能看到为一个项目开发的框架可以在其他项目中重用的方式吗? 169 | 170 | ## 练习 171 | 172 | ### 练习 4 (尽可能回答) 173 | 174 | 我们想要实现一种迷你语言来控制简单的绘图程序包(也许是海龟图形系统)。 该语言由单字母命令组成。 某些命令后跟一个数字。 例如,以下输入将绘制一个矩形。 175 | 176 | ```javascript 177 | P 2 # select pen 2 178 | D # pen down 179 | W 2 # draw west 2 180 | N 1 # then north 1 181 | E 2 # then east 2 182 | S 1 # then back south 183 | U # pen up 184 | ``` 185 | 186 | 执行解析该语言的代码。 它的设计应使其易于添加新命令。 187 | 188 | ### 练习 5 (尽可能回答) 189 | 190 | 在上一个练习中,我们为乌龟图形语言实现了一个简单的解析器-它是一种外部域语言。 现在,再次将其实现为内部语言。 不要做任何聪明的事情:只需为每个命令编写一个函数。 您可能必须将命令的名称更改为小写,并且可能需要将它们包装在某些内容中以提供一些上下文。 191 | 192 | ### 练习 6 (尽可能回答) 193 | 194 | 设计 BNF 语法以解析时间规范。 以下所有示例均应接受。 195 | 196 | ```javascript 197 | 4pm, 7:38pm, 23:42, 3:16, 3:16am 198 | ``` 199 | 200 | ### 练习 7 (尽可能回答) 201 | 202 | 在上一练习中,使用您选择的语言的 PEG 解析器生成器为 BNF 语法实现解析器。 输出应该是一个整数,其中包含午夜之后的分钟数。 203 | 204 | ### 练习 8 (尽可能回答) 205 | 206 | 使用脚本语言和正则表达式来实现时间解析器。 207 | -------------------------------------------------------------------------------- /Chapter2/好设计的本质.md: -------------------------------------------------------------------------------- 1 | # 好设计的本质 2 | 3 | 4 | 世界上到处都是专家和专家,他们都渴望在设计软件方面传递他们来之不易的智慧。 首字母缩略词,列表(似乎倾向于五个条目),模式,图表,视频,演讲,以及(互联网就是互联网)很可能是一部关于迪米特法则的很酷的系列,使用花里花哨非常华丽的解释。 5 | 6 | 我们,您的温柔作家,也对此感到内疚。 但是,我们想通过解释一些直到最近才对我们显而易见的东西来做出修正。 首先声明: 7 | 8 | --- 9 | ## 提示 14 好设计比糟糕的设计更容易更改 10 | --- 11 | 12 | 如果东西能够适应使用它的人,那么它就是经过精心设计的。 对于代码,这意味着它必须通过更改来适应。 因此,我们相信 ETC 原则:更容易更改。 ETC。 就是这样。 13 | 14 | 据我们所知,每种设计原则都有 ETC 的特殊情况。 15 | 16 | 为什么解耦好? 因为通过隔离关注点,我们使每个方面都更容易更改。 ETC。 17 | 18 | 为什么单一责任原则有用? 因为需求的变化只反映了一个模块的变化。 ETC。 19 | 20 | 为什么命名很重要? 因为好的命名使代码更易于阅读,因此您必须阅读代码才能进行更改。 ETC! 21 | 22 | ## ETC 是一种价值, 而不是规则 23 | 24 | 价值是可以帮助您做出决定的事情:我应该这样做还是那样做? 在考虑软件时,ETC 是一个指导方针,可以帮助您进行选择。 就像您所有其他价值观一样,它应该浮在您有意识的思想后面,并巧妙地将您推向正确的方向。 25 | 26 | 但是,您如何做到这一点呢? 我们的经验是,这需要一些初步的有意识的强化。 您可能需要花一个星期左右的时间自问:“我刚才所做的事情是否使整个系统更容易或更难更改?” 在你保存文件时问自己。 在编写测试时问自己。 修复错误时问自己。 27 | 28 | ETC 有一个隐含的前提。 它假设一个人可以在将来说出更容易改变的路径。 在很多时候,常识是正确的,您可以进行有根据的猜测。 29 | 30 | 不过,有时候,您不会有任何线索。 没关系。 在那种情况下,我们认为您可以做两件事。 31 | 32 | 首先,考虑到您不确定要进行哪种形式的更改,您可以始终退回到最终的“易于更改”路径:尝试使编写的内容可替换。 这样一来,无论将来发生什么,这一段代码都不会成为障碍。 这似乎很极端,但实际上,无论如何,这始终是您应该做的事情。 实际上,这只是在考虑保持代码分离和凝聚力。 33 | 34 | 其次,将此视为开发本能的一种方式。 请在工程日刊上记录下情况:您的选择以及对更改的一些猜测。 在源代码中留一个标签。 然后,将来某一天,当必须更改此代码时,您可以进行回顾并提供反馈。 下次您遇到类似的岔路口时可能会有所帮助。 35 | 36 | 本章的其余各个部分对设计有特定的想法,但是所有想法都是受这一原则启发的。 37 | 38 | ## 相关内容包括 39 | - 话题 14 [_域语言_](./域语言.md) 40 | - 话题 28 [_解耦_](./Chapter5/解耦.md) 41 | - 话题 9 [_重复的恶魔_](./重复的恶魔.md) 42 | - 话题 31 [_继承税_](./Chapter5/继承税.md) 43 | - 话题 10 [_正交性_](./正交性.md) 44 | - 话题 11 [_可逆性_](./可逆性.md) 45 | - 话题 30 [_转换编程_](./Chapter5/转换编程.md) 46 | 47 | ## 挑战 48 | - 考虑一下您经常使用的设计原则。 是否旨在使事情易于更改 49 | 50 | - 还要考虑语言和编程范例(OO,FP,Reactive 等)。 在帮助您编写 ETC 代码时,有没有正面的看法或负面的看法? 两者都有吗? 在编码时要避免负数并(如他们所说的)强调正数,该怎么办? 51 | 52 | - 保存文件时,许多编辑器都支持(内置或通过扩展名)运行命令。 让您的编辑器弹出 ETC? 每次保存时都会显示一条消息,并将其用作提示来考虑您刚刚编写的代码。 容易改变吗? 53 | -------------------------------------------------------------------------------- /Chapter2/正交性.md: -------------------------------------------------------------------------------- 1 | # 正交性 2 | 3 | 4 | 如果要生产易于设计,构建,测试和扩展的系统,则正交性是一个至关重要的概念。 但是,正交性的概念很少直接讲授。 通常,它是您学习的其他各种方法和技术的隐含功能。 这是个错误。 学习直接应用正交性原理后,您会发现所生产系统的质量立即得到改善。 5 | 6 | ## 什么是正交性? 7 | “正交”是从几何学借来的术语。 如果两条线成直角相交,则它们是正交的,例如图形上的轴。 用矢量表示,两条线是独立的。 当图表上的数字1向北移动时,它不会改变它向东或向西移动的距离。 数字2向东移动,但不向北或向南移动。 8 | 在计算中,该术语表示一种独立性或去耦性。 如果一个或多个变化不影响其他任何一个,则两个或多个事物是正交的。 在设计良好的系统中,数据库代码将与用户界面正交:您可以在不影响数据库的情况下更改界面,并在不更改界面的情况下交换数据库。 9 | 10 | ![_正交性_](../assets/Orthogonality.png) 11 | 12 | 在研究正交系统的好处之前,我们先来看一下非正交系统。 13 | 14 | ### 一个非正交系统 15 | 你在乘坐直升机游览大峡谷的时候,飞行员明显犯了午饭吃鱼的错误,突然呻吟并晕倒。幸运的是,他让你在离地100英尺的地方盘旋。 16 | 17 | 幸运的是,你前一天晚上读过一个关于直升机的维基百科页面。你知道直升机有四个基本控制装置。循环是你右手拿的棍子。移动它,直升机就会向相应的方向移动。你的左手握着集体投球杆。拉起这个,你增加了所有叶片的螺距,产生升力。在俯仰杆的末端是油门。最后,你有两个脚踏板,可以改变尾桨的推力,从而帮助直升机转弯。 18 | 19 | “别紧张!“你想。轻轻地降低集体投球杆,你将优雅地降落到地面,像一个英雄一样。然而,当你尝试的时候,你会发现事情并不是那么简单。直升机的机头下降,你开始向左盘旋。突然你发现你在驾驶一个系统,每个控制输入都有次要的影响。降低左侧操纵杆,您需要向右侧斗杆添加补偿向后移动并踩下右侧踏板。但是,这些更改都会再次影响所有其他控件。突然间,你在一个难以置信的复杂系统中游刃有余,每一个变化都会影响到所有其他的输入。你的工作量是惊人的:你的手和脚不断地移动,试图平衡所有相互作用的力量。 20 | 21 | 直升机的控制系统显然不是正交的。 22 | 23 | ### 正交的好处 24 | 25 | 如直升机的示例所示,非正交系统本质上更难以更改和控制。 当任何系统的组件高度相互依赖时,就没有本地修订之类的东西。 26 | 27 | --- 28 | ## 提示 17 消除无关事物之间的影响 29 | --- 30 | 31 | 我们希望设计独立的组件,并且具有单一的,明确定义的目的(Yourdon和Constantine在结构化设计中称之为凝聚力:计算机程序和系统设计学科的基础[_YC86_])。当组件彼此隔离时,您可以更改一个组件而不必担心其余的组件。 只要您不更改该组件的外部接口,就不会在整个系统中引起问题,您会感到很自在。 32 | 33 | 如果编写正交系统,将有两个主要好处:提高生产率和降低风险。 34 | 35 | ### 提高生产率 36 | 37 | - 更改已本地化,因此减少了开发时间和测试时间。 比起一个较大的代码块,编写相对较小的自包含组件要容易得多。 可以设计,编码,进行单元测试然后忘记简单的组件-添加新代码时无需不断更改现有代码。 38 | 39 | - 正交方法还可以促进重用。 如果组件具有特定的,明确定义的职责,则可以将其与新组件以原实施者未曾想到的方式组合在一起。 您的系统耦合越松散,它们越容易重新配置和重新设计。 40 | 41 | - 当您组合正交组件时,生产率会有相当微妙的提高。 假设一个组件执行 M 个不同的操作,而另一个组件执行 N 个操作。 如果它们是正交的,并且将它们组合在一起,那么结果将会有 M * N 种。 但是,如果两个分量不正交,则将有重叠,并且结果将更少。 通过组合正交组件,可以使每单位工作量获得更多功能。 42 | 43 | ### 降低风险 44 | 正交方法可降低任何开发中固有的风险。 45 | 46 | - 有问题的代码段要单独隔离起来。 如果模块有问题,则不太可能将症状传播到系统的其余部分。 将其切碎并移植到新的健康的环境里也更加容易。 47 | 48 | - 由此产生的系统不那么脆弱。 对特定区域进行较小的更改和修复,您产生的任何问题将仅限于该区域。 49 | 50 | - 正交系统可能会得到更好的测试,因为它将更容易在其组件上设计和运行测试。 51 | 52 | - 您将不会与特定的供应商,产品或平台紧密相连,因为与这些第三方组件的接口将被隔离到整个开发的较小部分。 53 | 54 | 让我们看一下将正交性原理应用到工作中的一些方法。 55 | 56 | ## 设计 57 | 58 | 大多数开发人员都熟悉设计正交系统的需求,尽管他们可能会使用诸如模块化,基于组件和分层之类的词来描述过程。 系统应由一组协作模块组成,每个模块都实现彼此独立的功能。 有时,这些组件被组织成层,每个层提供一个抽象级别。 这种分层方法是设计正交系统的有效方法。 由于每个层仅使用其下层提供的抽象,因此您可以在不影响代码的情况下灵活地更改基础实现。 分层还降低了模块之间依赖关系失控的风险。 您经常会看到图表中表示的分层: 59 | 60 | ![_设计分层_](../assets/layering.png) 61 | 62 | 正交设计有一个简单的测试。 布置好组件后,请问自己:如果我极大地改变了特定功能的要求,那么会影响多少个模块? 在正交系统中,答案应该是“一个”。[_16_]在GUI面板上移动按钮不需要更改数据库架构。 添加上下文相关的帮助不应更改计费子系统。 63 | 64 | 让我们考虑一个用于监视和控制加热设备的复杂系统。 最初的要求是使用图形用户界面,但要求已更改,以添加带有工厂按键电话控制的语音响应系统。 在正交设计的系统中,您只需要更改与用户界面关联的那些模块即可处理:控制工厂的底层逻辑将保持不变。 实际上,如果您精心构建系统,则应该能够使用相同的基础代码库支持两个接口。 话题 29 [_杂耍现实世界_](../Chapter5/杂耍现实世界.md) 讨论了使用模型-视图-控制器(MVC)范例编写解耦代码的方法,在这种情况下效果很好。 65 | 66 | 还要问自己,您的设计与现实世界的变化是如何脱钩的。 您是否使用电话号码作为客户标识符? 电话公司重新分配区号时会发生什么? 邮政编码,社会安全或政府ID,电子邮件地址和域都是您无法控制的外部标识符,并且可能由于任何原因随时更改。 不要依赖您无法控制的事物的属性。 67 | 68 | ## 工具箱和库 69 | 70 | 引入第三方工具包和库时,请小心保留系统的正交性。明智地选择您的技术。 71 | 72 | 当您引入工具箱(甚至是团队其他成员的库)时,请问自己是否对您的代码施加了不应该存在的更改。如果对象持久性方案是透明的,则它是正交的。如果需要您以特殊方式创建或访问对象,则不需要。将此类详细信息与您的代码隔离开,还有一个好处,就是将来可以更轻松地更改供应商。 73 | 74 | 企业Java Bean(EJB)系统是正交性的一个有趣示例。在大多数面向事务的系统中,应用程序代码必须描述每个事务的开始和结束。使用EJB时,此信息以声明的方式声明为批注,而不是进行工作的方法。相同的应用程序代码无需更改即可在不同的EJB事务环境中运行。 75 | 76 | 从某种意义上说,EJB是装饰器模式的一个示例:在不更改事物的情况下向它们添加功能。这种编程风格几乎可以在每种编程语言中使用,并且不一定需要框架或库。编程时只需要一点纪律。 77 | 78 | ## 编程 79 | 80 | 每次编写代码时,都有降低应用程序正交性的风险。 除非您不仅持续监视正在执行的操作,而且还持续监视应用程序的较大上下文,否则您可能会无意间在某些其他模块中复制功能,或者两次表达现有知识。 81 | 82 | 您可以使用几种技术来保持正交性: 83 | 84 | ### _使代码保持解耦_ 85 | 86 | 编写害羞的代码-这些模块不会向其他模块透露任何不必要的内容,并且不依赖其他模块的实现。 尝试我们在话题 28 [_解耦_](../Chapter5/解耦.md) 中讨论的Demeter定律。 如果您需要更改对象的状态,请让该对象为您完成。 这样,您的代码将与其他代码的实现保持隔离,并增加了保持正交的机会。 87 | 88 | ### _避免全局数据_ 89 | 90 | 每次您的代码引用全局数据时,它都会将自己绑定到共享该数据的其他组件中。 即使只打算读取的全局变量也可能导致麻烦(例如,如果您突然需要将代码更改为多线程)。 通常,如果将任何必需的上下文显式传递到模块中,则代码将更易于理解和维护。 在面向对象的应用程序中,上下文通常作为参数传递给对象的构造函数。 在其他代码中,您可以创建包含上下文的结构,并传递对它们的引用。 91 | 92 | 设计模式:可重用的面向对象软件的元素[_GHJV95_]中的单例模式是一种确保特定类的对象只有一个实例的方法。 许多人将这些单例对象用作一种全局变量(尤其是在Java之类的语言中,否则它们不支持全局概念)。 注意单例-它们也可能导致不必要的链接。 93 | 94 | ### _避免相同的函数_ 95 | 96 | 通常,您会遇到一组看上去都很相似的函数-也许它们在开始和结束时共享相同的代码,但是每个都有不同的中央算法。 代码重复是结构问题的征兆。 查看设计模式中的策略模式,以实现更好的实现。 97 | 98 | 养成不断批评代码的习惯。 寻找任何机会对其进行重组以改善其结构和正交性。 这个过程称为重构,它是如此重要,以至于我们专门为其专门设置了一个部分(请参阅话题 40 [_重构_](../Chapter7/重构.md))。 99 | 100 | ## 测试 101 | 102 | 正交设计和实施的系统更易于测试。 由于系统组件之间的交互是形式化的且受限制的,因此可以在单个模块级别执行更多的系统测试。 这是个好消息,因为与集成测试相比,模块级别(或单元)测试的指定和执行要容易得多。 实际上,我们建议将这些测试作为常规构建过程的一部分自动执行(请参阅话题 41 [_代码测试_](../Chapter7/代码测试.md) ) 103 | 104 | 编写单元测试本身就是一个有趣的正交性测试。 进行单元测试以构建和运行需要什么? 您是否必须导入系统其余大部分代码? 如果是这样,则说明您发现模块与系统其余部分的耦合不良。 105 | 106 | 漏洞修复也是评估整个系统正交性的好时机。 当您遇到问题时,请评估修复程序的本地化程度。 您仅更改一个模块,还是将更改分散在整个系统中? 进行更改时,它是否可以解决所有问题,还是会神秘地产生其他问题? 这是实现自动化的好机会。 如果您使用版本控制系统(阅读话题 19 [_版本控制_](../Chapter3/版本控制.md) 后),则在测试后将代码重新签入时将修复标签错误。 然后,您可以运行每月报告,分析受每个错误修复影响的源文件数量的趋势。 107 | 108 | ## 文档 109 | 110 | 或许令人惊讶的是,正交性也适用于文档。轴是内容和表示。使用真正正交的文档,您应该能够在不更改内容的情况下显著地更改外观。字处理器提供样式表和宏来帮助。我们个人更喜欢使用标记系统,例如 Markdown:在编写时,我们只关注内容,而将演示文稿留给我们用来呈现它的任何工具 111 | 112 | ## 正交生活 113 | 114 | 正交性与 DRY 原则密切相关。 借助 DRY,您希望最大程度地减少系统中的重复,而通过正交性,您可以减少系统组件之间的相互依赖性。 这可能是一个笨拙的词,但是如果您使用正交性原理并将其与 DRY 原理紧密结合,则会发现您开发的系统更加灵活,更易于理解,并且更易于调试,测试和维护。 115 | 116 | 如果您进入了一个项目,在这个项目中人们拼命地进行更改,并且每次更改似乎都会导致其他四件事出错,请记住直升机的噩梦。 该项目可能不是正交设计和编码的。 现在该重构了。 117 | 118 | 而且,如果您是直升机驾驶员,请不要吃鱼…… 119 | 120 | ## 相关内容包括 121 | 122 | - 话题 28 [_解耦_](../Chapter5/解耦.md) 123 | - 话题 8 [_好设计的本质_](./好设计的本质.md) 124 | - 话题 31 [_继承税_](../Chapter5/继承税.md) 125 | - 话题 11 [_可逆性_](./可逆性.md) 126 | - 话题 33 [_断开时间耦合_](../Chapter6/断开时间耦合.md) 127 | - 话题 34 [_共享状态不正确_](../Chapter6/共享状态不正确.md) 128 | - 话题 36 [_黑板_](../Chapter6/黑板.md) 129 | 130 | ## 挑战 131 | 132 | - 考虑一下具有图形用户界面的工具与在 shell 提示符下使用的小但可组合的命令行实用工具之间的区别。 哪个集合更正交,为什么? 哪一个更容易用于确切的目的? 哪一组更容易与其他工具结合起来应对新挑战? 哪一套更容易学习? 133 | 134 | - C++ 支持多重继承,而 Java 允许类实现多个接口。 Ruby 有 mixins。 使用这些工具对正交性有什么影响? 使用多个继承和多个接口在影响方面有区别吗? 使用委托和继承之间有区别吗? 135 | 136 | ## 练习 137 | 138 | ### 练习 1 (尽可能回答) 139 | 140 | 要求您一次读取文件行。 对于每一行,您必须将其拆分为多个字段。 以下哪个伪类定义集中可能更正交? 141 | 142 | ```elixir 143 | 144 | class Split1 { 145 | constructor(fileName) # opens the file for reading 146 | def readNextLine() # moves to the next line 147 | def getField(n) # returns nth field in current line 148 | } 149 | 150 | ``` 151 | 152 | 或者 153 | 154 | ```elixir 155 | class Split2 { 156 | constructor(line) # splits a line 157 | def getField(n) # returns nth field in current line 158 | } 159 | ``` 160 | 161 | ### 练习 2(尽可能回答) 162 | 163 | 面向对象语言和函数语言在正交性方面有什么区别?这些差异是语言本身固有的,还是仅仅是人们使用语言的方式? 164 | -------------------------------------------------------------------------------- /Chapter2/示踪子弹.md: -------------------------------------------------------------------------------- 1 | # 示踪子弹 2 | 3 | 4 | > _准备,开火,瞄准..._ 5 | > 6 | > _--- 匿名_ 7 | 8 | 我们在开发软件时经常谈论如何达到目标。我们实际上并没有在射击场发射任何东西,但它仍然是一个有用的和非常直观的比喻。特别值得一提的是,在一个复杂多变的世界里,考虑如何击中目标是很有意思的。 9 | 10 | 当然,答案取决于你瞄准设备的性质。对很多人来说,你只有一次瞄准的机会,然后看看你是否击中了靶心。但还有更好的办法。 11 | 您知道人们正在使用机枪射击的所有电影,电视节目和视频游戏吗? 在这些场景中,您经常会看到子弹的路径像是空中明亮的条纹。 这些条纹来自示踪子弹。 12 | 13 | 追踪器子弹与常规弹药间隔装载。当它们被发射时,它们的磷会被点燃,并留下从枪到它们击中的任何东西的烟火痕迹。如果追踪器击中目标,那么普通子弹也是。士兵们使用这些追踪弹来改进他们的目标:在实际情况下,这是实用的、实时的反馈。 14 | 15 | 同样的原则适用于项目,尤其是当您构建以前未构建的项目时。 我们使用 示踪子弹开发 这一术语来直观地说明在目标不断变化的实际条件下即时反馈的需求。 16 | 17 | 像枪手一样,您试图在黑暗中击中目标。 因为您的用户以前从未见过这样的系统,所以他们的要求可能很含糊。 由于您可能正在使用不熟悉的算法,技术,语言或库,因此您会遇到许多未知数。 而且由于项目需要花费时间才能完成,因此您可以保证在工作之前,您正在工作的环境将发生变化。 18 | 19 | 经典的应对方法是指定死亡制度。 产生大量的纸张,逐项列出每一项要求,将所有未知的事项都捆绑在一起,并限制环境。 20 | 21 | 使用航位推测法开火。 预先进行一次重大计算,然后射击并充满希望。 22 | 23 | 但是,实用程序员倾向于使用等同于示踪子弹的软件。 24 | 25 | --- 26 | 27 | ## 针对 Beta 读者的问题 28 | 我们认为示踪子弹的隐喻可以唯一地体现我们试图传达的信息:将反馈机制注入作品的实际背景。 29 | 30 | 但是,我们也意识到我们生活在敏感时期,在某些时代,某些主题被视为禁止进入,因为它们可能会引发困扰。在发生大量枪击事件后,我们想知道是否谈论枪支和子弹是否与这一点相抵触。 31 | 32 | 我们强烈希望它不会。世界充满了好事和坏事。我们不能简单地不提坏事,而把它们消除掉;对他们感到不安是一种务实的反应,并且需要采取行动。 33 | 34 | 那么,有什么问题吗?我们可以把它留在里面吗?如果没有,是否还有一个比喻能够像这个想法一样抓住这个想法? 35 | 请让我们知道:跳至 https://forms.gle/kfDvm5JSFhUozqXb9 进行的简短调查,并回答一个半问题。 36 | 37 | --- 38 | 39 | ## 黑暗中发光的代码 40 | 41 | 追踪子弹之所以起作用,是因为它们在与真实子弹相同的环境中和相同的约束下运行。 他们迅速到达目标,因此炮手会立即获得反馈。 从实际的角度来看,它们是一个相对便宜的解决方案。 42 | 43 | 为了在代码中获得相同的效果,我们正在寻找能够使我们从需求到最终系统的某些方面快速,可视和可重复的东西。 44 | 45 | 寻找重要的需求,即定义系统的需求。 寻找您有疑问和最大风险的领域。 然后确定您的开发优先级,以便这些是您编写的第一个区域。 46 | 47 | --- 48 | ## 提示 20 使用跟踪器子弹查找目标 49 | --- 50 | 51 | 实际上,考虑到当今项目设置的复杂性,加上大量的外部依赖关系和工具,追踪项目符号变得更加重要。 对于我们来说,追踪器的第一个项目符号就是简单地创建项目,添加一个 “hello world!”,并确保其可以编译并运行。 然后,我们在整个应用程序中寻找不确定的区域,并添加使其工作所需的框架。 52 | 53 | 看下图。 该系统具有五个体系结构层。 我们对它们的集成方式有所顾虑,因此我们寻找一种简单的功能,使我们可以一起使用它们。 对角线显示要素通过代码所经过的路径。 为了使它起作用,我们只需要在每一层中实现阴影区域即可:带有弯曲线条的内容将在以后完成。 54 | 55 | ![bullets](../assets/bullet.png) 56 | 57 | 我们曾经进行过一个复杂的客户-服务器数据库营销项目。 它的部分要求是能够指定和执行时间查询。 服务器是一系列关系数据库和专用数据库。 用随机语言A编写的客户端 UI 使用以不同语言编写的一组库来提供到服务器的接口。 用户的查询以类似 Lisp 的符号存储在服务器上,然后在执行之前就转换为优化的 SQL。 有许多未知数和许多不同的环境,而且没人能确定 UI 的行为方式。 58 | 59 | 这是使用跟踪代码的绝佳机会。 我们开发了前端框架,表示查询的库以及将存储的查询转换为特定于数据库的查询的结构。 然后,我们将它们放在一起并检查它是否有效。 对于最初的构建,我们所能做的就是提交一个查询,该查询列出了表中的所有行,但是事实证明,UI 可以与库通信,库可以序列化和反序列化查询,并且服务器可以从中生成 SQL。 结果。 在接下来的几个月中,我们逐步完善了此基本结构,通过并行扩展跟踪器代码的每个组件来添加新功能。 当 UI 添加新的查询类型时,库增加了,SQL生成变得更加复杂。 60 | 61 | 跟踪器代码不是一次性的:您将其编写为保持。 它包含任何生产代码所具有的所有错误检查,结构化,文档编制和自检。 它根本不能完全发挥作用。 但是,一旦在系统的各个组件之间实现了端到端连接,就可以检查与目标之间的距离,并在必要时进行调整。 达到目标后,添加功能就很容易。 62 | 63 | 示踪剂的开发与一个项目永远不会结束的想法是一致的:总是会有需要的更改和要添加的功能。 这是一种增量方法。 64 | 65 | 传统的替代方法是一种繁重的工程方法:将代码分为模块,然后在真空中进行编码。 将模块组合为子部件,然后再将其进一步组合,直到有一天您拥有完整的应用程序。 只有这样,整个应用程序才能呈现给用户并进行测试。 66 | 67 | 跟踪器代码方法具有许多优点: 68 | 69 | _用户可以尽早看到某些东西。_ 70 | 71 | 如果您已成功传达了您的操作(请参阅话题 51,[让用户满意](../Chapter9/让用户满意.md)),您的用户将知道他们看到的东西还不成熟。 他们不会因缺乏功能而感到失望; 他们会欣喜若狂,看到自己的系统取得了一些明显的进步。 随着项目的进展,他们也将做出贡献,增加他们的支持。 这些相同的用户很可能会告诉您每次迭代距离目标有多近。 72 | 73 | _开发人员建立了可以使用的结构。_ 74 | 75 | 最艰巨的是没有写任何东西的纸。 如果您已经弄清了应用程序的所有端到端交互,并已将它们体现在代码 中,那么您的团队将不需要花很多精力。 这使每个人都更有生产力,并鼓励保持一致。 76 | 77 | _您有一个集成平台。_ 78 | 79 | 由于系统是端对端连接的,因此您具有一个环境,一旦对新代码进行了单元测试,便可以向其中添加代码。 您无需每天进行大规模整合,而是每天(通常每天多次)进行整合。 每个新更改的影响更加明显,并且交互作用也更加有限,因此调试和测试变得更快,更准确。 80 | 81 | _你有事要示范。_ 82 | 83 | 项目赞助商和高层人士倾向于在最不方便的时间观看演示。 使用跟踪代码,您将总有一些东西可以显示出来。 84 | 85 | _您对进步有更好的感觉。_ 86 | 87 | 在跟踪代码开发中,开发人员一个接一个地解决用例。 当一个完成时,它们将移至下一个。 衡量性能并向用户演示进度要容易得多。 因为每个单独的开发都较小,所以避免创建每周报告 95% 完成的代码的整体块。 88 | 89 | ## 追赶者的子弹不一定总能命中目标 90 | 91 | 示踪剂项目符号显示您正在击中什么。这可能并不总是目标。然后,您可以调整目标,直到达到目标为止。这才是重点。 92 | 93 | 跟踪代码也是如此。您在不确定要去哪儿的情况下使用该技术。如果您前几次尝试失败,您都不会感到惊讶:用户说“这不是我的意思”,或者您所需的数据在需要时不可用,或者性能问题很可能出现。找出如何改变自己的方法,使它更接近目标,并感谢您使用了精益开发方法。一小段代码具有较低的惯性,可以轻松,快速地进行更改。与任何其他方法相比,您将能够收集有关您的应用程序的反馈并更快,更便宜地生成一个新的,更准确的版本。而且,由于每个主要应用程序组件都在跟踪代码中表示,因此用户可以确信他们所看到的内容基于现实,而不仅仅是纸张规范。 94 | 95 | ## 示踪代码与原型 96 | 97 | 您可能会认为,此跟踪代码概念仅是使用激进的名称进行原型设计。 它们是有区别的。 借助原型,您旨在探索最终系统的特定方面。 有了一个真正的原型,您就可以在尝试该概念时抛弃所有遇到的问题,并使用所学的课程正确地对其进行编码。 98 | 99 | 例如,假设您正在开发一个应用程序,可帮助托运人确定如何将奇特大小的盒子装进集装箱。除其他问题外,用户界面还必须直观,用于确定最佳包装的算法非常复杂。 100 | 101 | 您可以在UI工具中为最终用户创建用户界面原型。您编写的代码仅足以使界面响应用户的操作。一旦他们同意了布局,您就可以扔掉它并对其重新编码,这一次是使用目标语言在其背后进行业务逻辑处理。同样,您可能希望对执行实际打包的多种算法进行原型设计。您可以使用诸如Python之类的高级宽容语言编写功能测试,并以更接近机器的语言编写低级性能测试。无论如何,一旦您做出决定,就可以重新开始,并在最终环境中对算法进行编码,与现实世界相接。这是原型,非常有用。 102 | 103 | 跟踪代码方法解决了另一个问题。您需要知道整个应用程序如何挂在一起。您想向用户展示交互在实际中将如何工作,并且希望为您的开发人员提供一个架构框架,在该架构上悬挂代码。在这种情况下,您可以构造一个跟踪器,该跟踪器由一个简单的容器打包算法实现(例如,先到先得)和一个简单但有效的用户界面组成。将应用程序中的所有组件组合在一起后,便有了一个框架来显示用户和开发人员。随着时间的流逝,您将向该框架添加新功能,从而完成存根例程。但是框架保持完好无损,并且您知道系统将继续按照您的第一个跟踪程序代码完成时的方式进行操作。 104 | 105 | 这种区别非常重要,可以重复。 原型生成一次性代码。 跟踪器代码精简但完整,并且构成了最终系统框架的一部分。 将原型视为在发射单个示踪剂子弹之前进行的侦察和情报收集。 106 | 107 | ## 相关内容包括 108 | 109 | - 话题 27 [_别开过头了_](../Chapter4/别开过头了.md) 110 | - 话题 13 [_原型和便签_](./原型和便签.md) 111 | - 话题 40 [_重构_](../Chapter7/重构.md) 112 | - 话题 48 [_务实的团队_](../Chapter9/务实的团队.md) 113 | - 话题 50 [_实用入门套件_](../Chapter9/实用入门套件.md) 114 | - 话题 49 [_椰子不要切碎_](../Chapter9/椰子不要切碎.md) 115 | - 话题 51 [_让用户满意_](../Chapter9/让用户满意.md) 116 | -------------------------------------------------------------------------------- /Chapter2/评估.md: -------------------------------------------------------------------------------- 1 | # 评估 2 | 3 | 4 | 华盛顿特区的国会图书馆目前在线拥有大约75 TB的数字信息。快!通过1Gbps网络发送所有这些信息需要多长时间?一百万个名称和地址需要多少存储空间?压缩100Mb文本需要多长时间?交付项目需要多少个月? 5 | 6 | 一方面,这些都是毫无意义的问题,都是缺失的信息。但是,只要您能估计,就可以回答所有问题。而且,在进行估算的过程中,您将进一步了解程序所处的环境。 7 | 8 | 通过学习估算,并将此技能发展到对事物的大小具有直观感觉的程度,您将能够表现出明显的神奇能力来确定其可行性。当有人说“我们将通过网络连接将备份发送到S3”时,您将能够直观地知道这是否可行。在进行编码时,您将能够知道哪些子系统需要优化,哪些子系统可以单独放置。 9 | 10 | --- 11 | ## 提示 23 避免意外的估计 12 | --- 13 | 14 | 作为奖励,在这一部分的最后,我们将揭示一个正确的答案,无论何时有人要求你估计。 15 | 16 | ## 精确到什么程度? 17 | 18 | 在某种程度上,所有答案都是估计。 只是有些比其他的更准确。 因此,当有人要求您进行估算时,您必须问自己的第一个问题是答案的背景。 他们是否需要高精度,还是在寻找一个合适的数字? 19 | 20 | 估计有趣的事情之一是,您使用的单位会影响结果的解释。 如果您说某事大约需要130个工作日,那么人们会期望它很快就会到来。 但是,如果您说“哦,大约六个月”,那么他们知道从现在开始的五个到七个月之间的任何时间都将寻找它。 这两个数字表示相同的持续时间,但是“ 130天”可能意味着比您感觉的更高的准确性。 我们建议您按以下方式调整时间估算: 21 | 22 | | 持续时间 | 报价估算 | 23 | |:--:|:--:| 24 | |1-15 天|天| 25 | |3-6 周|周| 26 | |8-20 周|月| 27 | |20 周以上|在给出估计之前要认真思考| 28 | 29 | 因此,如果在完成所有必要的工作后,您决定一个项目将花费125个工作日(25周),那么您可能希望提供“大约六个月”的估计。 30 | 相同的概念适用于任何数量的估计:选择答案的单位以反映要传达的准确性。 31 | 32 | ## 估算值从何而来? 33 | 34 | 所有估计均基于问题的模型。 但是,在我们深入研究构建模型的技术之前,我们必须提到一个基本的估算技巧,该技巧总是能给出很好的答案:问一个已经做过的人。 在您过分致力于模型构建之前,请赶快过去曾经经历过类似情况的人。 看看他们的问题是如何解决的。 您不太可能会找到完全匹配的内容,但您会惊讶地发现有多少次可以成功借鉴他人的经验。 35 | 36 | ## 明白别人在问什么 37 | 38 | 估算工作的第一部分是建立对要求的理解。 除了上面讨论的准确性问题之外,您还需要掌握域的范围。 通常这在问题中是隐含的,但是您需要养成习惯,在开始猜测之前先考虑一下范围。 通常情况下,您选择的范围将构成您给出的答案的一部分:“假设没有交通事故并且车内有汽油,我应该在20分钟之内到达。” 39 | 40 | ## 建立系统模型 41 | 42 | 这是估计的有趣部分。从您对所问问题的理解中,构建一个粗糙而现成的准系统心智模型。如果您估计响应时间,则您的模型可能涉及服务器和某种到达流量。对于项目,模型可能是组织在开发过程中使用的步骤,以及有关如何实现系统的非常粗略的描述。 43 | 44 | 从长远来看,模型构建既有创意又有用。通常,建立模型的过程会导致发现表面上不明显的潜在模式和过程。您甚至可能想重新检查最初的问题:“您要求对X进行估算。但是,看起来像 X 的变体 Y 可以在大约一半的时间内完成,而您仅损失一项功能。” 45 | 46 | 建立模型会在估算过程中引入误差。这是不可避免的,也是有益的。您需要权衡模型的简单性和准确性。将模型上的工作量加倍可能只会使准确性略有提高。您的经验将告诉您何时停止精炼。 47 | 48 | ## 将模型分解为组件 49 | 50 | 一旦有了模型,就可以将其分解为组件。 您需要发现描述这些组件如何相互作用的数学规则。 有时,组件会贡献一个添加到结果中的值。 一些组件可能会提供乘数,而其他组件可能会更复杂(例如,模拟流量到达节点的组件)。 51 | 您会发现每个组件通常都具有影响其对整体模型的贡献方式的参数。 在此阶段,只需识别每个参数。 52 | 53 | ## 给每个参数一个值 54 | 55 | 分解完参数后,可以遍历并为每个参数分配一个值。您希望在此步骤中引入一些错误。诀窍是弄清楚哪些参数对结果影响最大,并集中精力使参数正确无误。通常,将其值添加到结果中的参数的重要性不如乘或除的参数重要。将线路速度提高一倍可能会使一小时内接收到的数据量增加一倍,而增加5ms的传输延迟将不会产生明显的影响。 56 | 57 | 您应该有合理的方法来计算这些关键参数。对于排队示例,您可能想要测量现有系统的实际交易到达率,或者找到一个类似的系统进行测量。同样,您可以使用本节中介绍的技术来衡量当前服务请求所花费的时间,或得出一个估算值。实际上,您经常会发现自己是根据其他估算值进行估算的。这是最大的错误将蔓延的地方。 58 | 59 | ## 计算答案 60 | 61 | 估计仅在最简单的情况下才会有一个答案。 您可能会高兴地说:“我可以在15分钟内步行五个跨镇街区。” 但是,随着系统变得越来越复杂,您将需要对冲您的答案。 运行多次计算,改变关键参数的值,直到确定哪些因素真正驱动了模型。 电子表格可以提供很大帮助。 然后根据这些参数提出您的答案。 “如果系统具有SSD和32GB内存,则响应时间约为四分之三秒,而具有16GB内存则为一秒钟。” (请注意,“四分之三秒”所传达的准确性与750ms有所不同。) 62 | 63 | 在计算阶段,您可能会开始获得看起来很奇怪的答案。 不要太快就解雇他们。 如果您的算术是正确的,那么您对问题或模型的理解可能是错误的。 这是有价值的信息。 64 | 65 | ## 跟踪您的估算能力 66 | 67 | 我们认为记录您的估算值是一个好主意,这样您就可以了解您的估算值。 如果总体估算涉及计算子估算,则也要跟踪这些估算。 通常,您会发现自己的估算值是非常不错的-实际上,过了一会儿,您就会对此有所期待。 68 | 69 | 如果估算结果有误,请不要耸耸肩走开。 找出为什么它与您的猜测不同。 也许您选择的参数与问题的实际情况不符。 也许您的模型是错误的。 不管是什么原因,请花一些时间来了解发生了什么。 如果这样做,您的下一个估计会更好。 70 | 71 | ## 估算项目进度 72 | 73 | 面对大型应用程序开发的复杂性和变数,估算的正常规则可能会崩溃。 我们发现,确定项目时间表的唯一途径通常是在同一个项目上获得经验。 如果您练习渐进式开发,请重复以下步骤,这不必是矛盾的。 74 | 75 | - 检查要求 76 | - 分析风险 77 | - 设计,实施,集成 78 | - 验证用户身份 79 | 80 | 最初,您可能只不清楚需要多少次迭代,或者可能需要多长时间。 有些方法要求您将其确定为初始计划的一部分,但是对于除最琐碎的项目之外的所有项目,这都是一个错误。 除非您使用相同的团队和相同的技术来执行与上一个应用程序类似的应用程序,否则您只是在猜测。 81 | 82 | 因此,您完成了对初始功能的编码和测试,并将其标记为第一次迭代的结束。 基于该经验,您可以对迭代次数以及每个迭代中可以包含的内容进行优化。 每次的改进都越来越好,对进度的信心也随之增加。 这种估算通常是在每个迭代周期结束时,在小组审核期间进行的。 83 | 84 | --- 85 | ## 提示 24 用代码迭代计划表 86 | --- 87 | 88 | 这在管理人员中可能并不流行,他们通常在项目开始之前就需要一个固定的数字。 您必须帮助他们了解团队,他们的生产力和环境将决定时间表。 通过对此进行形式化并在每次迭代中完善计划,您将为他们提供最准确的计划估算值。 89 | 90 | ## 当被要求估价时该说什么 91 | 92 | 您说:“我会尽快回复您。” 93 | 如果您减慢流程速度并花一些时间来完成我们在本节中介绍的步骤,则几乎总是可以获得更好的结果。 咖啡机提供的估算值(像咖啡一样)会再次困扰您。 94 | 95 | ## 相关内容包括 96 | 97 | - 话题 39 [算法速度](../Chapter7/算法速度.md) 98 | - 话题 7 [沟通](../Chapter1/沟通.md) 99 | 100 | ## 挑战 101 | 102 | - 开始记录您的估算值。 对于每一个,跟踪您原来的准确性。 如果您的错误大于50%,请尝试找出估计错误的地方。 103 | 104 | ## 练习 105 | 106 | ### 练习 9(尽可能回答) 107 | 108 | 您会被问到“哪个带宽更高:一个1Gbps的网络连接或一个人在口袋里装满1TB存储设备的两台计算机之间行走?”您将对答案施加什么限制以确保响应范围 是正确的? (例如,您可能会说访问存储设备所花费的时间被忽略了。) 109 | 110 | ### 练习 10(尽可能回答) 111 | 112 | 那么,哪个具有更高的带宽? 113 | -------------------------------------------------------------------------------- /Chapter3/shell.md: -------------------------------------------------------------------------------- 1 | # Shell 游戏 2 | 3 | 4 | 每个木工都需要一个良好,坚固,可靠的工作台,以便在工作时将工件固定在合适的高度。 工作台成为木工车间的中心,制造商一次又一次地将其恢复为成型的形状。 5 | 6 | 对于处理文本文件的程序员而言,该工作台是命令行。 在 shell 提示符下,您可以调用管道的全部功能,并使用管道以原始开发人员梦寐以求的方式将它们组合在一起。 在 shell 里面,您可以启动应用程序,调试器,浏览器,编辑器和实用程序。 您可以搜索文件,查询系统状态以及过滤输出。 通过对 shell 进行编程,可以为经常执行的活动构建复杂的宏命令。 7 | 8 | 对于使用GUI界面和集成开发环境(IDE)的程序员来说,这似乎是一个极端的位置。 毕竟,您不能通过指向和单击来完成所有工作吗? 9 | 10 | 简单的答案是“不。” GUI界面很棒,对于某些简单的操作,它们更快,更方便。移动文件,阅读电子邮件和键入字母都是您在图形环境中可能要做的所有事情。但是,如果您使用 GUI 进行所有工作,那么您将失去环境的全部功能。您将无法自动执行常见任务,也无法使用所有可用工具的强大功能。而且您将无法组合工具来创建自定义的宏工具。图形用户界面的好处是 WYSIWYG - 即所见即所得。缺点是 WYSIAYG,即所见即所有。 11 | 12 | GUI 环境通常仅限于其设计人员想要的功能。如果您需要超越设计者提供的模型,那么通常就不走运了-而且,通常情况下,您确实需要超越模型。务实的程序员不仅会削减代码,开发对象模型,编写文档或自动化构建过程,我们还会做所有这些事情。任何一种工具的范围通常都限于该工具应执行的任务。例如,假设您需要在IDE中集成一个代码预处理器(以实现契约设计,多处理编译指示等)。除非IDE的设计者明确为此功能提供了挂钩,否则您将无法做到。 13 | 14 | --- 15 | ## 提示 26 使用命令 shell 的力量 16 | --- 17 | 18 | 熟悉 shell 后,您会发现工作效率飞升。 是否需要创建由Java代码显式导入的所有唯一软件包名称的列表? 以下内容将其存储在名为“列表”的文件中: 19 | 20 | ```shell 21 | grep '^import ' *.java | 22 | sed -e's/.*import *//' -e's/;.*$//' | 23 | sort -u >list 24 | ``` 25 | 26 | 如果你还没有花了很多时间来研究你所使用的系统shell命令的能力,这样就可能出现艰巨。 但是,投入一些精力来熟悉您的外壳,事情很快就会开始发生。 试一试您的命令行shell,您会惊奇地发现它使您的工作效率更高。 27 | 28 | ## 你自己的 Shell 29 | 30 | 与木工将自定义工作区的方式相同,开发人员应自定义其shell。 这通常还涉及更改您使用的终端程序的配置。常见 31 | 32 | 更改包括: 33 | 34 | - 设置颜色主题。您可能需要花费许多小时来尝试各种特定主题的在线可用主题。 35 | 36 | - 配置提示。可以配置提示框,告诉您准备输入命令的外壳,可以将其配置为显示几乎所有您想要的信息(以及一些您不想要的东西)。个人喜好无处不在:我们倾向于喜欢简单的提示,它们会缩短当前目录名称和版本控制状态以及时间。 37 | 38 | - 别名和 shell 函数。通过将常用命令转换为简单别名来简化工作流程。也许您定期更新您的Linux机器,但永远不会记住您是更新和升级,还是升级和更新。创建一个别名: 39 | 40 | alias apt-up = 'sudo apt-get update && sudo apt-get upgrade' 41 | 42 | 也许是您一次不小心使用 rm 命令删除了文件,但经常是一次。编写一个别名,以便以后始终提示: 43 | 44 | alias rm ='rm -iv' 45 | 46 | - 命令补全。大多数 Shell 都会补全命令和文件的名称:输入前几个字符,单击Tab,然后将其填满。但是,您可以更进一步,将 shell 配置为识别您输入的命令并提供特定于上下文的补全。有些甚至根据当前目录自定义完成。 47 | 48 | 您将花费大量时间住在其中一个外壳中。像一只寄居蟹一样,成为自己的家。 49 | 50 | ## 相关内容包括 51 | - 话题 16 [_纯文本的力量_](./纯文本的力量.md) 52 | - 话题 13 [_原型和便签_](../Chapter2/原型和便签.md) 53 | - 话题 21 [_文本处理_](./文本处理.md) 54 | - 话题 50 [_实用入门套件_](../Chapter9/实用入门套件.md) 55 | - 话题 30 [_转换编程_](../Chapter5/转换编程.md) 56 | 57 | ## 挑战 58 | 59 | - 您当前是否正在 GUI 中手动执行某些操作? 您是否曾经将涉及多个单独“单击此按钮”,“选择此项目”步骤的指示传递给同事? 这些可以自动化吗? 60 | 61 | - 每当您迁移到新环境时,请务必找出可用的 shell 。 看看是否可以带上当前的 shell 。 62 | 63 | - 研究当前 shell 的替代方案。 如果遇到问题,您的 shell 无法解决,请查看替代 shell 是否会更好。 64 | -------------------------------------------------------------------------------- /Chapter3/基本工具.md: -------------------------------------------------------------------------------- 1 | # 基本工具 2 | 3 | 4 | 每个制造商都以一套基本的优质工具开始他们的旅程。木工可能需要尺子,量规,几把锯,一些好刨子,细凿子,钻和牙套,木槌和夹具。这些工具将经过精心挑选,经久耐用,将执行特定的工作,并且与其他工具几乎没有重叠,而且也许最重要的是,在发芽的木工手中会感觉很不错。 5 | 6 | 然后开始学习和适应的过程。每个工具将具有其自己的个性和怪癖,并且将需要其自身的特殊处理。每个都必须以独特的方式进行锐化,或者保持不变。随着时间的推移,每种工具都会根据使用情况进行磨损,直到握把看起来像木工手的手模,并且切割表面与工具的固定角度完美对齐。在这一点上,这些工具已成为制造商大脑到成品的管道,它们已经成为了他们的双手的延伸。随着时间的流逝,木工将添加新工具,例如饼干切割机,激光制导的斜切锯,燕尾夹具-所有这些都是很棒的技术。但是您可以打赌,他们手中的那些原始工具中最快乐的一种是,当飞机滑过树林时,它会发出歌声。 7 | 8 | 工具可以扩大您的才能。您的工具越好,您对工具的了解也越多,您的工作效率就越高。从一组基本的通用工具开始。随着经验的积累,以及遇到特殊要求,您将添加到此基本设置中。像制造商一样,期望定期添加到您的工具箱中。始终在寻找更好的做事方法。如果遇到无法使用当前工具的情况,请记录一下内容,寻找可以有所帮助的其他功能或功能更强大的产品。让需要推动您的收购。 9 | 10 | 许多新程序员都犯了采用单一动力工具(例如特定的集成开发环境(IDE))的错误,并且从不离开其舒适的界面。“这确实是一个错误。您需要超出IDE所施加的限制。唯一的方法是保持基本工具的锋利并准备使用。 11 | 12 | 在本章中,我们将讨论在您自己的基本工具箱中进行投资。就像对工具的任何精彩讨论一样,我们将从话题的原始资料(话题 16 [_纯文本的力量_](./纯文本的力量.md) 开始。从那里,我们将移至工作台,或移至计算机。您如何使用计算机来充分利用所使用的工具?我们将在 话题 17 [_Shell 游戏_](./shell.md) 中进行讨论。现在我们有了资料和工作台,接下来我们将转向您可能会比其他任何人使用的工具(您的编辑器)更多的工具。在话题 18 [_强大的编辑_](./强大的编辑.md) 中,我们将建议提高您效率的方法。 13 | 14 | 为了确保我们永远不会失去任何宝贵的工作,我们应该始终使用话题 19 [_版本控制_](./版本控制.md) 系统-甚至用于个人物品,例如食谱或便笺。而且,由于墨菲毕竟是一个真正的乐观主义者,因此,除非您精通话题 20 [_调试_](./调试.md),否则您就无法成为一名优秀的程序员。 15 | 16 | 您需要一些胶水才能将大部分魔术粘合在一起。 我们将在话题 21 [_文本处理_](./文本处理.md) 中讨论一些可能性 17 | 18 | 最后,最淡的墨水仍然比最好的记忆要好。 就像我们在话题 22 [_工程日记_](./工程日记.md) 中描述的那样,跟踪您的想法和历史。 19 | 20 | 花时间学习如何使用这些工具,有时您会惊讶地发现手指在键盘上移动,从而在没有意识的情况下操纵文本。 这些工具将成为您的双手的延伸。 21 | -------------------------------------------------------------------------------- /Chapter3/工程日记.md: -------------------------------------------------------------------------------- 1 | # 工程日记 2 | 3 | 4 | Dave 曾经在一家小型计算机制造商工作,这意味着有时候他和电子工程师,或是机械工程师一起工作。 5 | 6 | 他们中的许多人走路时也会带着纸质笔记本,通常是用一支笔塞在书脊上。每当我们谈话时,他们都会突然打开笔记本并随意涂鸦。 7 | 8 | 最终,Dave 问了一个明显的问题。事后证明他们接受过培训,即保留一本工程日记,这是日记记录了他们的工作,所学到的东西,想法草图,电表读数:基本上与他们的工作有关。笔记本写满后,他们会在书脊上写下日期范围,然后贴在前一天日记本旁边的书架上。可能是一场激烈的竞争,因为他们的书籍占据了最多的货架空间。 9 | 10 | 我们使用记事本在会议上做笔记,记下我们正在做的事情,在调试时记录变量值,在我们放置东西的地方留下提醒,记录荒谬的想法,有时甚至是涂鸦。 11 | 12 | 日记本具有三个主要优点: 13 | 14 | - 它比内存更可靠。 人们可能会问“关于内存问题,上周您说的那家公司叫什么名字?” 您可以翻转一页左右,然后给他们命名和编号。 15 | 16 | - 它为您提供了一个存储与即将完成的任务不立即相关的想法的地方。 这样一来,您就可以继续专注于自己所做的事情,并且知道绝妙的主意不会被遗忘。 17 | 18 | - 它充当一种橡皮鸭([_这里提到过_](./调试.md))。 当您停止写下一些东西时,您的大脑可能会切换齿轮,就像在和某人说话一样,这是一个很好的反思机会。 您可能会开始做笔记,然后突然意识到所做的事情(笔记的主题)完全是错误的。 19 | 20 | 还有一个额外的好处。 时不时地您可以回头看看自己在做什么哦! 等再过几年,想一下人员,项目以及糟糕的衣服和发型。 21 | 22 | 因此,请尝试保留一本工程日记。 使用纸张,而不是文件或 Wiki:与打字相比,写作有一些特别之处。 给它一个月,看看是否有任何好处。 23 | 24 | 如果没什么,当您富有和成名时,这将使撰写回忆录更加容易。 25 | 26 | ## 相关内容包括 27 | 28 | - 话题 37 [_聆听你的蜥蜴脑_](../Chapter7/聆听你的蜥蜴脑.md) 29 | - 话题 6 [_你的知识组合_](../Chapter1/你的知识组合.md) 30 | -------------------------------------------------------------------------------- /Chapter3/强大的编辑.md: -------------------------------------------------------------------------------- 1 | # 强大的编辑 2 | 3 | 4 | 我们之前已经讨论过工具是您的延伸。 嗯,这比其他软件工具更适用于编辑器。 您需要能够尽可能轻松地操作文本,因为文本是编程的基本原料。 5 | 6 | 在本书的第一版中,我们建议使用单个编辑器来处理所有内容:代码,文档,备忘录,系统管理等。 我们已经稍微简化了该位置。 我们很高兴您可以使用任意数量的编辑器。 我们希望您能努力做到流利。 7 | 8 | --- 9 | ## 提示 27 使编辑流畅 10 | --- 11 | 12 | 为什么这很重要? 我们是说您会节省很多时间吗? 实际上是:在一年的时间里,如果您使编辑效率仅提高4%并且每周编辑20个小时,则实际上可以多获得一周的时间。 13 | 14 | 但这不是真正的好处。 不,主要的好处是,通过变得流利,您不再需要考虑编辑的机制。 思考某事并使其出现在编辑器缓冲区中之间的距离会下降。 您的想法将流淌,您的编程将受益。 (如果您曾经教过某人驾驶汽车,那么您将了解必须考虑他们采取的每项行动的人与本能地控制汽车的经验丰富的驾驶员之间的区别。) 15 | 16 | ## “流利”是什么意思? 17 | 18 | 什么算流利。以下是挑战列表: 19 | 20 | - 编辑文本时,移动并按字符,单词,行和段落进行选择。 21 | - 编辑代码时,请移动各种语法单元(匹配定界符,功能,模块等)。 22 | - 更改后重新缩进代码。 23 | - 使用单个命令注释和取消注释代码块。 24 | - 撤消和重做更改。 25 | - 将编辑器窗口分成多个面板,然后在它们之间导航。 26 | - 导航到特定的行号。 27 | - 对选定的行进行排序。 28 | - 搜索字符串和正则表达式,然后重复以前的搜索。 29 | - 根据选择或模式匹配临时创建多个光标,然后并行编辑每个光标的文本。 30 | - 显示当前项目中的编译错误。 31 | - 运行当前项目的测试。 32 | 33 | 您无需使用鼠标/触控板就能完成所有这些操作吗? 34 | 35 | 您可能会说您当前的编辑器无法执行某些操作。 也许该切换了? 36 | 37 | ## 走向流畅 38 | 39 | 我们怀疑只有少数人知道任何特定功能强大的编辑器中的所有命令。 我们也不希望您这么做。 相反,我们建议一种更实用的方法:学习使您的生活更轻松的命令。 40 | 41 | 配方很简单。 42 | 43 | 首先,在编辑时看着自己。 每当您发现自己做重复的事情时,就养成思考“必须有更好的方法”的习惯。 然后找到它。 44 | 45 | 发现新的有用功能后,现在需要将其安装到您的肌肉记忆中,因此您无需考虑即可使用它。 我们知道做到这一点的唯一方法是重复。 有意识地寻找机会,最好每天使用许多次您的新超能力。 大约一周后,您会发现自己无需考虑就可以使用它。 46 | 47 | ## 扩大编辑器 48 | 49 | 大多数功能强大的代码编辑器都是围绕基本核心构建的,然后通过扩展对其进行扩充。 编辑器附带了许多工具,以后可以添加其他工具。 50 | 51 | 当您遇到使用的编辑器的明显限制时,请搜索可以完成此工作的扩展程序。 很有可能您不是一个人需要这种功能,如果幸运的话,其他人会发布他们的解决方案。 52 | 53 | 进一步采取此步骤。 深入研究编辑器的扩展语言。 弄清楚如何使用它来自动化您所做的一些重复性操作。 通常,您只需要一行或两行代码。 54 | 55 | 有时您可能会更进一步,并且发现自己编写了完整的扩展程序。 如果是这样,请发布它:如果您需要它,其他人也会这样做。 56 | 57 | ## 相关内容包括 58 | 59 | - 话题七 [_沟通_](../Chapter1/沟通.md) 60 | 61 | ## 挑战 62 | 63 | - 没有更多的自动重复。 64 | 65 | 每个人都这样做:您需要删除键入的最后一个单词,因此您要在退格键上按下并等待自动重复开始。事实上,我们打赌您的大脑“已经做了那么多的操作,因此您可以准确地判断何时 释放钥匙。 66 | 67 | 因此,请关闭自动重复,然后学习按字符,单词,行和块来移动,选择和删除的键序列 68 | 69 | - 这会很疼。 70 | 71 | 丢失鼠标/触控板。 在整个一周的时间里,仅使用键盘即可进行编辑。 您会发现很多东西,而这些东西如果没有指向和点击就无法做,所以现在该学习了。 记下您所学的按键顺序(我们建议您放学并用铅笔和纸做笔记)。 72 | 73 | 您将遭受几天的生产力打击。 但是,当您学会做事而不用动手离开原位时,您会发现您的编辑变得比以往更快,更流畅。 74 | 75 | - 寻找整合。 在撰写本章时,Dave 想知道他是否可以在编辑器缓冲区中预览最终布局(PDF文件)。 一次下载后,版式与原始文本并排放置,全部在编辑器中。 保留您想带入编辑器的清单,然后寻找它们。 76 | 77 | - 如果您找不到能满足您需求的插件或扩展程序,请编写一个。 安迪(Andy)喜欢为自己喜欢的编辑器制作自定义Wiki插件。 如果找不到,就造一个! 78 | -------------------------------------------------------------------------------- /Chapter3/文本处理.md: -------------------------------------------------------------------------------- 1 | # 文本处理 2 | 3 | 4 | 实用主义程序员处理文本的方式与木工塑造木材的方式相同。在前面的小节中,我们讨论了一些我们使用的特定工具shell、编辑器和调试器。这些工具类似于木工的凿子、锯子和刨子,专门用来做好一两项工作。但是,有时我们需要执行一些基本工具集不易处理的转换。我们需要一个通用的文本处理工具。 5 | 6 | 文本处理语言对于编程来说就像槽刨对于木工一样。他们吵闹,凌乱,有点野蛮。用它们犯错误,整件东西都会被毁掉。有些人发誓他们在工具箱里没有位置。但在右手边,槽刨和文本处理语言都可以非常强大和通用。你可以很快地修剪成形状,做关节,雕刻。使用得当,这些工具有惊人的技巧和微妙之处。但他们需要时间来掌握。 7 | 8 | 幸运的是,有很多优秀的文本处理语言。Unix 开发人员(这里包括 macOS 用户)通常喜欢使用命令 shell 的强大功能,并使用 awk 和 sed 等工具进行增强。喜欢更结构化工具的人可能更喜欢 Perl、Python 或 Ruby 等语言。 9 | 10 | 这些语言是重要的使能技术。使用它们,你可以快速破解实用程序和原型创意工作,使用传统语言可能需要5到10倍的时间。这个倍增因子对于我们所做的实验来说至关重要。花30分钟尝试一个疯狂的想法比花5个小时要好得多。花一天时间自动化一个项目的重要组成部分是可以接受的;花一周时间可能不是。在他们的《编程实践》(The Practice of Programming)一书中,Kernighan 和 Pike 用五种不同的语言构建了同一个程序。Perl 版本是最短的(17行,而 C 版本是150行)。使用 Perl,您可以操作文本、与程序交互、通过网络交谈、驱动网页、执行任意精度的算术运算,以及编写看起来像斯努比咒骂的程序。 11 | 12 | --- 13 | ## 提示 35 学习一门文本处理语言 14 | --- 15 | 16 | 为了展示文本处理语言的广泛适用性,以下是我们使用 Ruby 和 Python 所做的一些与本手册的创建有关的示例。 17 | 18 | ### _建立书_ 19 | 20 | 实用书架的构建系统是用 Ruby 编写的。作者,编辑,布局人员和支持人员使用 Rake 任务来协调 PDF 和电子书的构建。 21 | 22 | ### _代码包含和突出显示_ 23 | 24 | 我们认为重要的是,书中介绍的任何代码都应先经过测试。本书中的大多数代码都包含在内。但是,使用 DRY 原理(请参阅话题 9 [_重复的恶魔_](../Chapter2/重复的恶魔.md)),我们不想将经过测试的程序中的代码行复制并粘贴到本书中。那将意味着代码是重复的,实际上保证了我们在更改相应程序后忘记更新示例。对于某些示例,我们也不想让您感到厌倦所有使示例得以编译和运行的框架代码。我们转向 Ruby。在对书籍进行格式设置时,将调用一个相对简单的脚本-它提取源文件的命名段,进行语法突出显示,然后将结果转换为我们使用的排版语言。 25 | 26 | ### _网站更新_ 27 | 28 | 我们有一个简单的脚本,可以部分编译书籍,提取目录,然后将其上传到我们网站上的书籍页面。我们还有一个脚本,可提取书中的各个部分并将其作为样本上传。 29 | 30 | ### _包括方程式_ 31 | 32 | 有一个 Python 脚本可将 LaTeX 数学标记转换为格式正确的文本。 33 | 34 | ### _索引生成_ 35 | 36 | 大多数索引都是作为单独的文档创建的(如果文档发生更改,这将使维护变得很困难)。我们的标记在文本本身中,并且 Ruby 脚本整理和格式化条目。等等。实际上,实用书架是围绕文本处理构建的。而且,如果您遵循我们的建议以纯文本形式保存内容,那么使用这些语言来操纵该文本将带来很多好处。 37 | 38 | ## 相关话题包括 39 | - 话题 17 [_shell 游戏_](./shell.md) 40 | - 话题 16 [_纯文本的力量_](./纯文本的力量.md) 41 | 42 | ## 练习 43 | 44 | ### 练习 11 45 | 46 | 您正在重写一个将 YAML 用作配置语言的应用程序。您的公司现在已经对 JSON 进行了标准化,因此您有一堆需要转换为 .JSON 的 .yaml 文件。编写一个脚本,获取一个目录并将每个 .yaml 文件转换为相应的 .json 文件(因此database.yaml 变为 database.json,内容是有效的 json)。 47 | -------------------------------------------------------------------------------- /Chapter3/版本控制.md: -------------------------------------------------------------------------------- 1 | # 版本控制 2 | 3 | 4 | > _进步,远不在于变化,而在于坚持。那些不记得过去的人注定要重蹈覆辙。_ 5 | > 6 | > -- _乔治·桑塔亚纳_,《理性生活》 7 | 8 | 我们在用户界面中寻找的一个重要的东西是 撤销 键 —— 一个可以原谅我们错误的按钮。如果环境支持多个级别的撤消和重做,这样您就可以从几分钟前发生的事情中恢复过来。 9 | 10 | 但是如果这个错误发生在上周,并且从那以后你已经打开和关闭了十次你的电脑怎么办?好吧,这是使用版本控制系统(VCS)的众多好处之一:它是一个巨大的 撤销 键 —— 一个项目范围内的时间机器,可以让你回到上周那些平静的日子,当代码真正编译和运行时。 11 | 12 | 对很多人来说,这是他们使用 VCS 的极限。这些人错过了一个更大的协作、部署管道、问题跟踪和一般团队交互的世界。 13 | 14 | 所以让我们来看看VCS,首先是一个变更存储库,然后是您的团队及其代码的中心会议场所。 15 | 16 | --- 17 | ## 共享目录不是版本控制 18 | 19 | 我们偶尔还会遇到一些团队,他们通过网络共享他们的项目源文件:要么在内部共享,要么使用某种云存储。 20 | 21 | 这是不可行的。 22 | 23 | 这样做的团队不断地扰乱彼此的工作,失去变化,打破构建,并进入停车场拳击。这就像用共享数据编写并发代码,而没有同步机制。使用版本控制。 24 | 但还有更多!有些人使用版本控制,并保留存储库(包含网络或云驱动器上的更改历史记录的位置)。他们认为这是两个世界中最好的:存储库在任何地方都可以访问,而且(在云存储的情况下)是在异地备份的。 25 | 26 | 结果更糟,你有失去一切的危险。存储库本身通常是一组相互作用的文件和目录。如果两个人同时提交更改,就不知道会造成多大的损失。没有人喜欢看到开发商哭泣。 27 | 28 | --- 29 | 30 | ## 从源头开始 31 | 32 | 版本控制系统会跟踪您在源代码和文档中所做的每个更改。使用正确配置的源代码控制系统,您始终可以返回到以前版本的软件。 33 | 34 | 但是,版本控制系统所做的远远不止是纠正错误。一个好的 VCS 会让你跟踪变化,回答诸如:谁在这行代码中做了变化?现在的版本和上周的版本有什么区别?我们在这个版本中更改了多少行代码?哪些文件最常被更改?这类信息对于缺陷跟踪、审计、性能和质量都是非常宝贵的。 35 | 36 | VCS还可以让您识别软件的发行版本。一旦确定,您将始终能够返回并重新生成版本,而不受以后可能发生的更改的影响。 37 | 38 | 版本控制系统可能会将它们维护的文件保存在一个中央存储库中,这是一个很好的归档候选。 39 | 40 | 最后,版本控制系统允许两个或多个用户同时处理同一组文件,甚至在同一个文件中进行并发更改。然后,当文件发送回存储库时,系统管理这些更改的合并。尽管看起来有风险,但这种系统在各种规模的项目中都能很好地工作。 41 | 42 | ## 始终使用版本控制 43 | 44 | 总是使用。即使你是一个一周项目的单人团队。即使它是一个“扔掉”的原型。即使你正在处理的东西不是源代码。确保所有内容都在版本控制文档、电话号码列表、供应商备忘录、makefile、构建和发布过程中,以及整理日志文件的小shell脚本中。我们通常对我们键入的所有内容(包括本书的文本)使用版本控制。即使我们不是在做一个项目,我们的日常工作也会在一个存储库中得到保护。 45 | 46 | ## 分支出来 47 | 48 | 版本控制系统不仅保留您的项目的单一历史记录。它们最强大和有用的功能之一就是让您将开发孤岛隔离为称为分支的事物的方式。您可以在项目历史记录中的任何时候创建一个分支,并且您在该分支中所做的任何工作都将与所有其他分支隔离。在将来的某个时候,您可以将正在处理的分支合并回另一个分支,因此目标分支现在包含您在分支中所做的更改。多个人甚至可以在一个分支上工作:在某种程度上,分支就像小克隆项目。 49 | 50 | 分支机构的好处之一就是它们给您带来的隔离感。如果您在一个分支中开发功能 A,而团队成员在另一个分支中开发功能 B,则您不会互相干扰。 51 | 第二个好处可能令人惊讶,那就是分支机构通常是团队项目工作流程的核心。 52 | 53 | 这就是事情变得有些混乱的地方。版本控制分支和测试组织有一个共同点:他们都有成千上万的人告诉您应该如何做。该建议在很大程度上没有意义,因为他们真正说的是“这对我有用。 54 | 55 | 因此,在项目中使用版本控制,如果遇到工作流问题,请搜索可能的解决方案。记住,当你获得经验时,要回顾和调整你正在做的事情。 56 | 57 | --- 58 | ## 一个思想实验 59 | 将整杯茶(英式早餐,加一点牛奶)洒在笔记本电脑键盘上。 将机器带到聪明人酒吧,让他们抬头皱眉。 买一台新电脑。 拿回家 60 | 61 | 使该机器恢复到您最初举起那具命运的杯子时的状态需要多长时间? 所有的 SSH 密钥,编辑器配置,shell 设置,已安装的应用程序等等? 这发生在我们当中的一个人身上。 62 | 63 | 定义原始计算机的配置和用法的几乎所有内容都存储在版本控制中,包括: 64 | 65 | * 所有用户首选项和点文件 66 | 67 | * 编辑器配置 68 | 69 | * 使用 Homebrew 安装的软件列表 70 | 71 | * 用于配置应用程序的 Ansible 脚本 72 | 73 | * 当前所有项目 74 | 75 | 机器在下午结束时恢复了。 76 | 77 | --- 78 | 79 | ## 版本控制作为项目中心 80 | 81 | 尽管版本控制在个人项目上非常有用,但在与团队合作时确实会发挥作用。而且,这些价值中的大部分来自您托管资源库的方式。 82 | 83 | 现在,许多版本控制系统不需要任何托管。它们是完全分散的,每个开发人员都在对等基础上进行合作。但是即使使用这些系统,也值得考虑建立一个中央存储库,因为一旦这样做,您就可以利用大量的集成来简化项目流程。 84 | 85 | 许多存储库系统是开源的,因此您可以在公司中安装和运行它们。但这不是您真正的业务范围,因此我们建议大多数人托管第三方。查找以下功能: 86 | 87 | - 良好的安全性和访问控制 88 | 89 | - 直观的用户界面 90 | 91 | - 也可以从命令行执行所有操作(因为您可能需要使其自动化) 92 | 93 | - 自动化构建和测试 94 | 95 | - 对分支合并的良好支持(有时称为拉取请求) 96 | 97 | - 问题管理(理想情况下已集成到提交和合并中,因此您可以保留指标) 98 | 99 | - 良好的报告(类似看板的未决问题和任务的显示非常有用) 100 | 101 | - 良好的团队沟通:有关更改的电子邮件或其他通知,Wiki等 102 | 103 | 许多团队都配置了他们的VCS,以便推送到特定分支将自动构建系统,运行测试,以及如果成功将新代码部署到生产中。 104 | 105 | 听起来吓人吗?当您意识到正在使用版本控制时,情况并非如此。您可以随时将其回滚。 106 | 107 | ## 相关内容包括 108 | 109 | - 话题 11 [_可逆性_](../Chapter2/可逆性.md) 110 | - 话题 48 [_务实的团队_](../Chapter9/务实的团队.md) 111 | - 话题 50 [_实用入门套件_](../Chapter9/实用入门套件.md) 112 | 113 | ## 挑战 114 | 115 | - 知道可以使用VCS回滚到任何以前的状态是一回事,但是您实际上可以做到吗?您知道正确执行命令的命令吗?现在就学习它们,而不是在灾难袭来且您承受巨大压力时学习。 116 | 117 | - 花一些时间考虑在发生灾难时恢复自己的笔记本电脑环境。您需要恢复什么?许多东西只是文本文件。如果它们不在VCS中(由笔记本电脑托管),请找到一种添加方式。然后考虑其他因素:已安装的应用程序,系统配置等。您如何在文本文件中表达所有这些内容,以便也可以保存。 118 | 119 | 取得一些进展后,一个有趣的实验是找到不再使用的旧计算机,并查看是否可以使用新系统进行设置。 120 | 121 | - 有意识地探索当前未使用的VCS和托管服务提供商的功能。如果您的团队没有使用功能分支,请尝试引入它们。拉/合并请求也是如此。持续集成。建立管道。甚至持续部署。还要研究团队沟通工具:Wiki,看板等。 122 | 123 | 您不必使用任何一个。但是您确实需要知道它的作用,以便您做出决定。 124 | 125 | - 也对非项目内容使用版本控制。 126 | -------------------------------------------------------------------------------- /Chapter3/纯文本的力量.md: -------------------------------------------------------------------------------- 1 | # 纯文本的力量 2 | 3 | 4 | 作为务实的程序员,我们的基础材料不是木头或铁,而是知识。我们收集需求作为知识,然后在设计、实现、测试和文档中表达这些知识。我们相信,保存知识的最佳格式是纯文本。有了纯文本,我们就可以使用我们可以使用的几乎所有工具,手动和编程地操作知识。 5 | 6 | 大多数二进制格式的问题在于理解数据所需的上下文与数据本身是分离的。您是在人为地将数据与其含义分离。数据也可能被加密;如果没有应用程序逻辑来解析它,它就毫无意义。但是,使用纯文本,您可以实现独立于创建它的应用程序的自描述数据流。 7 | 8 | ## 什么是纯文本 9 | 10 | 纯文本 是由可打印字符组成的,其形式是传递信息的。它可以像购物单一样简单: 11 | 12 | - 牛奶 13 | - 生菜 14 | - 咖啡 15 | 16 | 或者像这本书的来源一样复杂(是的,它是纯文本的,这让出版商非常懊恼,他们希望我们使用文字处理器)。 17 | 18 | 信息部分很重要。以下文字不是有用的纯文本: 19 | 20 | hlj;uijn bfjxrrctvh jkni'pio6p7gu;vh bjxrdi5rgvhj 21 | 22 | 这种也不是: 23 | 24 | Field19=467abe 25 | 26 | 读者不知道 467abe 的意义可能是什么。我们希望我们的纯文本能够被人类理解。 27 | 28 | --- 29 | ## 提示 25 以纯文本形式保留知识 30 | --- 31 | 32 | ## 文本的力量 33 | 34 | 纯文本并不意味着文本是非结构化的; HTML,JSON,YAML等都是纯文本。 网络上的大多数基本协议也是如此,例如HTTP,SMTP,IMAP等。 这是有一些很好的理由的。 35 | 36 | - 避免过时的保险 37 | - 利用现有工具 38 | - 轻松测试 39 | 40 | ## 避免过时的保险 41 | 42 | 易于理解的数据形式和自我描述的数据将超过所有其他形式的数据以及创建它们的应用程序。 期。 只要数据仍然存在,您就有机会使用它-可能在写入它的原始应用程序失效之后很长时间了。 43 | 44 | 您可以仅部分了解其格式来解析此类文件。 对于大多数二进制文件,您必须知道整个格式的所有详细信息才能成功解析它。 45 | 46 | 考虑一个给定的旧系统中的数据文件。[24] 您对原始应用程序了解甚少; 对您而言重要的是,它维护了一个客户的社会保险号列表,您需要查找并提取该列表。 在数据中,您会看到 47 | 48 | 123-45-6789 49 | ... 50 | 567-89-0123 51 | ... 52 | 901-23-4567 53 | 54 | 认识到社会安全号码的格式,您可以快速编写一个小程序来提取该数据,即使您对文件中的其他任何信息都没有。 55 | 56 | 但是想象一下,如果文件是这样格式化的: 57 | 58 | AC27123456789B11P 59 | ... 60 | XY43567890123QTYL 61 | ... 62 | 6T2190123456788AM 63 | 64 | 您可能不太容易意识到数字的重要性。 这是人类可读性与人类可理解性之间的区别。 65 | 66 | 在此过程中,FIELD10 也无济于事。 就像是 67 | 68 | 123-45-6789 69 | 70 | 使练习变得轻而易举-并确保数据将比创建它的任何项目都有效。 71 | 72 | ## 杠杆作用 73 | 74 | 实际上,从版本控制系统到编辑器再到命令行工具,计算世界中的每个工具都可以在纯文本上运行。 75 | 76 | --- 77 | 78 | ### Unix 哲学 79 | Unix 以围绕小型精巧工具的理念而设计而闻名,每种工具都旨在做好一件事情。 通过使用通用的基础格式(面向行的纯文本文件)可以启用此原理。 用于系统管理(用户和密码,网络配置等)的数据库都保留为纯文本文件。 (作为性能优化,某些系统还维护某些数据库的二进制形式。纯文本版本保留为二进制版本的接口。) 80 | 81 | 当系统崩溃时,您可能只需要一个最小的环境即可还原它(例如,您可能无法访问图形驱动程序)。 诸如此类的情况确实可以使您欣赏纯文本的简单性。 82 | 83 | --- 84 | 85 | 例如,假设您具有大型应用程序的生产部署,该应用程序具有复杂的特定于站点的配置文件。 如果该文件为纯文本格式,则可以将其放在版本控制系统下(请参阅话题 19 [_版本控制_](./版本控制.md) ),以便自动保留所有更改的历史记录。 文件比较工具(例如 diff 和 fc)使您可以一目了然地查看所做的更改,而 sum 则允许您生成校验和以监视文件的意外(或恶意)修改。 86 | 87 | ## 更易测试 88 | 89 | 如果使用纯文本创建综合数据来驱动系统测试,那么添加,更新或修改测试数据而无需创建任何特殊工具就很简单。 同样,可以使用 Shell 命令或简单脚本对回归测试的纯文本输出进行简单分析。 90 | 91 | ## 最低公分母 92 | 93 | 即使在基于区块链的智能代理的未来,它们可以自主地在荒芜而危险的互联网上旅行,在它们之间协商数据交换,无处不在的文本文件仍然会存在。事实上,在异构环境中,纯文本的优势可以超过所有的缺点。您需要确保所有各方都可以使用通用标准进行通信。纯文本就是这个标准。 94 | 95 | ## 相关内容包括 96 | 97 | - 话题 32 [_配置_](../Chapter5/配置.md) 98 | - 话题 17 [_Shell 游戏_](./shell.md) 99 | - 话题 21 [_文本处理_](./文本处理.md) 100 | 101 | ## 挑战 102 | 103 | - 使用您选择的语言中的简单二进制表示形式,设计一个小的通讯录数据库(名称,电话号码等)。 在阅读本挑战的其余部分之前,请执行此操作。 104 | - 使用 XML 将该格式转换为纯文本格式。 105 | - 对于每个版本,请添加一个新的长度可变的字段,称为“方向”,您可以在其中输入指向每个人房屋的方向。 106 | 107 | 关于版本和可扩展性会出现什么问题? 哪种形式更容易修改? 如何转换现有数据? 108 | -------------------------------------------------------------------------------- /Chapter3/调试.md: -------------------------------------------------------------------------------- 1 | # 调试 2 | 3 | 4 | > _这是一件痛苦的事_ 5 | > _看着自己的麻烦并且知道_ 6 | > _没有人做到,包括你自己_ 7 | > 8 | > -- _Sophocles,阿贾克斯_ 9 | 10 | 自十四世纪以来,错误(bug)一词就一直被用来描述“恐怖的对象”。海军少将 Grace Hopper 博士是 COBOL 的发明者,他因观察到了第一个计算机错误-从字面上看,它是早期计算机系统中继器中捕获的飞蛾。当被问及为什么机器不按预期运行时,一名技术人员报告说“系统中存在错误”,并忠实地将其翅膀和所有部件粘贴到日志中。 11 | 12 | 遗憾的是,尽管系统不是飞行类,但仍然存在错误。但是,十四世纪的意义 - 一个鬼怪 - 也许现在比那时更适用。从错误的需求到编码错误,软件缺陷以多种方式表现出来。不幸的是,现代计算机系统仍然仅限于执行您要求它们执行的操作,而不一定要执行您希望它们执行的操作。 13 | 14 | 没有人会编写完美的软件,因此调试将占用您一天的大部分时间。让我们看一下调试中涉及的一些问题以及发现难以捉摸的错误的一些一般策略。 15 | 16 | ## 调试心理学 17 | 18 | 对于许多开发人员来说,调试是一个敏感、感性的话题。与其把它当作一个有待解决的难题来攻击,你可能会遇到否认、指手画脚、蹩脚的借口,或者只是单纯的冷漠。 19 | 20 | 接受这样一个事实,调试只是解决问题,并攻击它本身。 21 | 22 | 找到别人的 bug 后,你可以花时间和精力去责怪制造它的肮脏的罪魁祸首。在一些工作场所,这是文化的一部分,可能是一种宣泄。然而,在技术领域,你要集中精力解决问题,而不是指责。 23 | 24 | --- 25 | ## 提示 29 解决问题,而不是责怪 26 | --- 27 | 28 | 该错误是您的错还是其他人的错并不重要。 仍然是你的问题。 29 | 30 | ## 调试心态 31 | 32 | 在开始调试之前,务必要采用正确的心态。 您需要关闭每天用于保护自我的许多防御措施,调整可能承受的任何项目压力,并使自己感到舒适。 首先,请记住调试的第一条规则: 33 | 34 | --- 35 | ## 提示 30 稳住别慌 36 | --- 37 | 38 | 恐慌很容易发生,尤其是在您面临最后期限的情况下,或者当您试图找出错误的原因时,有一个紧张的老板或客户喘口气的时候。 但是,退后一步,并认真考虑可能导致您认为是错误的症状的原因非常重要。 39 | 40 | 如果您对目睹错误或看到错误报告的第一反应是“不可能的”,那显然是错误的。 不要在开始于“但那不可能发生”的思路上浪费单个神经元,因为很明显它可以而且已经存在。 41 | 42 | 调试时要小心近视。抵制只解决您所看到的症状的冲动:实际故障很可能是从您所观察的内容中删除了几个步骤,并且可能涉及许多其他相关事项。始终尝试发现问题的根本原因,而不仅仅是问题的特殊外观。 43 | 44 | ## 从哪儿开始 45 | 46 | 在开始查看该错误之前,请确保您正在使用干净构建的代码-没有警告。我们通常将编译器警告级别设置为尽可能高。浪费时间去寻找计算机可以为您找到的问题是没有道理的!我们需要集中精力解决眼前的难题。 47 | 48 | 尝试解决任何问题时,您需要收集所有相关数据。不幸的是,错误报告并不是一门精确的科学。很容易被巧合所误导,您也不能浪费时间调试巧合。您首先需要在观察中保持准确。 49 | 50 | 通过第三方报告时,错误报告的准确性会进一步降低-您可能实际上需要观察实际报告错误的用户,以获取足够的详细程度。 51 | 52 | 安迪曾经研究过一个大型的图形应用程序。在即将发布的时候,测试人员报告说,每次他们用特定的画笔绘制笔划时,应用程序都会崩溃。负责的程序员辩称没有什么问题;他试过用它画画,效果很好。这段对话持续了好几天,情绪迅速高涨。 53 | 54 | 最后,我们把他们聚集在同一个房间里。测试人员选择了画笔工具并绘制了从右上角到左下角的笔划。应用程序崩溃了。“哦,”程序员用一种微弱的声音说,然后他不好意思地承认,他只做了从左下到右上的测试笔划,没有暴露出错误。 55 | 56 | 这个故事有两点: 57 | 58 | - 为了收集比最初给出的更多的数据,您可能需要访问报告错误的用户。 59 | 60 | - 人工测试(例如程序员从下到上的单笔笔划)对应用程序的锻炼不够。您必须残酷地测试边界条件和实际的最终用户使用模式。 61 | 62 | 你需要系统地做这件事(见 无情和持续的测试 )。 63 | 64 | ## 调试策略 65 | 66 | 一旦您知道发生了什么,就该找出程序认为正在发生什么的时候了。 67 | 68 | ### Bug 复现 69 | 70 | 不,我们的漏洞并没有真的成倍增长(尽管其中一些可能已经足够大,可以合法地做到这一点)。我们说的是另一种复现。 71 | 72 | 开始修复错误的最好方法是使其可复现。毕竟,如果你不能复现它,你怎么知道它是否被修复好? 73 | 74 | 但是,我们需要的不仅仅是一个可以通过一系列步骤复现的 bug;我们需要一个可以通过一个命令复现的 bug。如果你必须经过15个步骤才能达到错误出现的程度,那么修复错误就要困难得多。 75 | 76 | 因此,以下是调试的最重要规则: 77 | 78 | --- 79 | ## 提示 31 代码修复前测试失败 80 | --- 81 | 82 | 有时,通过强迫自己隔离显示该错误的情况,您甚至可以深入了解如何解决该错误。 编写测试的行为为解决方案提供了依据。 83 | 84 | ### 陌生土地上的编码员 85 | 86 | 关于隔离bug的所有讨论都很好,当面对50,000行代码和滴答作响的时钟时,糟糕的编码器怎么办? 87 | 88 | 首先,看问题。 这是崩溃吗? 当我们讲授与编程有关的课程时,总是令人惊讶的是,有多少开发人员看到异常以红色弹出,并在代码中带有即时选项卡。 89 | 90 | --- 91 | ## 提示 32 阅读该死的错误消息 92 | --- 93 | 94 | ### 糟糕的结果 95 | 96 | 如果这不是程序崩溃。只是结果不好怎么办?使用调试器进入,然后使用失败的测试来触发问题。 97 | 98 | 首先,请确保您还在调试器中看到了错误的值。我们俩都浪费了很多时间来试图查找错误,只是发现这种特定的代码运行良好。 99 | 有时问题很明显:interest_rate 为 4.5,应为 0.045。更多时候,您必须更深入地研究,以找出为什么值首先是错误的。确保您知道如何上下移动调用堆栈并检查本地堆栈环境。 100 | 101 | 我们发现通常可以将笔和纸放在附近,这样我们就可以记笔记了。特别是,我们经常碰到一个线索并追逐它,却发现它没有成功。如果我们在开始追赶时没有记下自己的位置,则可能会浪费很多时间回到那里。 102 | 103 | 有时您正在查看似乎一直滚动的堆栈跟踪。在这种情况下,通常使用二进制印章比检查每个堆栈帧更快地找到问题。但是在讨论之前,让我们看一下另外两个常见的错误场景。 104 | 105 | ### 对输入值的敏感性 106 | 107 | 你遇到过。 您的程序可以很好地处理所有测试数据,并且可以在生产的第一周中幸存下来。 然后在送入特定数据集时突然崩溃。 108 | 您可以尝试查看它崩溃的地方并向后工作。 但是有时候从数据开始比较容易。 获取数据集的副本,并通过应用程序的本地运行副本将其提供,以确保它仍然崩溃。 然后将数据二进制二进制化,直到您准确隔离导致崩溃的输入值为止。 109 | 110 | ### 各个版本之间的回归 111 | 112 | 您拥有一支优秀的团队,并且将软件投入生产。 在某个时候,一个错误会在一周前可以正常工作的代码中弹出。 如果您能确定引入它的特定更改,那岂不是很好吗? 你猜怎么了? 二进制印章时间。 113 | 114 | ## 二进制印章 115 | 116 | 每个计算机专业的本科生都被迫编写二进制印章(有时称为二进制搜索)。这个想法很简单。您正在寻找排序数组中的特定值。您可以依次查看每个值,但最终会平均查看大约一半的条目,直到找到所需的值,或者找到的值大于该值,这意味着该值不在数组。 117 | 118 | 但是使用 _分而治之_ 的方法更快。在数组中间选择一个值。如果这是您要寻找的那个,请停止。否则,您可以将阵列切成两半。如果找到的值大于目标值,则说明它必须位于数组的前半部分,否则位于后半部分。在适当的子数组中重复该过程,您将很快获得结果。(正如我们在谈论 Big-O 表示法 时所看到的那样,线性搜索为 O(n) ,二进制印章为O(log n) 。 119 | 120 | 因此,二进制印章是解决任何体面大小问题的方法。让我们看看如何将其应用于调试。 121 | 122 | 当您面对大量的堆栈跟踪并试图准确找出哪个函数破坏了错误的值时,您可以通过在中间的某个位置选择一个堆栈框架并查看错误是否明显来进行切碎。如果是这样,那么您就知道要集中在前面的帧上,否则问题就出在后面的帧上。即使您的堆栈跟踪中有64帧,这种方法最多也可以在尝试6次后为您提供答案。 123 | 124 | 如果您发现某些数据集中出现的错误,则可以执行相同的操作。将集合分成两部分,如果一部分通过应用程序,另一部分则出现问题。继续划分数据,直到获得最小的显示问题的值集为止。 125 | 126 | 如果您的团队在一组发行版中引入了一个错误,则可以使用相同类型的技术。创建一个导致当前版本失败的测试。然后选择从现在到最后一个已知的工作版本之间的发布。再次运行测试,然后决定如何缩小搜索范围。能够执行此操作只是在项目中拥有良好的版本控制的众多好处中的一部分。的确,许多版本控制系统会更进一步,并且会自动执行该过程,并根据测试结果为您选择发行版本。 127 | 128 | ## 记录和/或跟踪 129 | 130 | 调试器现在通常关注程序的状态。有时你需要更多的时间来观察程序或数据结构的状态。看到堆栈跟踪只能告诉你是怎么直接到这里的。它通常不能告诉你在这个调用链之前你在做什么,特别是在基于事件的系统中。 131 | 132 | _跟踪语句_ 是打印到屏幕或文件中的一些诊断消息,例如“got here” 和 “value of x=2”。与IDE风格的调试器相比,这是一种原始技术,但它在诊断调试器不能诊断的几类错误时特别有效。跟踪在任何情况下都是非常宝贵的时间本身是一个因素的系统:并发进程、实时系统和基于事件的应用程序。 133 | 134 | 您可以使用跟踪语句来深入代码。 也就是说,您可以在调用树下降时添加跟踪语句。 135 | 136 | 跟踪消息应采用常规、一致的格式,因为您可能希望自动解析它们。例如,如果需要跟踪资源泄漏(例如文件打开/关闭不平衡),可以跟踪日志文件中的每个打开和关闭。通过使用文本处理工具或 shell 命令处理日志文件=您可以很容易地确定违规打开的位置。 137 | 138 | ## 橡皮鸭 139 | 140 | 找到问题原因的一种非常简单但特别有用的方法是简单地向其他人解释。另一个人应该看着你的屏幕,不停地点头(就像一只在浴缸里上下摆动的橡皮鸭)。他们不需要说一个字;一步一步地解释代码应该做什么,这种简单的行为通常会导致问题自己从屏幕上跳出来。 141 | 142 | 这听起来很简单,但是在向另一个人解释问题时,您必须明确地说明在您自己浏览代码时可能认为理所当然的事情。通过口头表达这些假设,你可能会突然对问题有新的见解。如果你没有人,橡胶鸭,泰迪熊,盆栽植物就可以了。 143 | 144 | ## 排除法 145 | 146 | 在大多数项目中,您要调试的代码可能是您和其他项目团队里的人编写的,或者第三方产品(数据库,连接性,Web框架,专用通信或算法等)和平台上编写的应用程序代码的混合体环境(操作系统,系统库和编译器)。 147 | 148 | 操作系统,编译器或第三方产品中可能存在错误,但这不是您首先想到的。该错误很可能存在于正在开发的应用程序代码中。通常,假定应用程序代码错误地调用了一个库比假定库本身已损坏,这更有利可图。即使问题确实出在第三方,您仍必须在提交错误报告之前消除代码。 149 | 150 | 我们在一个项目中工作,高级工程师确信该项目在 Unix 系统上中断了 select 系统调用。没有任何说服力或逻辑可以改变他的想法(盒子里的所有其他网络应用程序都运行良好这一事实是无关紧要的)。他花了数周的时间编写解决方法,出于某种奇怪的原因,该解决方法似乎无法解决问题。当最终被迫坐下来阅读 select 文档时,他发现了问题并在几分钟内纠正了问题。现在,只要我们中的一个人开始将系统归咎于可能是我们自己的错误,我们就会在此使用“选择已损坏”这个词来温和地提醒您。 151 | 152 | --- 153 | ## 提示 33 "select" 没有损坏 154 | --- 155 | 156 | 请记住,如果您看到蹄印,请思考马而不是斑马。 操作系统可能没有损坏。 select 可能是好的。 157 | 158 | 如果您“仅更改了一件事情”而系统停止运行,则不管看起来多么牵强,该一件事情都可能直接或间接地引起责任。 有时,发生变化的事情超出了您的控制范围:新版本的OS,编译器,数据库或其他第三方软件可能会对以前正确的代码造成严重破坏。 可能会出现新的错误。 解决了您需要解决的错误,从而破坏了解决方法。 API更改,功能更改; 简而言之,这是一个全新的球类游戏,您必须在这些新条件下重新测试系统。 因此,在考虑升级时,请密切注意时间表。 您可能要等到下一个版本发布。 159 | 160 | ## 惊喜元素 161 | 162 | 当您发现自己对错误感到惊讶时(甚至在我们听不到您的呼吸中喃喃自语“那不可能”),您必须重新评估您所珍视的真理。 在这种折扣计算算法中(您知道的那种算法是防弹的,不可能是导致该错误的原因),您是否测试了所有边界条件? 您使用了多年的其他代码-可能仍然没有bug。 这可以吗? 163 | 164 | 当然可以。 当出现问题时,您感到惊讶的程度与您对正在运行的代码的信任和信任程度成正比。 因此,当您面对“令人惊讶的”失败时,您必须意识到自己的一个或多个假设是错误的。 不要掩盖该错误所涉及的例程或代码段,因为您“知道”它的工作原理。 证明给我看。 在此情况下,使用这些数据和这些边界条件进行证明。 165 | 166 | --- 167 | ## 提示 34 不要假设它-证明它 168 | --- 169 | 170 | 当您遇到一个令人惊讶的错误时,不仅要修复它,还需要确定为什么未尽早发现此故障。考虑是否需要修改单元或其他测试,以使他们能够抓住它。 171 | 172 | 此外,如果该错误是由于不良数据在引起崩溃之前已通过几个级别传播的结果,请查看这些例程中更好的参数检查是否可以更早地对其进行隔离(请参见此处及此处有关早期崩溃和断言的讨论,分别。) 173 | 174 | 当您使用它时,代码中是否还有其他地方可能容易受此相同错误的影响?现在是时候找到并修复它们了。确保无论发生什么,您都将知道是否再次发生。 175 | 176 | 如果修复此错误花了很长时间,请问问自己为什么。您是否可以采取任何措施使下次更轻松地修复此错误?也许您可以建立更好的测试挂钩,或编写日志文件分析器。 177 | 178 | 最后,如果错误是某人错误假设的结果,请与整个团队讨论该问题:如果一个人误解了,那么很多人可能会这样做。 179 | 180 | 完成所有这些操作,希望您下次不会感到惊讶。 181 | 182 | ## 调试检查表 183 | 184 | - 被报告的问题是潜在错误的直接结果,还是仅仅是症状? 185 | 186 | - 该错误确实在您使用的框架中吗? 在操作系统中吗? 还是在您的代码中? 187 | 188 | - 如果您向同事详细解释了这个问题,您会怎么说? 189 | 190 | - 如果可疑代码通过了其单元测试,则测试是否足够完成? 如果使用此数据运行测试会怎样? 191 | 192 | - 导致此错误的条件是否存在于系统中的其他任何地方? 幼虫还在等待孵化吗? 193 | 194 | ## 相关内容包括 195 | 196 | - 话题 24 [_死程序不说谎_](../Chapter4/死程序不说谎.md) 197 | 198 | ## 挑战 199 | 200 | - 调试就是足够的挑战 201 | -------------------------------------------------------------------------------- /Chapter4/别开过头了.md: -------------------------------------------------------------------------------- 1 | # 别开过头了 2 | 3 | > _很难做出预测,尤其是对未来的预测。_ 4 | > 5 | > _-- 劳伦斯“约吉”贝拉,丹麦谚语_ 6 | 7 | 夜深了,天黑了,下着倾盆大雨。这辆两座车在蜿蜒的小山路上走着,几乎没有拐弯。一个发夹冲了上来,汽车没撞上,撞破了那简陋的护栏,在下面的山谷里冲向了一场激烈的车祸。州警到达现场,高级长官悲伤地摇摇头。“一定是开过头了。” 8 | 9 | 超速的双座车真的比光速还快吗?不,那个速度限制是固定的。这名警官所说的是司机能够根据前照灯的照明情况及时停车或转向。 10 | 11 | 车头灯的射程有限,称为 抛射距离 。过了那一点,光线的扩散太过“漫射”而无法生效。此外,前照灯只在直线上投射,不会照亮任何偏离轴的东西,例如曲线、山丘或路面凹陷。根据美国国家公路交通安全管理局(National Highway Traffic Safety Administration),近光前照灯照明的平均距离约为 160 英尺。不幸的是,40英里/小时的停车距离是 189 英尺,70英里/小时的停车距离是 464 英尺。所以,实际上,它很容易超过你的头灯。 12 | 13 | 在软件开发中,我们的“头灯”同样受到限制。我们看不到太远的未来,离轴越远,越暗。所以务实的程序员有一条坚定的原则: 14 | 15 | --- 16 | ## 提示 42 总是小步走 17 | --- 18 | 19 | 在继续之前,始终采取小心谨慎的步骤,检查反馈和调整。考虑到反馈率是限制你的速度。你从不采取“太大”的步骤或任务。 20 | 反馈到底是什么意思?任何能独立证实或反驳你行为的东西。例如: 21 | 22 | - REPL中的结果提供了关于您对api和算法理解的反馈 23 | 24 | - 单元测试提供对上一次代码更改的反馈 25 | 26 | - 用户演示和对话提供有关功能和可用性的反馈 27 | 28 | 一个什么样的任务算太大了?任何需要“算命”的任务,就像汽车前灯的射程有限一样,我们只能看到未来可能只有一两步,最多可能几个小时或几天。过了这个阶段,你就可以很快地超越有教养的猜测,进入疯狂的猜测。你可能会发现,当你不得不: 29 | 30 | - 估计未来几个月的完工日期 31 | 32 | - 为将来的维护或可扩展性计划设计 33 | 34 | - 猜测用户的未来需求 35 | 36 | - 猜测未来的技术可用性 37 | 38 | 但是,我们听到你哭了,难道我们不应该为将来的维修设计吗?是的,但仅限于一点:只有在你所能看到的前方。你越是要预测未来的样子,你就越有可能犯错。与其浪费精力为不确定的未来进行设计,还不如总是将代码设计为可替换的。很容易抛出代码,并用更适合的代码替换它。使代码可替换还将有助于内聚、耦合和 [_DRY_](../Chapter2/重复的恶魔.md),从而带来更好的总体设计。 39 | 40 | 即使你对未来充满信心,但总有一天会有一只黑天鹅出现。 41 | 42 | ## 黑天鹅 43 | 44 | 纳西姆·尼古拉斯·塔勒布在他的著作《黑天鹅:极不可能发生的事件的影响》(The Black Swan:The Impact of The high Incombable)中认为,历史上所有重大事件都来自于高调、难以预测的、超出正常预期范围的罕见事件。这些异常值虽然在统计上很少见,但其影响却不成比例。此外,我们自己的认知偏见往往会使我们对工作边缘的变化视而不见(见话题 4,[_石汤和煮青蛙_](../Chapter1/石汤和煮青蛙.md) ) 45 | 46 | 在《实用主义程序员》第一版面世之际,计算机杂志和在线论坛上关于“谁将赢得桌面 GUI 战争、Motif 或OpenLook”这一热门问题的争论如火如荼?这是个错误的问题。很有可能你从来没有听说过这些技术,没有人“赢”过,而且基于浏览器的万维网很快占据了这一领域。 47 | 48 | --- 49 | ## 提示 43 避免算命 50 | --- 51 | 52 | 很多时候,明天看起来很像今天。但别指望它。 53 | 54 | ## 相关内容包括 55 | 56 | - 话题 13 [_原型和便签_](../Chapter2/原型和便签.md) 57 | - 话题 40 [_重构_](../Chapter7/重构.md) 58 | - 话题 41 [_代码测试_](../Chapter7/代码测试.md) 59 | - 话题 12 [_示踪子弹_](../Chapter2/示踪子弹.md) 60 | - 话题 49 [_椰子不要切碎_](../Chapter9/椰子不要切碎.md) 61 | - 话题 47 [_敏捷的本质_](../Chapter8/敏捷的本质.md) 62 | -------------------------------------------------------------------------------- /Chapter4/契约设计.md: -------------------------------------------------------------------------------- 1 | # 契约设计 2 | 3 | 4 | > _没有什么比常识和简单的交易更能使人惊讶了。_ 5 | > 6 | > -- _拉尔夫·沃尔多·爱默生,随笔_ 7 | 8 | 处理计算机系统很困难。 与人打交道更加困难。 但是作为一个物种,我们不得不花更长的时间来解决人类互动的问题。 我们在过去的几千年中提出的一些解决方案也可以应用于编写软件。 确保简单交易的最佳解决方案之一:契约。 9 | 10 | 契约定义了您以及另一方的权利和责任。 此外,如果任何一方不遵守契约,则有关于后果的协议。 11 | 12 | 也许您有一份雇佣契约,其中规定了您的工作时间和必须遵守的行为准则。 作为回报,公司向您支付薪水和其他津贴。 各方都履行其义务,每个人都将从中受益。 13 | 14 | 这个想法在全世界范围内(正式和非正式)都可以用来帮助人们互动。 我们可以使用相同的概念来帮助软件模块进行交互吗? 答案是“是”。 15 | 16 | ## DBC 17 | 18 | Bertrand Meyer 在《面向对象的软件构造》中在 Eiffel 语言里提到了按契约设计的概念。这是一种简单而强大的技术,致力于文档化(并同意)软件模块的权利和责任以确保程序正确性。 什么是正确的程序? 一个只做声称的事情,不多做而且也不少做。 记录并验证索赔是“按契约设计”(简称DBC)的核心。 19 | 20 | 软件系统中的每个功能和方法都可以执行某些操作。在开始做某件事之前,这个函数可能对世界的状态有一些期望,当它结束时,它可能能够对世界的状态做一个声明。 Meyer 将这些期望和主张描述如下: 21 | 22 | _前提条件_ 23 | 24 | 为了调用例程,必须满足的条件; 常规的要求。 当例程的先决条件被违反时,绝对不应调用该例程。 传递良好数据是调用者的责任(请参见后文)。 25 | 26 | _后置条件_ 27 | 28 | 例行程序保证要做的事; 例行程序完成后的世界状态。 例程具有后置条件这一事实意味着它将得出结论:不允许无限循环。 29 | 30 | _类不变式_ 31 | 32 | 从调用者的角度出发,一个类确保此条件始终为真。 在例程的内部处理过程中,不变式可能不会成立,但是当例程退出并控制权返回给调用者时,该不变式必须为 true。 (请注意,一个类不能对参与该变量的任何数据成员提供无限制的写访问权限。) 33 | 34 | 因此,例程和任何潜在调用方之间的协定都可以理解为: 35 | 36 | 如果调用方满足例程的所有先决条件,则例程应保证完成时所有后置条件和不变量均为真。 37 | 38 | 如果任何一方未能遵守契约的条款,则将采取一项补救措施(先前已同意)-可能引发例外情况或程序终止。 无论发生什么情况,请不要误解未能履行契约是一个错误。 这是永远不会发生的事情,这就是为什么不应使用前提条件来执行诸如用户输入验证之类的事情的原因。 39 | 40 | 某些语言比其他语言对这些概念有更好的支持。 例如,Clojure 支持前置条件和后置条件以及规范提供的更全面的工具。 这是使用简单的前提条件和后置条件进行存款的银行功能示例: 41 | 42 | ```Clojure 43 | ( 44 | defn accept_deposit [account-id amount] 45 | { 46 | :pre [ (> amount 0.00) (is-open-account account-id) ] 47 | :post [ (contains? (account-transactions account-id) %) ] 48 | } 49 | 50 | "Accept a deposit and return the new transaction id" 51 | ;; Some other processing goes here... 52 | ;; Return the newly created transaction: 53 | (create-transaction account-id :deposit amount) 54 | ) 55 | ``` 56 | 57 | accept_deposit 函数有两个先决条件。 第一个是金额大于零,第二个是该帐户已打开且有效,这由名为is-open-account 的某些功能确定。 还有一个后置条件:该函数保证可以在该帐户的交易中找到新的交易(此函数的返回值,在此以“%”表示)。 58 | 如果您用一个正数的存款和一个有效的帐户调用 accept_deposit,它将继续创建适当类型的交易并执行其他任何处理。 但是,如果程序中存在错误,并且您以某种方式为存款支付了一笔负数,您将获得运行时异常: 59 | 60 | ```shell 61 | Exception in thread "main"... 62 | Caused by: java.lang.AssertionError: Assert failed: (> amount 0.0) 63 | ``` 64 | 同样,此功能要求指定的帐户是开放且有效的。 如果不是,您会看到该异常: 65 | 66 | ```shell 67 | Exception in thread "main"... 68 | Caused by: java.lang.AssertionError: Assert failed: (is-open-account account-id) 69 | ``` 70 | 71 | 其他语言具有的功能虽然不是特定于 DBC 的,但仍可以很好地使用。 例如,Elixir 使用保护子句针对多个可用主体分派函数调用: 72 | 73 | ```elixir 74 | defmodule Deposits do 75 | def accept_deposit(account_id, amount) when (amount > 0) do 76 | # Some processing... 77 | end 78 | 79 | def accept_deposit(account_id, amount) when (amount > 10000) do 80 | # Extra Federal requirements for reporting 81 | # Some processing... 82 | end 83 | 84 | def accept_deposit(account_id, amount) when (amount > 100000) do 85 | # Call the manager! 86 | end 87 | end 88 | ``` 89 | 90 | 在这种情况下,调用足够多的 accept_deposit 可能会触发其他步骤和处理。 尝试以小于或等于零的数量来调用它,但是会出现异常通知您,您不能执行以下操作: 91 | 92 | ```shell 93 | ** (FunctionClauseError) no function clause matching in Deposits.accept_deposit/2 94 | ``` 95 | 96 | 这比简单地检查您的输入更好。 在这种情况下,如果参数超出范围,则根本无法调用此函数。 97 | 98 | --- 99 | ## 提示 37 按契约设计 100 | --- 101 | 102 | ### DBC 和测试驱动开发 103 | 104 | 在开发人员进行单元测试,测试驱动开发(TDD),基于属性的测试或防御性编程的世界中,是否需要按契约设计? 105 | 106 | 简短的答案 “是”。 107 | 108 | DBC 和测试是解决程序正确性这一更广泛主题的不同方法。它们都有价值,并且在不同情况下都有用处。与特定的测试方法相比, 109 | 110 | DBC 具有多个优点: 111 | 112 | - DBC 不需要任何设置或模拟 113 | 114 | - DBC 定义了所有情况下成功或失败的参数,因为测试一次只能针对一个特定情况 115 | 116 | - TDD 和其他测试仅在构建周期内的“测试时间”进行。但是 DBC 和断言是永远存在的:在设计,开发,部署和维护期间 117 | 118 | - TDD 并不专注于检查被测代码中的内部不变量,而是采用黑盒形式来检查公共界面 119 | 120 | - DBC 比防御性编程更为有效(和DRY-er),在防御性编程中,每个人都必须验证数据,以防其他人没有这样做。 121 | 122 | TDD 是一种很棒的技术,但是和许多其他技术一样,它可能会邀请您专注于“happy path”,而不是充满坏数据,坏actors,坏版本和坏规范的真实世界。 123 | 124 | 在话题 10 [_正交性_](../Chapter2/正交性.md) 中,我们建议编写 “害羞” 代码。 在这里,重点是“惰性”代码:在开始之前要严格接受,并承诺尽可能少的返回。 请记住,如果您的契约表明您会接受任何东西并承诺返回世界,那么您将有很多代码可以编写! 125 | 126 | 无论是功能性,面向对象还是程序性的任何编程语言,DBC 都会迫使您进行思考。 127 | 128 | ### 类不变式和功能语言 129 | 130 | 这是一个命名的东西。 Eiffel 是一种面向对象的语言,因此 Meyer 将此想法命名为“类不变式”。 但是,实际上,它比这更笼统。 这个想法真正指的是 状态。 在面向对象的语言中,状态与类的实例相关联。 但是其他语言也有陈述。 131 | 132 | 在函数式语言中,通常将状态传递给功能并接收更新后的状态。 在这些情况下,不变的概念同样有用。 133 | 134 | ## 实现 DBC 135 | 136 | 简单地列举输入域范围是什么,边界条件是什么,例程承诺交付什么,或者更重要的是,在编写代码之前它不承诺交付什么,这是编写更好的软件的一个巨大飞跃。如果不声明这些内容,您就回到了 巧合编程(参见此处的讨论),这是许多项目开始、完成和失败的地方。 137 | 138 | 在代码不支持 DBC 的语言中,这可能是您能做的最远的事情,这也不算太糟。毕竟,DBC 是一种 设计 技术。即使没有自动检查,您也可以将契约作为注释或单元测试放入代码中,并且仍然可以获得非常实际的好处。 139 | 140 | ### 断言 141 | 142 | 虽然记录这些假设是一个不错的开始,但是让编译器为您检查契约可以使您获得更大的收益。您可以使用断言在某些语言中部分模拟此内容:逻辑条件的运行时检查(请参见话题 25,[_断言式编程_](./断言式编程.md) )。为什么只部分地?您不能使用断言来完成 DBC 可以做的一切吗? 143 | 144 | 不幸的是,答案是否定的。首先,在面向对象的语言中,可能不支持沿继承层次结构传播声明。这意味着,如果您覆盖具有契约的基类方法,则将无法正确调用实现该契约的断言(除非您在新代码中手动复制它们)。您必须记得在退出每个方法之前手动调用类不变式(以及所有基类不变式)。基本问题是契约不会自动执行。 145 | 146 | 在其他环境中,从 DBC 样式的断言生成的异常可能会全局关闭或在代码中被忽略。 147 | 148 | 此外,没有内置的“旧”价值观。也就是说,方法入口处存在的值。如果您使用断言来执行契约,则必须在前提条件中添加代码,以保存您希望在后置条件中使用的所有信息(即使该语言甚至允许这样做)。在 DBC 诞生的 Eiffel 语言中,您可以使用 旧 表达式。 149 | 150 | 最后,传统的运行时系统和库并非旨在支持契约,因此不会检查这些调用。这是一个很大的损失,因为通常是在代码和它使用的库之间的边界处检测到最多的问题(有关更多详细讨论,请参见话题 24,[_死程序不说谎_](./死程序不说谎.md) )。 151 | 152 | --- 153 | 154 | ### 谁的责任 155 | 156 | 谁负责检查前提条件,调用方或被调用的例程?当作为语言的一部分实现时,答案都不是:前提条件是在调用者调用例程之后但在例程本身进入之前在后台进行测试。因此,如果要对参数进行任何显式检查,则必须由调用方执行,因为例程本身永远不会看到违反其先决条件的参数。 (对于没有内置支持的语言,您需要将被调用的例程与检查这些断言的前导和/或后导括起来。) 157 | 158 | 考虑一个从控制台读取数字,计算平方根(通过调用 sqrt )并打印结果的程序。 sqrt 函数有一个前提条件-它的参数不能为负。如果用户在控制台上输入负数,则取决于调用代码,以确保它永远不会传递给 sqrt。此调用代码有很多选择:可以终止,可以发出警告并读取另一个数字,或者可以使数字为正数,并将 i 附加到 sqrt 返回的结果中。无论选择什么,这绝对不是 sqrt 的问题。 159 | 160 | 通过在 sqrt 例程的前提下表达平方根函数的域,可以将正确性的负担转移给调用程序所在的位置。然后,您可以在知道其输入将在范围内的情况下设计 sqrt 例程安全。 161 | 162 | --- 163 | 164 | ## DBC和早期崩溃 165 | 166 | 您可以使用语义不变量来表示违规要求,这是一种“哲学契约”。 167 | 168 | 我们曾经写过一个借记卡交易开关。一个主要的要求是,借记卡的用户绝不能将同一笔交易两次应用于其帐户。换句话说,无论发生哪种故障模式,错误都应该在于不处理事务而不是处理重复事务。 169 | 170 | 直接从需求出发的简单法则被证明对整理复杂的错误恢复方案非常有帮助,并指导了许多领域的详细设计和实现。 171 | 确保不要混淆固定的要求,违反法律的要求和仅仅是随新管理制度而可能改变的策略的要求。这就是为什么我们使用“语义不变式”一词的原因,它必须是事物含义的核心,并且不受制于政策的异想天开(这是更动态的业务规则所针对的)。 172 | 173 | 当您发现符合条件的需求时,请确保它成为您正在制作的任何文档中的众所周知的部分-无论是需求文档中的项目符号列表都一式三份地签名,还是只是每个人在通用白板上的一个大注记看到。尝试清楚明确地陈述它。例如,在借记卡示例中,我们可能会写 174 | 175 | 对消费者有利的错误。 176 | 177 | 这是一条清晰,简洁,明确的声明,适用于系统的许多不同领域。这是我们与系统所有用户的契约,是我们行为的保证。 178 | 179 | ## 动态契约和代理 180 | 181 | 到目前为止,我们一直把契约看作是固定不变的规范。但在自主代理的情况下,不需要这样。根据“自治”的定义,代理可以自由拒绝他们不想遵守的请求。他们可以自由地重新谈判契约-- “我不能提供,但如果你给我这个,那么我可能会提供其他东西。” 182 | 183 | 当然,任何依赖于代理技术的系统都非常依赖于契约安排,即使它们是动态生成的。 184 | 185 | 想象一下:如果有足够的组件和代理可以在他们之间协商他们自己的契约来实现一个目标,那么我们可以通过让软件为我们解决软件生产率危机。 186 | 187 | 但如果我们不能手工使用契约,我们就不能自动使用它们。所以下次你设计一个软件的时候,也要设计它的契约。 188 | 189 | ## 相关内容包括 190 | 191 | - 话题 25 [_断言式编程_](./断言式编程.md) 192 | - 话题 38 [_巧合编程_](../Chapter7/巧合编程.md) 193 | - 话题 24 [_死程序不说谎_](./死程序不说谎.md) 194 | - 话题 42 [_基于属性的测试_](../Chapter7/基于属性的测试.md) 195 | - 话题 45 [_需求坑_](../Chapter8/需求坑.md) 196 | - 话题 43 [_在某处保持安全_](../Chapter7/在某处保持安全.md) 197 | 198 | ## 挑战 199 | 200 | - 需要考虑的问题:如果 DBC 如此强大,为什么它没有被更广泛地使用? 签订契约难吗? 这是否会让您想到您现在宁愿忽略的问题? 它会迫使您思考吗? 显然,这是一个危险的工具! 201 | 202 | ## 练习 203 | ### 练习 12 (尽可能回答) 204 | 205 | 设计一个与厨房搅拌机的接口。 最终它是基于 Web 的,支持 IoT 的混合器,但现在我们只需要用于控制它的界面即可。 它有十种速度设置(0表示关闭)。 您不能将其设为空,并且一次只能更改一个单位的速度(即从0更改为1,从1更改为2,而不是从0更改为2)。 206 | 207 | 这里是方法。 添加适当的前置条件和后置条件以及不变式。 208 | 209 | ``` 210 | int getSpeed() 211 | void setSpeed(int x) 212 | boolean isFull() 213 | void fill() 214 | void empty() 215 | ``` 216 | 217 | ### 练习 13 (尽可能回答) 218 | 219 | 序列 0,5,10,15,…,100 中有多少个数字? 220 | -------------------------------------------------------------------------------- /Chapter4/如何平衡资源.md: -------------------------------------------------------------------------------- 1 | # 如何平衡资源 2 | 3 | 4 | > _点燃蜡烛是在投下阴影。_ 5 | > 6 | > -- _Ursula K. Le Guin, 《地海奇才》_ 7 | 8 | 每当我们编写代码时,我们所有人都会管理资源:内存,事务,线程,网络连接,文件,计时器-可用性有限的所有事物。 在大多数情况下,资源使用情况遵循可预测的模式:您分配资源,使用它,然后再分配它。 9 | 10 | 但是,许多开发人员没有一致的计划来处理资源分配和释放。 因此,让我们提出一个简单的提示: 11 | 12 | --- 13 | ## 提示 40 善始善终 14 | --- 15 | 16 | 在大多数情况下,此技巧很容易应用。 它只是意味着分配资源的函数或对象应负责对其进行分配。 让我们看一些不良代码的示例,它是 Ruby 程序的一部分,该程序打开文件,从文件中读取客户信息,更新字段,然后将结果写回。 我们已经消除了错误处理,以使示例更清晰。 17 | 18 | ```ruby 19 | def read_customer 20 | @customer_file = File.open(@name + ".rec", "r+") 21 | @balance = BigDecimal(@customer_file.gets) 22 | end 23 | 24 | def write_customer 25 | @customer_file.rewind 26 | @customer_file.puts @balance.to_s 27 | @customer_file.close 28 | end 29 | 30 | def update_customer(transaction_amount) 31 | read_customer 32 | @balance = @balance.add(transaction_amount, 2) 33 | write_customer 34 | end 35 | ``` 36 | 37 | 乍一看,例程 update_customer 看起来很合理。 似乎已实现了我们所需的逻辑-读取记录,更新余额并写回记录。 但是,这种整洁度隐藏了一个主要问题。 例程 read_customer 和 write_customer 紧密耦合-它们共享实例变量 customer_file。 read_customer 打开文件并将文件引用存储在 customer_file 中,然后 write_customer使用该存储的引用在文件完成时关闭文件。 这个共享变量甚至没有出现在 update_customer 例程中。 38 | 39 | 为什么这样不好? 让我们考虑一个不幸的维护程序员,他被告知规格已更改—仅当新值不为负时,才应更新余额。 他们进入源代码并更改 update_customer: 40 | 41 | ```ruby 42 | def update_customer(transaction_amount) 43 | read_customer 44 | if (transaction_amount >= 0.00) 45 | @balance = @balance.add(transaction_amount, 2) 46 | write_customer 47 | end 48 | end 49 | ``` 50 | 51 | 在测试过程中一切似乎都很好。 但是,当代码投入生产时,它会在几个小时后崩溃,并抱怨打开的文件太多。 由于在某些情况下不会调用 write_customer ,因此不会关闭文件。 52 | 53 | 解决此问题的一个非常糟糕的方法是处理 update_customer 中的特殊情况: 54 | 55 | ```ruby 56 | def update_customer(transaction_amount) 57 | read_customer 58 | if (transaction_amount >= 0.00) 59 | @balance = @balance.add(transaction_amount, 2) 60 | write_customer 61 | else 62 | @customer_file.close # Bad idea! 63 | end 64 | end 65 | ``` 66 | 67 | 这将解决问题(无论新余额如何,文件现在都将关闭),但此修复程序意味着三个例程通过共享变量 customer_file耦合,并跟踪文件何时打开或不打开。变得凌乱。我们陷入了陷阱,如果我们继续这一过程,事情将开始迅速下坡。这是不平衡的! 68 | 69 | 善始善终 的技巧告诉我们,理想情况下,分配资源的例程也应该释放它。我们可以在这里通过稍微重构代码来应用它: 70 | 71 | ```ruby 72 | def read_customer(file) 73 | @balance = BigDecimal(file.gets) 74 | end 75 | 76 | def write_customer 77 | file.rewind 78 | file.puts @balance.to_s 79 | end 80 | 81 | def update_customer(transaction_amount) 82 | file = File.open(@name + ".rec", "r+") 83 | # >-- 84 | read_customer(file) 85 | # / 86 | @balance = @balance.add(transaction_amount, 2) 87 | # / 88 | file.close 89 | # <-- 90 | end 91 | ``` 92 | 93 | 我们没有保留文件引用,而是更改了代码以将其作为参数传递。现在该文件的所有责任都在 update_customer 例程中。它打开文件,并(善始善终)在返回之前将其关闭。该例程平衡了文件的使用:打开和关闭位于同一位置,很明显,每次打开都会有一个对应的关闭。重构还删除了难看的共享变量。 94 | 95 | 我们可以做的又一个小而重要的改进。在许多现代语言中,您可以将资源的生存期限定为某种封闭的块。在 Ruby 中,文件打开方式有所不同,它将打开文件引用传递给一个块,如下所示,该代码位于 do 和 end 之间: 96 | 97 | ```ruby 98 | def update_customer(transaction_amount) 99 | File.open(@name + ".rec", "r+") do |file| 100 | # >-- 101 | read_customer(file) 102 | # / 103 | @balance = @balance.add(transaction_amount, 2) 104 | # / 105 | file.close 106 | end 107 | # <-- 108 | end 109 | ``` 110 | 111 | 在这种情况下,在块末尾,文件变量超出范围,并且外部文件关闭。无需记住关闭文件并释放源代码,可以保证会发生这种情况。 112 | 113 | 如有疑问,缩小范围总是值得的 114 | 115 | --- 116 | ## 提示 41 本地行动 117 | --- 118 | 119 | ### 随时间变化的平衡 120 | 121 | 在本主题中,我们主要关注的是运行过程中使用的临时资源。 但是您可能要考虑可能留下的其他麻烦。 122 | 123 | 例如,如何处理您的日志文件? 您正在创建数据并耗尽存储空间。 是否有东西可以旋转原木并清理它们? 对于要删除的非官方调试文件呢? 如果您要在数据库中添加日志记录,是否有类似的流程来使它们过期? 对于您创建的任何占用有限资源的内容,请考虑如何平衡它。 124 | 125 | 你还剩下什么? 126 | 127 | --- 128 | 129 | ## 嵌套分配 130 | 131 | 可以将资源分配的基本模式扩展为一次需要多个资源的例程。 还有两个建议: 132 | 133 | - 以与分配资源相反的顺序释放资源。 这样,如果一个资源包含对另一个资源的引用,您就不会孤立资源。 134 | 135 | - 在代码的不同位置分配同一组资源时,请始终以相同的顺序分配它们。 这将减少死锁的可能性。 (如果进程A声明了 136 | resource1并将要声明resource2,而进程B声明了resource2并试图获取resource1,则这两个进程将永远等待。) 137 | 138 | 不管我们使用哪种资源(事务,网络连接,内存,文件,线程,窗口),都适用基本模式:分配资源的人应负责分配资源。 但是,在某些语言中,我们可以进一步发展该概念。 139 | 140 | ## 对象和异常 141 | 142 | 分配与释放之间的平衡让人想起面向对象类的构造函数和析构函数。 该类表示资源,构造函数为您提供该资源类型的特定对象,而析构函数将其从您的范围中删除。 143 | 144 | 如果使用面向对象的语言进行编程,则可能会发现将资源封装在类中很有用。 每次需要特定的资源类型时,都实例化该类的对象。 当对象超出范围或被垃圾回收器回收时,对象的析构函数将重新分配包装的资源。 145 | 146 | 当您使用的语言可能会干扰资源的重新分配时,这种方法特别有用。 147 | 148 | ## 平衡和异常 149 | 150 | 支持异常的语言会使资源重新分配变得棘手。 如果引发异常,您如何保证整理在异常之前分配的所有内容? 答案在某种程度上取决于语言支持。 通常,您有两种选择: 151 | 152 | - 使用变量范围(例如,C++ 或 Rust 中的堆栈变量) 153 | 154 | - 在 try...catch 块中使用 finally 子句 155 | 156 | 使用 C++ 或 Rust 等语言的常用作用域规则,当变量通过返回,块退出或异常超出范围时,将回收变量的内存。 但是,您也可以连接到变量的析构函数,以清理所有外部资源。 在此示例中,当变量超出范围时,名为 accounts 的Rust 变量将自动关闭关联文件: 157 | 158 | ```Rust 159 | { 160 | let mut accounts = File::open("mydata.txt")?; 161 | // >-- 162 | // use 'accounts' 163 | // / 164 | ... 165 | // / 166 | } 167 | // <-- 168 | // 'accounts' is now out of scope, and the file is automatically closed 169 | ``` 170 | 171 | 如果语言支持,则另一个选项是 finally 子句。 finally 子句将确保无论在 try…catch 块中是否引发异常,指定的代码都将运行: 172 | 173 | ```Rust 174 | try 175 | // some dodgy stuff 176 | catch 177 | // exception was raised 178 | finally 179 | // clean up in either case 180 | ``` 181 | 182 | 然而,这里有一个 catch。 183 | 184 | ## 异常反模式 185 | 186 | 我们通常会看到人们写这样的东西: 187 | 188 | ``` 189 | begin 190 | thing = allocate_resource() 191 | process(thing) 192 | finally 193 | deallocate(thing) 194 | end 195 | ``` 196 | 197 | 看到哪里错了吗? 198 | 199 | 如果资源分配失败并引发异常怎么办? finally 子句将捕获它,并尝试取消分配从未分配过的东西。 200 | 201 | 在异常环境中处理资源释放的正确模式是 202 | 203 | ``` 204 | thing = allocate_resource() 205 | begin 206 | process(thing) 207 | finally 208 | deallocate(thing) 209 | end 210 | ``` 211 | 212 | ## 当你无法平衡资源时 213 | 214 | 有时候,基本资源分配模式不恰当。通常,这在使用动态数据结构的程序中可以找到。一个例程将分配一个内存区域,并将其链接到更大的结构中,该结构可能会保留一段时间。 215 | 216 | 这里的技巧是为内存分配建立语义不变式。您需要确定谁负责聚合数据结构中的数据。取消分配顶层结构时会发生什么?您有三个主要选择: 217 | 218 | - 顶层结构还负责释放它包含的所有子结构。然后,这些结构递归删除它们包含的数据,依此类推。 219 | 220 | - 顶级结构被简单地释放。它指向的任何结构(在其他地方未引用)都是孤立的。 221 | 222 | - 如果顶层结构包含任何子结构,则它拒绝释放自身。 223 | 224 | 这里的选择取决于每个单独数据结构的情况。但是,您需要使每个参数都明确,并一致地执行您的决定。用诸如C之类的过程语言实现这些选项中的任何一个都是一个问题:数据结构本身不处于活动状态。在这种情况下,我们倾向于为每个主要结构编写一个模块,为该结构提供标准的分配和解除分配功能。 (此模块还可以提供调试打印,序列化,反序列化和遍历钩子等功能。) 225 | 226 | ## 检查平衡 227 | 228 | 因为务实的程序员不信任任何人,包括我们自己,所以我们认为构建代码来检查资源是否确实适当释放总是一个好主意。 对于大多数应用程序,这通常意味着为每种类型的资源生成包装器,并使用这些包装器跟踪所有分配和释放。 在代码的某些点,程序逻辑将指示资源将处于某种状态:使用包装器对此进行检查。 例如,为请求提供服务的长时间运行的程序可能在其主处理循环的顶部有一个点,在该点它等待下一个请求到达。 这是确保自上次执行循环以来资源使用量没有增加的好地方。 229 | 230 | 在较低但有用的级别上,您可以投资于(除其他外)检查正在运行的程序是否存在内存泄漏的工具。 231 | 232 | ## 相关内容包括 233 | 234 | - 话题 24 [_死程序不说谎_](./死程序不说谎.md) 235 | - 话题 33 [_断开时间耦合_](../Chapter6/断开时间耦合.md) 236 | - 话题 30 [_转换编程_](../Chapter5/转换编程.md) 237 | 238 | ## 挑战 239 | - 尽管没有确保您始终释放资源的保证方法,但是某些设计技术在持续应用时将有所帮助。 在本文中,我们讨论了为主要数据结构建立语义不变性如何指导内存释放分配的决策。 考虑话题 23 [_契约设计_](./契约设计.md) 如何有助于完善这个想法。 240 | 241 | ### 练习 15(尽可能回答) 242 | 243 | 一些 C 和 C++ 开发人员在释放其引用的内存之后,将指针设置为 null 。为什么这是个好主意? 244 | 245 | ### 练习 16(尽可能回答) 246 | 247 | 一些 Java 开发人员在使用完对象后,会将对象变量设置为 NULL。为什么这是个好主意? 248 | -------------------------------------------------------------------------------- /Chapter4/断言式编程.md: -------------------------------------------------------------------------------- 1 | # 断言式编程 2 | 3 | 4 | > _自责是奢侈的。当我们责怪自己时,我们觉得没有人有权利责怪我们。_ 5 | > 6 | > _-- 奥斯卡·王尔德,《道林·格雷的照片》_ 7 | 8 | 似乎每个程序员在职业生涯的早期都必须记住一个咒语。它是计算的一个基本原则,是我们学习应用于需求、设计、代码、注释的核心信念,几乎是我们所做的一切。它就是: 9 | 10 | 这永远不会发生 11 | 12 | “此代码从现在起80年后将不再使用,所以两位数的日期就可以了。” “此应用程序永远不会在国外使用,那么为什么要使其国际化?” “计数不能为负。” “记录不会失败。” 13 | 14 | 我们不要练习这种自我欺骗,尤其是在编码时。 15 | 16 | --- 17 | ## 提示 39 用断言来防止不可能的事情 18 | --- 19 | 20 | 每当您发现自己在思考“但当然永远不会发生”时,请添加代码进行检查。 最简单的方法是使用断言。 在许多语言实现中,您会找到某种形式的 断言 来检查布尔条件。这些检查是无价的。 如果参数或结果永远不应为 null,则应明确检查它: 21 | 22 | assert(result != null); 23 | 24 | 在 Java 实现中,您可以(并且应该)添加一个描述性字符串: 25 | 26 | assert result != null && result.size() > 0:“Empty result from XYZ”; 27 | 28 | 断言对于检查算法的操作也很有用。 也许您已经编写了一个聪明的排序算法,名为 my_sort。 检查它是否有效: 29 | 30 | books = my_sort(find("scifi")) 31 | assert(is_sorted?(books)) 32 | 33 | 当然,传递给断言的条件不能有副作用(请参见后文)。还要记住,断言可能在构建或执行时关闭,永远不要将必须执行的代码放入断言中。 34 | 35 | 不要用断言代替真正的错误处理。断言检查不应该发生的事情:您不希望编写诸如 36 | 37 | puts("Enter 'Y' or 'N': ") 38 | ans = gets[0] # Grab first character of response 39 | assert((ch == 'Y') || (ch == 'N')) # Very bad idea!” 40 | 41 | 仅仅因为大多数断言实现会在断言失败时终止进程,所以没有理由编写版本。如果需要释放资源,请捕获断言的异常或捕获出口,然后运行自己的错误处理程序。只要确保在那些垂死的毫秒内执行的代码不依赖于最初触发断言失败的信息。 42 | 43 | --- 44 | ### 断言和副作用 45 | 46 | 当我们为检测错误而添加的代码最终导致新的错误时,这是很尴尬的。如果评估条件有副作用,则断言可能会发生这种情况。例如,编写诸如 47 | 48 | while (iter.hasMoreElements()) { 49 | assert(iter.nextElement() != null); 50 | Object obj = iter.nextElement(); 51 | // .... 52 | } 53 | 54 | 断言中的 .nextElement() 调用的副作用是将迭代器移过要获取的元素,因此循环将只处理集合中的一半元素。最好还是写 55 | 56 | while (iter.hasMoreElements()) { 57 | Object obj = iter.nextElement(); 58 | assert(obj != null); 59 | // .... 60 | } 61 | 62 | 这个问题是一种 Heisenbug 调试,它改变了被调试系统的行为。一般来说,您应该使用语言构造,其中循环和迭代是由语言本身计算的,使用诸如 map、filter 或 apply 之类的东西来处理列表中的所有元素,而无需手动确定迭代的边界。 63 | 64 | --- 65 | 66 | ## 保持断言处于打开状态 67 | 68 | 关于断言存在一个普遍的误解。它是这样的: 69 | 70 | 断言为代码增加了一些开销。因为他们检查了永远都不会发生的事情,所以它们只会被代码中的错误触发。一旦代码经过测试和发布,就不再需要它们,应该将其关闭以使代码运行更快。断言是一种调试工具。 71 | 72 | 这里有两个明显错误的假设。首先,他们认为测试可以找到所有错误。实际上,对于任何复杂的程序,您甚至都不可能测试将要通过代码的排列的很小一部分。其次,乐观主义者忘记了您的程序在危险的世界中运行。在测试期间,老鼠可能不会咬通讯电缆,玩游戏的人不会耗尽内存,日志文件也不会填满存储分区。当您的程序在生产环境中运行时,可能会发生这些事情。您的第一道防线是检查任何可能的错误,而第二道防线是使用断言来尝试检测您遗漏的错误。 73 | 74 | 将程序交付到生产环境时,关闭断言就像在没有网络的情况下越过高线,因为您曾经在实践中将其跨过。有巨大的价值,但很难获得人寿保险。 75 | 76 | --- 77 | ### 在生产中使用断言,赢得大笔资金 78 | 79 | 安迪(Andy)的一位前邻居领导了一家制造网络设备的小型创业公司。 他们成功的秘诀之一是决定在生产版本中保留断言。 这些声明经过精心设计,可以报告导致失败的所有相关数据,并通过美观的 UI 呈现给最终用户。 来自实际用户在实际条件下的这种反馈水平使开发人员可以填补漏洞并修复这些晦涩难懂的难以重现的错误,从而获得非常稳定的防弹软件。 80 | 81 | 这家不知名的小型公司拥有如此可靠的产品,很快就以数亿美元的价格被收购。 82 | 83 | 只是说说而已。 84 | 85 | --- 86 | 87 | 即使您确实有性能问题,也请仅关闭确实遇到问题的断言。 上面的排序示例可能是应用程序的关键部分,并且可能需要很快。 添加检查意味着再次传递数据,这可能是不可接受的。 使该特定检查为可选,其余部分留在原处。 88 | 89 | ### 练习 14(尽可能回答) 90 | 91 | 快速的现实检查。 哪些“不可能”的事情会发生? 92 | 93 | - 少于 28 天的一个月 94 | - 系统调用中的错误代码:无法访问当前目录 95 | - 在 C++ 中: a = 2; b = 3; 但是(a + b)不等于5 96 | - 内角总和 ≠ 180° 的三角形 97 | - 一分钟没有60秒 98 | - (a + 1) <= a 99 | 100 | ## 相关内容包括 101 | 102 | - 话题 24 [_死程序不说谎_](./死程序不说谎.md) 103 | - 话题 23 [_契约设计_](./契约设计.md) 104 | - 话题 42 [_基于属性的测试_](../Chapter7/基于属性的测试.md) 105 | - 话题 43 [_在某处保持安全_](../Chapter7/在某处保持安全.md) 106 | -------------------------------------------------------------------------------- /Chapter4/死程序不说谎.md: -------------------------------------------------------------------------------- 1 | # 死程序不说谎 2 | 3 | 4 | 您是否注意到,有时别人可以在您自己意识到问题之前就发现您的问题?与其他人的代码相同。如果我们的某个程序开始出现问题,首先捕获它的是库或框架例程。也许我们传入了一个 `nil` 值或一个空列表。可能是该哈希中缺少键,或者我们认为包含哈希的值实际上包含一个列表。可能是我们没有发现网络错误或文件系统错误,并且我们的数据为空或损坏。数百万条指令之前的逻辑错误意味着 case 语句的选择器不再是预期的1、2或3。我们将意外地遇到默认大小写。这也是每个 case/switch 语句都需要使用默认子句的原因之一:我们想知道“不可能”发生的时间。 5 | 6 | 很容易陷入“不可能发生”的想法。我们大多数人编写的代码都无法检查文件是否成功关闭,或者没有按照我们的预期编写跟踪语句。在所有条件都相同的情况下,我们可能不需要这样做—在任何正常情况下,相关代码都不会失败。但是我们是在防御性地编码。我们确保数据就是我们认为的那样,生产中的代码就是我们认为的代码。我们正在检查是否已正确加载依赖项的版本。 7 | 8 | 所有错误都会为您提供信息。您可以使自己确信该错误不会发生,然后选择忽略它。相反,实用程序员告诉自己,如果有错误,则发生了非常非常糟糕的事情。不要忘记阅读该死的错误消息(请参阅 [_陌生土地上的编码员_](../Chapter3/调试.md) ) 9 | 10 | 捕获和释放是为了鱼 11 | 一些开发人员认为捕获或挽救所有异常,并在编写某种消息后重新引发它们是一种很好的样式。 他们的代码充满了这样的内容(其中一个简单的 raise 语句重新引发了当前异常): 12 | 13 | ```elixir 14 | try do 15 | add_score_to_board(score); 16 | rescue InvalidScore 17 | Logger.error("Can't add invalid score. Exiting"); 18 | raise 19 | rescue BoardServerDown 20 | Logger.error("Can't add score: board is down. Exiting"); 21 | raise 22 | rescue StaleTransaction 23 | Logger.error("Can't add score: stale transaction. Exiting"); 24 | raise 25 | end 26 | ``` 27 | 28 | 这是实用程序员的写法: 29 | 30 | ```elixir 31 | add_score_to_board(score) 32 | ``` 33 | 34 | 我们选择它有两个原因。 首先,错误处理不会使应用程序代码黯然失色。 其次,也许更重要的是,代码之间的耦合较少。 在详细示例中,我们必须列出 add_score_to_board 方法可能引发的每个异常。 如果该方法的编写者添加了另一个异常,则我们的代码可能会过时。 在更实用的第二个版本中,新的异常会自动传播。 35 | 36 | --- 37 | ## 提示 38 早期崩溃 38 | --- 39 | 40 | ## 崩溃,不要垃圾 41 | 42 | 让你尽早发现问题的好处之一就是你可以更早地让程序崩溃。而让程序崩溃通常是您可以做的最好的事情。另一种选择是继续操作,将损坏的数据写入某个重要数据库,或命令洗衣机进入其第二十个连续旋转周期。 43 | 44 | Erlang 和 Elixir 语言采用了这种哲学。 Erlang 的发明者,以及《Erlang编程:并发世界的软件》的作者 Joe Armstrong 被经常引用的一句话:“防御性编程是浪费时间。让它崩溃!” 在这些环境中,程序被设计为失败,但是该失败由 supervisor 管理。supervisor 负责运行代码,并且知道在代码失败的情况下该怎么办,这可能包括清理代码,重新启动代码等。supervisor 本身发生故障时会发生什么?它自己的 supervisor 来管理该事件,从而导致由 supervisor 树组成的设计。该技术非常有效,有助于解决这些语言在高可用性,容错系统中的使用。 45 | 46 | 在其他环境中,仅退出正在运行的程序可能是不合适的。您可能已声明可能无法释放的资源,或者您可能需要编写日志消息,整理未完成的事务或与其他进程进行交互。 47 | 48 | 但是,基本原理保持不变-当您的代码发现原本不可能发生的事情刚刚发生时,您的程序将不再可行。从现在开始,它所做的任何事情都值得怀疑,因此请尽快将其终止。 49 | 50 | 失效的程序通常比损坏的程序少得多的损害。 51 | 52 | ## 相关内容包括 53 | - 话题 25 [_断言式编程_](./断言式编程.md) 54 | - 话题 26 [_如何平衡资源_](./如何平衡资源.md) 55 | - 话题 23 [_契约设计_](./契约设计.md) 56 | - 话题 20 [_调试_](../Chapter3/调试.md) 57 | - 话题 43 [_在某处保持安全_](../Chapter7/在某处保持安全.md) 58 | -------------------------------------------------------------------------------- /Chapter4/程序性妄想症.md: -------------------------------------------------------------------------------- 1 | # 程序性妄想症 2 | 3 | 4 | --- 5 | ## 提示 36 你写不出完美的软件 6 | --- 7 | 8 | 这会让你疼吗?不应该。 接受它作为生活的公理。 拥抱它。 庆祝它。 因为不存在完美的软件。 在计算机的简要历史中,没有人曾经编写过一款完美的软件。 您不太可能成为第一位。 除非您接受这一事实,否则最终您将浪费时间和精力去追求一个不可能实现的梦想。 9 | 10 | 那么,鉴于这个令人沮丧的现实,务实程序员如何将其变成优势? 这就是本章的主题。 11 | 12 | 每个人都知道他们自己是地球上唯一的好司机。世界上其他地方都是为了得到他们,吹过停车标志,在车道间穿梭,不打指示灯转弯,在手机上发短信,而且一般都达不到我们的标准。因此,我们开车的时候都防守。我们会在麻烦发生之前就去寻找它们,预见到意外的事情,并且永远不要陷入无法自拔的境地。 13 | 14 | 与编码的类比非常明显。我们一直在与其他人的代码(可能不符合我们的高标准的代码)进行交互,并处理可能有效或无效的输入。因此,我们学会了防御性编码。如有任何疑问,我们将验证所获得的所有信息。我们使用断言来检测不良数据,并不信任来自潜在攻击者或坏的数据。我们检查一致性,在数据库列上设置约束,并且通常对自己感觉很好。 15 | 16 | 但是,实用编程人员将更进一步。他们也不信任自己。务实的程序员知道没人会写完美的代码,包括他们自己,所以会为自己的错误建立防御。我们在话题 23 [_契约设计_](./契约设计.md) 中描述了第一个防御措施:客户和供应商必须就权利和责任达成共识。 17 | 18 | 在话题 24 [_死程序不说谎_](./死程序不说谎.md),我们希望确保在解决问题时不会造成任何损害。因此,我们尝试经常检查并在出现问题时终止程序。 19 | 20 | 话题 25 [_断言式编程_](./断言式编程.md) 描述了一种简单的检查方法-编写可主动验证您的假设的代码。 21 | 22 | 随着程序变得更加动态,您会发现自己在忙于利用系统资源(内存,文件,设备等)。在话题 26 [_如何平衡资源_](./如何平衡资源.md) 中,我们将建议确保您不会丢球的方法。 23 | 24 | 最重要的是,我们始终坚持小步骤,如话题 27 所述,[_别开过头了_](./别开过头了.md),因此我们不会跌落悬崖边缘。 25 | 26 | 在不完善的系统,荒谬的时间尺度,可笑的工具和不可能的要求的世界中,请放心使用。正如伍迪·艾伦(Woody Allen)所说:“当每个人都真正想得到您时,妄想症就是一个好主意。" 27 | -------------------------------------------------------------------------------- /Chapter5/弯曲或折断.md: -------------------------------------------------------------------------------- 1 | # 弯曲或折断 2 | 3 | 4 | 生活不会停滞不前。 5 | 6 | 我们编写的代码也不能。所以为了跟上当今近乎疯狂的变化步伐,我们需要尽一切努力编写尽可能松散(尽可能灵活)的代码。否则我们可能会发现我们的代码很快就变得过时,或者太脆弱而无法修复,并最终可能在对未来的疯狂冲击中落伍。 7 | 8 | 回到话题 11 [_可逆性_](../Chapter2/可逆性.md),我们谈到了不可逆决策的危险。在本章中,我们将告诉您如何做出可逆的决策,以便您的代码在面对不确定的世界时可以保持灵活性和适应性。 9 | 10 | 首先,我们需要研究耦合 - 代码模块之间的依赖关系。在话题 28 [_解耦_](./解耦.md) 中,我们将展示如何使单独的概念保持独立,并减少耦合。 11 | 12 | 接下来,我们将讨论话题 29 [_杂耍现实世界_](./杂耍现实世界.md) 时可以使用的不同技术。我们将研究四种不同的策略来帮助管理事件并对事件做出反应,这是现代软件应用程序的重要方面。 13 | 14 | 传统的面向过程代码和面向对象的代码可能出于您的目的而过于紧密地耦合在一起。在话题 30 [_转换编程_](./转换编程.md) 中,即使您的语言不直接支持函数管道,我们也将利用函数管道提供的更灵活,更清晰的样式。 15 | 16 | 常见的面向对象样式可能会诱使您陷入另一个陷阱。不要为此而屈服,否则您最终将支付沉重的 [_遗产税_](./遗产税.md),这是话题 31 的内容。我们将探索更好的替代方案,以使您的代码更灵活,更容易更改。 17 | 18 | 当然,保持灵活性的一种好方法是编写更少的代码。更改代码会让您引入新的错误。话题 32,[_配置_](./配置.md) 将说明如何将详细信息完全移出代码,从而可以更安全,更轻松地进行更改。 19 | 20 | 有了这些技巧,您就可以编写“随波逐流”的代码。 21 | -------------------------------------------------------------------------------- /Chapter5/杂耍现实世界.md: -------------------------------------------------------------------------------- 1 | # 杂耍现实世界 2 | 3 | > _事情不会随随便便发生;_ 4 | > _它们是注定要发生的_ 5 | > 6 | > _-- 约翰·F·肯尼迪_ 7 | 8 | 在过去,当你的作者仍然有他们男孩般的美貌时,计算机不是特别灵活。我们通常会根据它们的局限性来组织与它们的交互方式。 9 | 10 | 今天,我们期待更多:计算机必须融入我们的世界,而不是相反。我们的世界是混乱的:事情不断地发生,东西四处移动,我们改变了我们的想法,……我们所写的应用程序必须知道怎么做。 11 | 12 | 本节主要讨论如何编写这些响应型应用程序。 13 | 14 | 我们将从事件的概念开始。 15 | 16 | ## 事件 17 | 事件就是信息的可用性。它可能来自外部世界:用户单击按钮,或股票报价更新。它可能是内部的:计算结果就绪,搜索完成。它甚至可以像获取列表中的下一个元素那样微不足道。 18 | 19 | 无论事件源是什么,如果我们编写响应事件的应用程序,并根据这些事件调整它们的工作方式,那么这些应用程序将在现实世界中更好地工作。他们的用户会发现他们更具交互性,应用程序本身也会更好地利用资源。 20 | 21 | 但是我们如何编写这些类型的应用程序呢?如果没有某种策略,我们很快就会发现自己很困惑,我们的应用程序将是一堆紧密耦合的代码。 22 | 23 | 让我们看看有帮助的四个策略。 24 | 25 | - 有限状态机 26 | - 观察者模式 27 | - 发布/订阅 28 | - 反应式编程和流 29 | 30 | ## 有限状态机 31 | 戴夫发现他几乎每周都用有限状态机(FSM)编写代码。通常,FSM 实现只需要几行代码,但这几行代码有助于解决很多潜在的问题。 32 | 33 | 使用 FSM 非常简单,但是许多开发人员都回避使用它。似乎有一种观点认为它们是困难的,或者它们只在您使用硬件时才适用,或者您需要使用一些难以理解的库。这些都不是真的。 34 | 35 | ### 实用 FSM 的剖析 36 | 37 | 状态机基本上只是一个如何处理事件的规范。它由一组状态组成,其中一个是当前状态。对于每个状态,我们列出对该状态重要的事件。对于每个事件,我们定义系统的当前新的状态。 38 | 39 | 例如,我们可能正在从 websocket 接收很多消息。第一条消息是头。随后是任意数量的数据消息。最后我们会收到一条结束的消息。 40 | 41 | 这可以表示为如下的 FSM : 42 | 43 | ![FSM](../assets/topic29_1.png) 44 | 45 | 我们从“初始状态”开始。如果我们接收到头消息,我们就转换到“读取消息”状态。如果我们在初始状态(标记为星号的行)时接收到任何其他信息,我们就转换到“错误”状态,就完成了。 46 | 47 | 当我们处于“读取消息”状态时,我们可以接受数据消息,在这种情况下,我们可以继续以相同的状态读取,也可以接受尾部消息,它将我们转换到“完成”状态。任何其他操作都会导致转换到错误状态。 48 | 49 | FSMs的精妙之处在于,我们可以将它们纯粹地表示为数据。下面是一个表,表示我们的消息解析器: 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
StateEvents
HeaderDataTrailerOther
InitialReadingErrorErrorError
ReadingErrorReadingDoneError
77 | 78 | 表中的行表示状态。若要了解事件发生时要执行的操作,请在行中查找当前状态,并沿行扫描表示该事件的列,该单元格的内容是新状态。 79 | 80 | 处理它的代码同样简单: 81 | 82 | ```ruby 83 | TRANSITIONS = { 84 | initial: {header: :reading}, 85 | reading: {data: :reading, trailer: :done} 86 | } 87 | 88 | state = :initial 89 | 90 | while state != :done && state != :error 91 | msg = get_next_msg() 92 | state = TRANSITIONS[state][msg.msg_type] || :error 93 | end 94 | ``` 95 | 96 | 在第 10 行实现状态之间转换的代码。它使用当前状态索引转换表,然后使用消息类型索引该状态的转换。如果没有匹配的新状态,则将状态设置为 _:error_ 97 | 98 | ### 添加操作 99 | 一个纯粹的 FSM,就像我们刚才看到的,基本上是一个解析器,将传入的事件与状态转换相匹配。它的唯一输出是最终状态。我们可以通过添加在某些转换上触发的操作来增强它。 100 | 101 | 例如,我们可能需要提取源文件中的所有字符串。字符串是引号之间的文本,但字符串中的反斜杠将转义下一个字符,因此 "Ignore\"quotes"" 是单个字符串。这里有一个 FSM 可以做到这一点: 102 | 103 | ![Action](../assets/topic29_2.png) 104 | 105 | 这次,每个转换都有两个标签。上一个是触发它的事件,下一个是我们在不同状态之间移动时要采取的操作。 106 | 我们会用一张表格来表达,就像上次一样。但是,在这种情况下,表中的每个条目都是一个包含下一个状态和操作名称的双元素列表。 107 | 108 | ```ruby 109 | TRANSITIONS = { 110 | # current new state action to take 111 | # -------------------------------- 112 | look_for_string: { 113 | '"' => [:in_string, :start_new_string], 114 | :default => [:look_for_string, :ignore], 115 | }, 116 | 117 | in_string: { 118 | '"' => [:look_for_string, :finish_current_string], 119 | '\\' => [:copy_next_char, :add_current_to_string], 120 | :default => [:in_string, :add_current_to_string], 121 | }, 122 | 123 | copy_next_char: { 124 | :default => [:in_string, :add_current_to_string], 125 | }, 126 | } 127 | ``` 128 | 129 | 我们还添加了指定默认转换的功能,如果事件与此状态的任何其他转换都不匹配,则执行该转换。 130 | 现在让我们看看代码: 131 | 132 | ```ruby 133 | state = :look_for_string 134 | result = [] 135 | while ch = STDIN.getc 136 | state, action = TRANSITIONS[state][ch] || TRANSITIONS[state][:default] 137 | case action 138 | when :ignore 139 | when :start_new_string 140 | result = [] 141 | when :add_current_to_string 142 | result << ch 143 | when :finish_current_string 144 | puts result.join 145 | end 146 | end 147 | ``` 148 | 149 | 这与前面的示例类似,因为我们循环事件(输入中的字符),触发转换。但它比以前的代码做得更多。每次转换的结果都是一个新状态和一个操作的名称。在返回循环之前,我们使用操作名来选择要运行的代码。 150 | 151 | 这段代码非常基本,但它完成了任务。还有许多其他变体:转换表可以对操作使用匿名函数或函数指针,可以将实现状态机的代码包装在单独的类中,并使用其自己的状态,等等。 152 | 153 | 不用说你必须同时处理所有的状态转换。如果你要在你的应用上注册一个用户,当他们输入他们的详细信息,验证他们的电子邮件,同意在线应用现在必须发出的 107 个不同的法律警告时,可能会有一些过渡,等等。将状态保存在外部存储中,并使用它来驱动状态机,是处理此类工作流需求的一种好方法。 154 | 155 | ### 状态机是一个开始 156 | 开发人员对状态机的使用不足,我们鼓励您寻找应用它们的机会。但它们并不能解决所有与事件相关的问题。所以让我们来看看其他一些处理事件的方法。 157 | 158 | ## 观察者模式 159 | 在 _观察者模式_ 中,我们有一个称为可观察事件的事件源,一个观察者列表,对这些事件感兴趣。 160 | 161 | 观察者向可观察对象注册其兴趣,通常是通过传递对要调用的函数的引用。随后,当事件发生时,observate 遍历它的观察者列表,并调用每个观察者传递它的函数。事件作为该调用的参数给定。 162 | 163 | 这里有一个简单的 Ruby 示例。终止器 模块用于终止应用程序。但是,在执行此操作之前,它会通知所有观察者应用程序将退出。他们可能会使用此通知来整理临时资源、提交数据等。 164 | 165 | ```ruby 166 | module Terminator 167 | CALLBACKS = [] 168 | 169 | def self.register(callback) 170 | CALLBACKS << callback 171 | end 172 | 173 | def self.exit(exit_status) 174 | CALLBACKS.each { 175 | |callback| callback.(exit_status) 176 | exit!(exit_status) 177 | } 178 | end 179 | end 180 | ``` 181 | 182 | ```shell 183 | Terminator.register(-> (status) { puts "callback 1 sees #{status}"}) 184 | Terminator.register(-> (status) { puts "callback 2 sees #{status}"}) 185 | 186 | Terminator.exit(99) 187 | $ ruby event/observer.rb 188 | callback 1 sees 99 189 | callback 2 sees 99 190 | ``` 191 | 192 | 创建一个observate并不需要太多代码:您将一个函数引用推送到一个列表上,然后在事件发生时调用这些函数。这是不使用库的一个很好的例子。 193 | 194 | 观察者/可观测模式已经使用了几十年,它对我们很有帮助。它在用户界面系统中特别流行,在这些系统中,回调用于通知应用程序发生了一些交互。 195 | 196 | 但是观察者模式有一个问题:因为每个观察者都必须向被观察者注册,所以它引入了耦合。此外,因为在典型的实现中,回调是由可观察的、同步的内联处理的,所以它会引入性能瓶颈。 197 | 198 | 下一个策略 发布/订阅 解决了这个问题。 199 | 200 | ## 发布/订阅 201 | 发布/订阅(pubsub)扩展了观察者模式,同时解决了耦合和性能问题。 202 | 203 | 在 pubsub 模型中,我们有发布者和订阅者。这些是通过 channel 连接的。channel 在单独的代码体中实现:有时是库,有时是进程,有时是分布式基础设施。所有这些实现细节都隐藏在代码中。 204 | 205 | 每个 channel 都有名字。订阅者在一个或多个命名 channel 中注册兴趣,发布者向其写入事件。与观察者模式不同,发布者和订阅者之间的通信是在代码外部处理的,并且可能是异步的。 206 | 207 | 尽管您可以自己实现一个非常基本的 pubsub 系统,但您可能不想这样做。大多数云服务都提供了 pubsub 服务,允许您连接世界各地的应用程序。每种流行语言都至少有一个 pubsub 库。 208 | 209 | Pubsub 是一种很好的分离异步事件处理的技术。它允许在应用程序运行时添加和替换代码,而无需更改现有代码。缺点是,在大量使用 pubsub 的系统中很难看到发生了什么:您不能查看发布者,也不能立即看到哪些订阅者与特定消息有关。 210 | 211 | 与观察者模式相比,pubsub 是一个通过共享接口(通道)进行抽象来减少耦合的好例子。然而,它基本上仍然只是一个消息传递系统。创建对事件组合做出响应的系统需要的不仅仅是这些,所以让我们来看看如何将时间维度添加到事件处理中。 212 | 213 | ## 反应式编程、流和事件 214 | 如果您曾经使用过电子表格,那么您将熟悉反应式编程。如果单元格包含引用第二个单元格的公式,则更新该第二个单元格也会导致第一个单元格更新。这些值随着它们使用的值的变化而变化。 215 | 216 | 有许多框架可以帮助实现这种数据级的反应性:在浏览器领域,React 和 Vue.js 是当前最受欢迎的(但是,这是JavaScript,在这本书出版之前,这些信息就已经过时了)。 217 | 218 | 很明显,事件也可以用来触发代码中的反应,但不一定容易查明它们。那是流流入的地方。 219 | 220 | 流允许我们将事件视为数据的集合。好像我们有一个活动列表,当新的活动到来时,列表会变长。它的美妙之处在于,我们可以像对待任何其他集合一样对待流:我们可以操作、组合、过滤和执行我们非常熟悉的所有其他数据相关的事情。我们甚至可以组合事件流和常规集合。流可以是异步的,这意味着您的代码有机会在事件到达时响应它们。 221 | 222 | 反应性事件处理的当前事实基线在网站 http://reactivex.io 上定义,该网站定义了一组与语言无关的原则,并记录了一些常见的实现。这里我们将使用 JavaScript 的 RxJs 库 223 | 224 | 我们的第一个示例采用两个流并将它们压缩在一起:结果是一个新流,其中每个元素包含第一个 inout 流中的一个项和另一个 inout 流中的一个项。在本例中,第一个流只是5个动物名称的列表。第二个流更有趣:它是一个间隔计时器,每 500 毫秒生成一个事件。因为流是压缩在一起的,所以只有当两个流上都有可用的数据时才会生成一个结果,因此我们的结果流每半秒只发出一个值。 225 | 226 | ```js 227 | import * as Observable from 'rxjs' 228 | import { logValues } from './logger.js' 229 | let animals = Observable.of("ant", "bee", "cat", "dog", "elk") 230 | let tiker = Obverable.interval(500) 231 | let combined = Observable.zip(animals, ticker) 232 | combined.subscribe(next => logValues(JSON.stringify(next))) 233 | ``` 234 | 235 | 这段代码使用了一个简单的日志记录函数,它将项目添加到浏览器窗口的列表中。每个项目都用程序开始运行后的时间(毫秒)作为时间戳。下面是我们的代码得到的: 236 | 237 | ![rxjs 500](../assets/topic29_3.png) 238 | 239 | 注意时间戳:我们每500毫秒从流中获取一个事件。每个事件包含一个序列号(由可观察到的 间隔 创建)和列表中下一个动物的名称。看着它在浏览器中运行,日志行每半秒出现一次。 240 | 241 | 事件流通常在事件发生时填充,这意味着填充它们的可观测数据可以并行运行。下面是一个从远程站点获取用户信息的示例。为此,我们将使用 https://reqres.in ,这是一个提供开放 REST 接口的公共站点。作为其 API 的一部分,我们可以通过对 users/«id» 执行 GET 请求来获取特定(假的)用户的数据。我们的代码获取 ID 为 3、2 和 1 的用户。 242 | 243 | ```js 244 | import * as Observable from 'rxjs' 245 | import { mergeMap } from 'rxjs/operators' 246 | import { ajax } from 'rxjs/ajax' 247 | import { logValues } from './logger.js' 248 | let users = Observable.of(3, 2, 1) 249 | let result = users.pipe( 250 | mergeMap((user) => ajax.getJSON(`https://reqres.in/api/users/${user}`)) 251 | ) 252 | result.subscribe( 253 | resp => logValues(JSON.stringify(resp.data)), 254 | err => console.error(JSON.stringify(err)) 255 | ) 256 | ``` 257 | 258 | 代码的内部细节并不太重要。令人兴奋的是结果: 259 | 260 | ![rxjs 50](../assets/topic29_4.png) 261 | 262 | 看看时间戳:这三个请求,或者说是三个独立的流,是并行处理的,第一个返回,对于 id 2,需要 82ms,接下来的两个返回 50 和 51ms。 263 | 264 | ### 事件流是异步集合 265 | 在前面的例子中,我们的用户 ID 列表(在可观察的 用户 中)是静态的。但这不一定。也许我们想在人们登录我们的网站时收集这些信息。我们所要做的就是在创建会话时生成一个包含用户 id 的可观察事件,并使用该可观察事件而不是静态事件。然后,我们会在收到这些 id 时获取用户的详细信息,并可能将它们存储在某个地方。 266 | 267 | 这是一个非常强大的抽象概念:我们不再需要把时间看作是我们必须管理的东西。事件流将同步和异步处理统一在一个通用、方便的 API 后面。 268 | 269 | ## 事件无处不在 270 | 事件无处不在。有些是显而易见的:一个按钮点击,一个计时器过期。另一些则不那么重要:有人登录,文件中的一行与模式匹配。但无论它们的源代码是什么,围绕事件精心编制的代码都可以比其线性对应的代码更具响应性和更好的解耦。 271 | 272 | ## 相关内容包括 273 | 274 | - 话题 28 [_解耦_](./解耦.md) 275 | - 话题 36 [_黑板_](../Chapter6/黑板.md) 276 | 277 | ## 练习 278 | 279 | ### 练习 17(尽可能回答) 280 | 在 FSM 部分中,我们提到可以将通用状态机实现移动到它自己的类中。该类可能通过传入转换表和初始状态来初始化。 281 | 尝试以这种方式实现字符串提取器。 282 | 283 | ### 练习 18(尽可能回答) 284 | 这些技术中哪一种(可能是组合使用的)最适合以下情况: 285 | 286 | - 如果您在五分钟内收到三个网络接口关闭事件,请通知操作人员。 287 | - 如果是日落后,在楼梯底部检测到运动,然后在楼梯顶部检测到运动,请打开楼上的灯。 288 | - 您要通知各种报告系统订单已完成。 289 | - 为了确定客户是否有资格获得汽车贷款,应用程序需要向三个后端服务发送请求并等待响应。 290 | -------------------------------------------------------------------------------- /Chapter5/继承税.md: -------------------------------------------------------------------------------- 1 | # 继承税 2 | 3 | 4 | > _你想要一个香蕉,但你得到的是一只大猩猩拿着香蕉和整个丛林。_ 5 | > 6 | > _-- 乔 阿姆斯特朗_ 7 | 8 | 你使用面向对象语言编程吗?你使用继承吗? 9 | 10 | 如果是,请停下来。这很可能并不是你想要做的。 11 | 12 | 让我们来看看为什么。 13 | 14 | ## 一些背景 15 | 继承最早出现在 1969 年的 模拟语言 Simula67中。这是解决在同一个列表上对多种类型的事件进行排队的问题的一个很好的解决方案。Simula方法是使用前缀类。你可以这样写: 16 | 17 | link CLASS car; 18 | ... implementation of car 19 | link CLASS bicycle; 20 | ... implementation of bicycle 21 | 22 | 然后你可以把汽车和自行车都加到在红绿灯处等着的东西的列表里。在当前的术语中,link 将是父类。 23 | 24 | Simula 程序员使用的思想模型是,在实现类 car 和 bicycle 之前,预先准备好实例数据和类 link 的实现。link 部分几乎被视为一个集装箱,用来运载汽车和自行车。这给了他们一种多态性:汽车和自行车都实现了 link 接口,因为它们都包含 link 代码。 25 | 26 | Simula 之后是 Smalltalk。Smalltalk 的创始人之一艾伦•凯(Alan Kay)在 2019 年的一份 Quora 答卷中描述了Smalltalk 拥有继承的原因。 27 | 28 | 因此,当我设计 Smalltalk-72 时,想到 Smalltalk-71 时,我觉得用它类似 Lisp 的动力学来做“差分编程”的实验是很有趣的(意思是:用各种方法来实现“除了这一点以外,这就是它”)。 29 | 30 | 这是纯粹为了行为的子类化。 31 | 32 | 这两种继承方式(实际上有相当数量的共同点)在接下来的几十年里发展起来。Simula方法,它建议继承是一种组合类型的方法,在 C++ 和 java 语言中继续使用。Smalltalk 学派是一个动态的行为组织,它出现在 Ruby 和 JavaScript 等语言中。 33 | 34 | 因此,现在我们面对的是一代使用继承的面向对象开发人员,原因有二: 35 | 36 | - 他们不喜欢打字 37 | - 他们喜欢类型 38 | 39 | 那些不喜欢键入的人可以通过使用继承将基类中的公共功能添加到子类中来节省开支:类 User 和类 Product 都是ActiveRecord::Base 的子类。 40 | 41 | 喜欢类型的人使用继承来表示类之间的关系:Car 是一种 Vehicle。 42 | 43 | 不幸的是,这两种继承都有问题。 44 | 45 | ## 使用继承共享代码的问题 46 | 继承是耦合的。子类不仅耦合到父类、父类的父类等;而且使用子类的代码也耦合到所有祖先。 47 | 48 | ```ruby 49 | class Vehicle 50 | def initialize 51 | @speed = 0 52 | end 53 | 54 | def stop 55 | @speed = 0 56 | end 57 | 58 | def mova_at(speed) 59 | @speed = speed 60 | end 61 | end 62 | class Car < Vehicle 63 | def info 64 | "I'm car driving at #{@speed}" 65 | end 66 | end 67 | 68 | # top-level code 69 | my_car = Car.new 70 | mycar.move_at(30) 71 | ``` 72 | 73 | 当顶级调用 my_car.move_at 时,调用的方法是在 Car 的父级 Vehicle 中。 74 | 75 | 现在负责 Vehicle 的开发人员更改了 API,所以 move_at 变成了 set_velocity,实例变量 @speed 变成了@velocity。 76 | 77 | 预计 API 更改将中断类 Vehicle 的客户端。但最高层并不是:就它而言,它使用的是 Car。Car 类在实现方面所做的并不是顶层代码所关心的,但它仍然会中断。 78 | 79 | 类似地,实例变量的名称纯粹是一个内部实现细节,但是当 Vehicle 改变时,它也(无声地)中断 Car。 80 | 81 | 如此多的耦合。 82 | 83 | ### 使用继承生成类型的问题 84 | 有些人认为继承是定义新类型的一种方式。他们最喜欢的设计图显示了类层次结构。他们像维多利亚时代的绅士科学家看待自然的方式看待问题,把问题分成不同的类别。 85 | 86 | ![problem](../assets/topic31_1.png) 87 | 88 | 不幸的是,这些图表很快就会变成覆盖着墙壁的怪物,一层一层地添加,以表达类之间最小的差别。这种增加的复杂性会使应用程序更加脆弱,因为更改会在许多层上下波动。 89 | 90 | ![problem](../assets/topic31_2.png) 91 | 92 | 不过,更糟糕的是多重继承问题。Car 可以是一种 Vehicle,但也可以是一种 Asset, InsuredItem, LoadCollateral 等。正确建模需要多重继承。 93 | 94 | 20世纪90年代,由于存在一些歧义消除的语义,C++赋予了多重继承一个坏名字。因此,许多当前的OO语言都不提供它。所以,即使你对复杂类型的树很满意,你也不能准确地建模你的域。 95 | 96 | --- 97 | ## 提示 51 不要付继承税 98 | --- 99 | 100 | ## 其他选择更好 101 | 让我们建议三种技术,这意味着您不应该再使用继承: 102 | 103 | - 接口和协议 104 | - 委托 105 | - 混合与特征 106 | 107 | ### 接口和协议 108 | 大多数OO语言允许您指定一个类实现一组或多组行为。例如,可以说 Car 类实现了 Drivable 行为和 Locatable 行为。用于执行此操作的语法各不相同:在 Java 中,可能如下所示: 109 | 110 | ```java 111 | public class Car implements Drivable, Locatable { 112 | // ... 113 | } 114 | ``` 115 | 116 | Drivable 和 Locatable 在 Java 里称为接口;其他语言称它们为协议,有些称它们为 trait(尽管这不是我们稍后将要调用的trait)。 117 | 118 | 接口的定义如下: 119 | 120 | ```java 121 | public interface Drivable() { 122 | double getSpeed(); 123 | void stop(); 124 | } 125 | 126 | public interface Locatable() { 127 | Coordinate getLocation(); 128 | boolean locationIsValid(); 129 | } 130 | ``` 131 | 132 | 这些声明不创建代码:它们只是说,实现 Drivable 的任何类都必须实现 getSpeed 和 stop 两个方法,而 Locatable 的类必须实现 getLocation 和 locationIsValid。这意味着,我们以前的 Car 类定义只有包含所有这四个方法时才有效。 133 | 134 | 接口和协议之所以如此强大,是因为我们可以将它们用作类型,而实现适当接口的任何类都将与该类型兼容。如果Car 和 Phone 都实现 Locatable ,我们可以将它们存储在可定位项目列表中: 135 | 136 | ```java 137 | List items = new ArrayList<>(); 138 | items.add(new Car(...)); 139 | items.add(new Phone(...)); 140 | items.add(new Car(...)); 141 | // ... 142 | ``` 143 | 144 | 然后我们可以安全地处理该列表,因为我们知道每个项目都有 getLocation 和 locationIsValid。 145 | 146 | ```java 147 | void printLocation(Locatable item) { 148 | if (item.locationIsValid) { 149 | print(item.getLocation().asString()); 150 | } 151 | 152 | // ... 153 | 154 | items.forEach(printLocation) 155 | } 156 | ``` 157 | 158 | --- 159 | ## 提示 52 更喜欢接口来表示多态性 160 | --- 161 | 162 | 接口和协议给了我们无继承性的多态性。 163 | 164 | ### 委托 165 | 继承鼓励开发人员创建对象具有大量方法的类。如果一个父类有20个方法,而子类只想使用其中的两个,那么它的对象仍然会有另外18个方法,而这些方法是可以调用的。类已失去对其接口的控制。这是一个常见的问题:许多持久性和UI框架都坚持要求应用程序组件子类化某些提供的基类: 166 | 167 | ```ruby 168 | class Account < PersitenceBaseClass 169 | end 170 | ``` 171 | 172 | Account类现在携带了持久性类的所有API。相反,设想一种使用委托的替代方法: 173 | 174 | ```ruby 175 | class Account 176 | def initialize(...) 177 | @repo = Persister.for(self) 178 | end 179 | def save 180 | @repo.save() 181 | end 182 | end 183 | ``` 184 | 185 | 我们现在不向 Account 类的客户端公开任何框架API:这种分离现在已经被打破。但还有更多。现在我们不再受我们使用的框架的 API 的约束,我们可以自由地创建我们需要的 API。是的,我们以前可以这样做,但是我们总是冒着这样的风险:我们编写的接口可能被绕过,而持久性 API 则被使用。现在我们控制一切。 186 | 187 | --- 188 | ## 提示 53 委托给服务:Has-A 胜过 Is-A 189 | --- 190 | 191 | 事实上,我们可以更进一步。为什么一个 Account 必须知道如何保存自己?它的工作不是了解并执行帐户业务规则吗? 192 | 193 | ```ruby 194 | class Account 195 | # nothing but account stuff 196 | end 197 | 198 | class AccountRecord 199 | # wraps an account with the ability 200 | # to be fetched and stored 201 | end 202 | ``` 203 | 204 | 现在我们真的解耦了,但这是要付出代价的。我们必须编写更多的代码,其中一些代码通常是样板文件:例如,很可能我们所有的记录类都需要 find 方法。 205 | 206 | 幸运的是,这就是 mixins 和 traits 对我们的作用。 207 | 208 | ### 混合,特征,类别,协议扩展… 209 | 作为一个行业,我们喜欢给事物命名。我们常常给同一件事起许多名字。越多越好,对吧? 210 | 211 | 这就是我们在看混合时要处理的问题。基本思想很简单:我们希望能够使用新功能扩展类和对象,而不使用继承。所以我们创建了一组这些函数,给它们命名,然后用它们扩展一个类或对象。在这一点上,您已经创建了一个新的类或对象,该类或对象结合了原始类及其所有混合的功能。在大多数情况下,即使您无法访问要扩展的类的源代码,也可以进行此扩展。 212 | 213 | 现在,这个特性的实现和名称因语言而异。我们倾向于在这里称它们为 mixins,但我们真的希望您将其视为语言不可知的特性。我们将集中讨论所有这些实现所具有的功能:将现有功能与新功能合并。 214 | 215 | 作为一个例子,让我们回到我们的 AccountRecord 例子。之前,AccountRecord 需要同时了解帐户以及我们保存它的框架。它还需要将持久层中的所有方法委托给外部世界。 216 | 217 | 混合给了我们另一种选择。首先,我们可以编写一个 mixin 来实现(例如)三个标准 finder 方法中的两个。然后我们可以将它们作为 mixin 添加到 AccountRecord 中。而且,当我们为持久化的东西编写新类时,我们也可以向它们添加 mixin。 218 | 219 | ```ruby 220 | mixin CommonFinders { 221 | def find(id) { ... } 222 | def findAll() { ... } 223 | } 224 | 225 | class AccountRecord extends BasicRecord with CommonFinders 226 | class OrderRecord extends BasicRecord with CommonFinders 227 | ``` 228 | 229 | 我们可以更进一步。例如,我们都知道我们的业务对象需要验证代码来防止坏数据渗透到我们的计算中。但我们所说的 验证 到底是什么意思? 230 | 231 | 例如,如果我们拿到一个账户,可能有许多不同的验证层可以应用: 232 | 233 | - 验证哈希密码是否与用户输入的密码匹配 234 | - 创建帐户时验证用户输入的表单数据 235 | - 正在验证更新用户详细信息的管理员输入的表单数据 236 | - 正在验证其他系统组件添加到帐户的数据 237 | - 在数据被持久化之前验证其一致性 238 | 239 | 一种常见的(我们认为不太理想的)方法是将所有验证捆绑到一个类(业务对象/持久性对象)中,然后添加标志来控制在哪种情况下触发。 240 | 241 | 我们认为更好的方法是使用 mixin 为适当的情况创建专门的类: 242 | 243 | ```ruby 244 | class AccountForCustomer extend Account 245 | with AccountValidations, AccountCustomerValidations 246 | 247 | class AccountForAdmin extend Account 248 | with AccountValidations, AccountAdminValidations 249 | ``` 250 | 251 | 在这里,两个派生类都包含对所有帐户对象通用的验证。客户变型还包括适合面向客户的 api 的验证,而管理员变型包含(可能限制较少的)管理验证。 252 | 253 | 现在,通过来回传递 AccountForCustomer 或 AccountForAdmin 的实例,我们的代码自动确保应用了正确的验证。 254 | 255 | --- 256 | ## 技巧 54 使用 Mixins 共享功能 257 | --- 258 | 259 | ## 继承很少是答案 260 | 我们快速了解了传统类继承的三种替代方法: 261 | 262 | - 接口和协议 263 | - 委托 264 | - 混合与特征 265 | 266 | 根据您的目标是共享类型信息、添加功能还是共享方法,这些方法在不同的情况下可能更适合您。与编程中的任何事情一样,目标是使用最能表达您意图的技术。 267 | 268 | 尽量不要把整个丛林都拖过去。 269 | 270 | ## 相关内容包括 271 | 272 | - 话题 28 [_解耦_](./解耦.md) 273 | - 话题 8 [_好设计的本质_](../Chapter2/好设计的本质.md) 274 | - 话题 10 [_正交性_](../Chapter2/正交性.md) 275 | 276 | ## 挑战 277 | - 下一次你发现自己是子类化的时候,花点时间检查一下选项。你能用接口、委托或混合实现你想要的吗?你这样做能减少耦合吗? 278 | -------------------------------------------------------------------------------- /Chapter5/解耦.md: -------------------------------------------------------------------------------- 1 | # 解耦 2 | 3 | 4 | > _当我们试图自己挑选东西的时候,我们发现它与宇宙中的其他一切联系在一起。_ 5 | > 6 | > _-- 约翰·缪尔,《我在塞拉的第一个夏天》_ 7 | 8 | 在话题 8 中,[_好设计的本质_](../Chapter2/好设计的本质.md) 我们主张使用良好的设计原则将使您编写的代码易于更改。 耦合是变化的敌人,因为它将必须并行变化的事物链接在一起。 这使更改变得更加困难:要么花时间跟踪所有需要更改的部分,要么花时间思考为什么当您更改“仅一件事”而不是与之关联的其他事情时事情就破裂了。 9 | 10 | 当您要设计刚性的东西时,也许是桥或塔,就需要将组件耦合在一起。 11 | 12 | ![桥或塔](../assets/topic28_1.png) 13 | 14 | 您不能更改任何单个链接的长度而不影响其他链接:这就是使结构僵化的原因。 15 | 16 | 将此与以下内容进行比较: 17 | 18 | ![单链](../assets/topic28_2.png) 19 | 20 | 这里没有结构上的僵化:单个链接可以更改,而其他链接可以容纳它。 21 | 22 | 在设计桥梁时,您希望它们保持其形状。 您需要他们变得僵硬。 但是,当您设计要更改的软件时,您恰恰相反:您希望它具有灵活性。 并且为了灵活起见,应将各个组件耦合到尽可能少的其他组件。 23 | 24 | 更糟糕的是,耦合是传递的:如果 A 跟 B 和 C 耦合 ,B 跟 M 和 N 耦合,C 跟 X 和 Y 耦合,那么 A 实际上跟 B、C、M、N、X 和 Y 都耦合。 25 | 26 | 这意味着你应该遵循一个简单的原则: 27 | 28 | --- 29 | ## 技巧 44 解耦的代码更容易更改 30 | --- 31 | 32 | 考虑到我们通常不使用钢梁和铆钉进行编码,那么编码解耦意味着什么呢?在本节中,我们将讨论: 33 | 34 | - 火车残骸 - 方法调用链 35 | 36 | - 全局化 - 静态事物的危险 37 | 38 | - 继承 - 为什么子类化是危险的 39 | 40 | 在某种程度上,这个列表是人为的:耦合可能发生在两段代码共享某个东西的任何时候,因此当您阅读下面的内容时,请注意底层模式,以便将它们应用到您的代码中。注意一些耦合的症状: 41 | 42 | - 不相关模块或库之间古怪的依赖关系。 43 | 44 | - 对一个模块来说,“简单”的更改是指: 45 | 46 | - 通过系统中不相关的模块传播; 47 | 48 | - 或者在系统的其他地方打破东西。系统中不相关的模块。 49 | 50 | - 那些不敢更改代码的开发人员主要是因为他们不确定会受到什么影响。 51 | 52 | - 每个人都必须参加会议,因为没有人确定谁会受到变化的影响。 53 | 54 | ## 火车残骸 55 | 56 | 我们都见过(可能也写过)这样的代码: 57 | 58 | ```java 59 | public void applyDiscount(customer, order_id, discount) { 60 | totals = customer 61 | .orders 62 | .find(order_id) 63 | .getTotals; 64 | 65 | totals.grandTotdal = totals.grandTotal - discount; 66 | totals.discount = discount; 67 | } 68 | ``` 69 | 70 | 我们从一个 customer 对象中获取到一些订单的引用,使用它来查找一个特定的订单,然后获取订单的总价格。使用这些总价格,我们从订单总价格减去折扣,并用该折扣更新它们。 71 | 72 | 这段代码跨越了五个抽象层次,从客户到总量。最终,我们的顶层代码必须知道,customer 对象公开了 orders,orders 有一个 find 方法,该方法接受一个 order id 并返回一个 order,order 对象有一个 totals 对象,totals 对象有一个 getter 和 setter,用于汇总和折扣。这是很多隐性知识。但更糟糕的是,如果这段代码要继续工作,很多事情在未来是无法改变的。在一列火车里的所有的车都是我们耦合在一起的,就像在一列失事的火车里的所有的方法和属性一样。 73 | 74 | 让我们想象一下,企业认为任何订单的折扣都不能超过 40%。我们将把执行这一规则的代码放在哪里? 75 | 76 | 你可能会说它属于我们刚刚编写的 applyDiscount 函数。这当然是答案的一部分。但是现在的代码,你不知道这就是全部的答案。任何一段代码,任何地方,都可以在 totals 对象中设置字段,如果代码的维护者没有得到备忘录,它就不会检查新策略。 77 | 78 | 看待这一点的一种方法是考虑责任。totals 对象似乎应该负责管理总价格。但事实并非如此:它实际上只是一个容器,可供任何人查询和更新一堆字段。 79 | 80 | 解决方法是应用我们称之为: 81 | 82 | --- 83 | ## 提示 45 说明,不要问 84 | --- 85 | 86 | 这个原则说,你不应该根据一个对象的内部状态来做决定,然后再更新这个对象。这样做会完全破坏封装的好处,并在代码中传播实现的知识。 87 | 88 | 因此,我们火车失事的第一个解决方案是将折扣委托给总价格: 89 | 90 | ```java 91 | public void applyDiscount(customer, order_id, discount) { 92 | customer 93 | .orders 94 | .find(order_id) 95 | .getTotals() 96 | .applyDiscount(discount); 97 | } 98 | ``` 99 | 100 | 我们对 customer 对象及其订单也有同样的 tell-don't-ask(TDA)问题:我们不应该获取它的订单列表并搜索它们。我们应该直接从顾客那里得到我们想要的订单。 101 | 102 | ```java 103 | public void applyDiscount(customer, order_id, discount) { 104 | customer 105 | .orders 106 | .findOrder(order_id) 107 | .getTotals() 108 | .applyDiscount(discount); 109 | } 110 | ``` 111 | 112 | 同样的事情也适用于我们的 order 对象及其总数。为什么外部世界必须知道订单的实现使用一个单独的对象来存储其总数? 113 | 114 | ```java 115 | public void applyDiscount(customer, order_id, discount) { 116 | customer 117 | .findOrder(order_id) 118 | .applyDiscount(discount); 119 | } 120 | ``` 121 | 122 | 到这里我们可能就停下来了。 123 | 124 | 此时,您可能认为 TDA 将使我们向 customers 添加一个 applyDiscountToOrder(order_id) 方法。而且,如果被奴性地跟踪,它会的。 125 | 126 | 但 TDA 不是自然规律,它只是帮助我们认识问题的一种模式。在这种情况下,我们很容易暴露这样一个事实:customers 有 orders,我们可以通过询问 customer 对象来找到其中一个 order 。这是一个务实的决定。 127 | 128 | 在每个应用程序中都有一些通用的顶级概念。在这个应用程序中,这些概念包括 customer 和 order。完全将 order 隐藏在 customer 对象中是没有意义的:它们有自己的存在。因此,我们可以创建公开 order 对象的 api。 129 | 130 | ## 迪米特法则 131 | 132 | 人们经常谈论有关耦合的称为德米特法则(LoD)的东西。LoD 是由 Ian Holland 在80年代末编写的一套指南。他创建它们是为了帮助 Demeter 项目的开发人员保持其功能的干净和分离。 133 | 134 | LoD 表示,在类 C 中定义的方法只应调用: 135 | 136 | - C语言中的其他实例方法 137 | 138 | - 它的参数 139 | 140 | - 在堆栈和堆中创建的对象中的方法 141 | 142 | - 全局变量 143 | 144 | 在这本书的第一版中,我们花了一些时间来描述 LoD。在这中间的 20 年里,那朵玫瑰的花期已经褪去了。我们现在不喜欢“global variable”子句(原因我们将在下一节讨论)。我们还发现在实践中很难使用它:这有点像在调用方法时必须解析法律文档。 145 | 146 | 然而,这一原则仍然是正确的。我们只是建议用一种更简单的方式来表达几乎相同的东西: 147 | 148 | --- 149 | ## 提示 46 不链式调用方法 150 | --- 151 | 152 | 当你访问某物时,尽量不要有多个“.”。访问还包括使用中间变量的情况,如下代码所示: 153 | 154 | ```java 155 | # This is pretty poor style 156 | 157 | amount = customer.orders.last().totals().amount 158 | 159 | # and so is this 160 | 161 | orders = customer.orders; 162 | last = orders.last(); 163 | totals = last.totals(); 164 | amount = totals.amount(); 165 | ``` 166 | 167 | 一个 “.” 法则有一个很大的例外:如果你链接的东西真的,真的不太可能改变,那么这个法则就不适用。实际上,应用程序中的任何内容都应该被视为可能发生更改。第三方库中的任何内容都应该被视为不稳定的,特别是当已知第三方库的维护者在不同版本之间更改 api 时。不过,该语言附带的库可能非常稳定,因此我们很乐意使用以下代码: 168 | 169 | ```java 170 | people 171 | .sort_by {| person | person.age} 172 | .first(10) 173 | .map(| person | person.name) 174 | ``` 175 | 176 | 20 年前,当我们编写第一版的时候,Ruby 代码就已经工作了,而且当我们进入老程序员的家(现在的任何一天…)的时候,Ruby 代码仍然可能工作。 177 | 178 | ## 链式和管道 179 | 180 | 在话题 30 [_转换编程_](./转换编程.md) 中,我们讨论如何将函数组合为管道。这些管道转换数据,将数据从一个函数传递到下一个函数。这与方法调用的火车残骸不同,因为我们不依赖隐藏的实现细节。 181 | 182 | 这并不是说管道不引入耦合:它们引入耦合。管道中一个函数返回的数据格式必须与下一个函数接受的格式兼 183 | 容。 184 | 185 | 我们的经验是,这种形式的耦合远不是改变由火车引入的形式破坏的代码的障碍。 186 | 187 | ## 全局化的罪恶 188 | 189 | 全局可访问的数据是应用程序组件之间耦合的潜在来源。每一条全局数据的作用就好像应用程序中的每一个方法突然获得了一个额外的参数:毕竟,每个方法中都有全局数据可用。 190 | 191 | Globals 耦合代码有很多原因。最明显的是,对全局实现的更改可能会影响系统中的所有代码。当然,在实践中,影响是相当有限的;问题的关键在于知道你已经找到了每一个需要改变的地方。 192 | 193 | 当涉及到将代码分开时,全局数据也会创建耦合。 194 | 195 | 代码重用的好处已经做了很多工作。我们的经验是,在创建代码时,重用可能不应该是主要的关注点,但是使代码可重用的思想应该是编码例程的一部分。当您使代码可重用时,您将为它提供干净的接口,使其与其他代码分离。这允许您提取一个方法或模块,而不必拖拽其他任何东西。如果您的代码使用全局数据,则很难将其与其他代码分离。 196 | 197 | 当您为使用全局数据的代码编写单元测试时,就会出现这样的例子。您将发现自己正在连接一堆设置代码,以创建一个全局环境,从而允许您的测试运行。 198 | 199 | --- 200 | ## 避免全局数据 201 | --- 202 | 203 | ### 全局数据包括单例 204 | 205 | 在前一节中,我们仔细讨论了全局数据,而不是全局变量。那是因为人们经常告诉我们“看!没有全局变量。我将其全部打包为一个单例对象或全局模块中的实例数据。” 206 | 207 | 再试一次,Skippy。如果您只有一个带有一堆导出实例变量的单例,那么它仍然只是全局数据。它只是有一个更长的名字。 208 | 209 | 所以人们就用这个单例将数据隐藏在方法后面。他们现在说的是 Config.log_level() 或 Config.getLogLevel(),而不是对 Config.log_level 进行编码。 210 | 211 | 这更好,因为这意味着你的全局数据背后有一点智能。如果决定更改日志级别的表示形式,可以通过在 API 中配置映射新的和旧的来保持兼容性。但您仍然只有一组配置数据。 212 | 213 | ### 全局数据包括外部资源 214 | 215 | 任何可变的外部资源都是全局数据。如果您的应用程序使用数据库、数据存储、文件系统、服务 API 等,则有落入全局陷阱的风险。同样,解决方案是确保始终将这些资源包装在您控制的代码后面。 216 | 217 | --- 218 | ## 提示 48 如果它足够重要,可以是全局的,就用一个 API 包起来 219 | --- 220 | 221 | (但前提是你真的,真的希望它是全局性的) 222 | 223 | ## 继承增加耦合 224 | 225 | 当一个类从另一个类继承状态和行为时,滥用子类是如此重要,以至于我们在它自己的章节,主题 31,继承税 中讨论了它。 226 | 227 | ## 再说一次,一切都是为了改变 228 | 229 | 耦合代码很难更改:在一个地方的更改可能会对代码中的其他地方产生副作用,并且通常会在一个月后才在生产中曝光的难以找到的地方产生副作用。 230 | 231 | 保持代码害羞:让它只处理它直接知道的事情,将有助于保持应用程序的解耦,这将使它们更易于更改。 232 | 233 | ## 相关内容包括 234 | 235 | - 话题 32 [_配置_](./配置.md) 236 | - 话题 9 [_重复的恶魔_](../Chapter2/重复的恶魔.md) 237 | - 话题 8 [_好设计的本质_](../Chapter2/好设计的本质.md) 238 | - 话题 31 [_继承税_](./继承税.md) 239 | - 话题 10 [_正交性_](../Chapter/正交性.md) 240 | - 话题 11 [_可逆性_](../Chapter/可逆性.md) 241 | - 话题 33 [_断开时间耦合_](../Chapter6/断开时间耦合.md) 242 | - 话题 29 [_杂耍现实世界_](./杂耍现实世界.md) 243 | - 话题 34 [_共享状态不正确_](../Chapter6/共享状态不正确.md) 244 | - 话题 35 [_Actors和进程_](../Chapter6/actors和进程.md) 245 | - 话题 36 [_黑板_](../Chapter6/黑板.md) 246 | - 话题 30 [_转换编程_](./转换编程.md) 247 | - 我们在 2003 年的软件构建文章《嵌入的艺术》中讨论了讲述,不要问 248 | -------------------------------------------------------------------------------- /Chapter5/配置.md: -------------------------------------------------------------------------------- 1 | # 配置 2 | 3 | 4 | > _把你所有的事都摆在它该有的的位置,每一部分都各有各的时候。_ 5 | > 6 | > _-- 本杰明富兰克林,自传_ 7 | 8 | 当代码依赖于应用程序启动后可能更改的值时,请将这些值保留在应用程序外部。当您的应用程序将在不同的环境中运行,并可能为不同的客户运行时,请将环境和特定于客户的值保留在应用程序之外。这样,您就可以对应用程序进行参数化;代码是根据它运行的位置定制的。 9 | 10 | --- 11 | ## 提示 55 使用外部配置参数化应用程序 12 | --- 13 | 14 | 您可能希望放入配置数据中的常见内容包括: 15 | 16 | - 外部服务的凭据(数据库,第三方API等) 17 | - 日志记录级别和目的地 18 | - 应用程序使用的端口,IP地址,计算机和群集名称 19 | - 特定于环境的验证参数 20 | - 外部设置的参数,例如税率 21 | - 特定于站点的格式详细信息 22 | - 许可证密钥 23 | 24 | 基本上,寻找可以更改的内容,您可以在代码主体之外表达这些内容,然后将其放入一些配置存储区中。 25 | 26 | ## 静态配置 27 | 许多框架和许多自定义应用程序将配置保留在平面文件或数据库表中。如果信息是平面文件,则趋势是使用某些现成的纯文本格式。当前,YAML 和 JSON 很流行。有时,用脚本语言编写的应用程序使用专用的源代码文件,专用于仅包含配置。如果信息是结构化的,并且可能由客户更改(例如,销售税率),则最好将其存储在数据库表中。而且,当然,您可以同时使用这两种方法,并根据用途拆分配置信息。 28 | 29 | 无论使用哪种形式,通常都会在应用程序启动时将配置作为数据结构读入应用程序。 通常,此数据结构是全局的,认为这会使代码的任何部分更容易获得其所拥有的值。 30 | 31 | 我们希望您不要这样做。 而是将配置信息包装在一个(瘦)API后面。 这使您的代码与配置表示的细节分离。 32 | 33 | --- 34 | ### 别做过头了 35 | 在本书的第一版中,我们建议以类似的方式使用配置而不是代码,但是显然在我们的说明中应该更加具体。 任何建议都可以采取极端措施或不当使用,因此这里有一些注意事项: 36 | 37 | 别做过头了。 我们的一个早期客户决定应用程序中的每个字段都应可配置。 结果,即使是最小的更改也要花费数周的时间,因为您必须同时实现字段和所有管理代码才能保存和编辑它。 他们手上有_40,000_个配置变量和一个编码梦魇。 38 | 39 | 不要懒惰地推动配置决策。 如果对于某项功能是否应该以这种方式工作还是应该由用户选择进行真正的辩论,请尝试一种方式并获得有关决策是否正确的反馈。 40 | 41 | --- 42 | 43 | ## 配置即服务 44 | 虽然静态配置很常见,但我们目前倾向于其他方法。我们仍然希望将配置数据保留在应用程序外部,而不是将其保存在平面文件或数据库中,我们希望将其存储在服务API的后面。这有很多好处: 45 | 46 | - 多个应用程序可以共享配置信息,而身份验证和访问控制限制了每个应用程序可以看到的内容 47 | - 可以全局进行配置更改 48 | - 可以通过专门的UI维护配置数据 49 | - 配置数据变为动态 50 | 51 | 最后一点,即配置应该是动态的,对于我们向高可用性应用程序迈进至关重要。我们必须停止并重新启动应用程序才能更改单个参数的想法已过时。使用配置服务,应用程序的组件可以注册以通知其使用的参数的更新,并且如果更改了它们,则服务可以向它们 52 | 发送包含新值的消息。 53 | 54 | 无论采用哪种形式,配置数据都会驱动应用程序的运行时行为。当配置值更改时,无需重建代码。 55 | 56 | ## 不要写渡渡鸟代码 57 | 如果没有外部配置,您的代码将无法适应或灵活。 这是坏事吗? 好吧,在现实世界中,不适应的物种将会死亡。 58 | 59 | 渡渡鸟无法适应毛里求斯岛上人类和牲畜的存在,并迅速灭绝。这是第一个被人类灭绝的物种灭绝。 60 | 61 | 不要让您的项目(或您的职业生涯)像渡渡鸟一样前进。 62 | 63 | ## 相关内容包括 64 | 65 | - 话题 14 [_域语言_](../Chapter2/域语言.md) 66 | - 话题 28 [_解耦_](./解耦.md) 67 | - 话题 9 [_重复的恶魔_](../Chapter2/重复的恶魔.md) 68 | - 话题 16 [_纯文本的力量_](../Chapter3/纯文本的力量.md) 69 | -------------------------------------------------------------------------------- /Chapter6/actors和进程.md: -------------------------------------------------------------------------------- 1 | # Actors 和进程 2 | 3 | 4 | > _没有作家,就不会有故事。_ 5 | > _没有演员,故事就不可能活灵活现。_ 6 | > 7 | > _-- 安吉·玛丽·德尔桑特_ 8 | 9 | Actors 和进程提供了实现并发的有趣方法,而不需要同步访问共享内存的负担。 10 | 11 | 然而在讲解它们之前,我们需要先定义一下我们的概念。这听起来会很学术。但是别担心,我们将在不久之后进行研究。 12 | 13 | - 一个 actor 是一个独立的虚拟处理器,有自己的本地(私有)状态。每个 actor 都有一个邮箱。当一个消息出现在邮箱中,而执行器是空闲的时候,它就会启动并处理这个消息。当它完成处理后,它会处理邮箱中的另一条消息,或者,如果邮箱是空的,它会回到睡眠状态。 14 | 15 | 当处理一个消息时,一个 actor 可以创建其他 actor,向它所知道的其他 actor 发送消息,并创建一个新的状态,当下一个消息被处理时,该状态将成为当前状态。 16 | 17 | - 进程通常是一个比较通用的虚拟处理器,通常由操作系统实现,以方便并发。进程可以被约束(按照惯例)表现为 actor,这就是我们这里所说的进程的类型。 18 | 19 | ## Actors 只能是并发的 20 | 在 actors 的定义中,有一些东西是缺失的。 21 | 22 | - 没有任何一个东西是可以控制的。没有任何东西可以安排下一步发生的事情,或者协调信息从原始数据到最终输出的传递。 23 | 24 | - 系统中唯一的状态被保存在消息和每个 actor 的本地状态中。消息不能被检查,除非被接收者读取,否则无法检查,而本地状态在 actor 之外是无法访问的。 25 | 26 | - 所有的消息都是单向的,没有回复的概念。如果你想让 actor 返回一个响应,你在发送的消息中包含自己的邮箱地址,它就会(最终)将响应作为另一个消息发送至该邮箱。 27 | 28 | - 一个 actor 处理每个消息到完成,并且每次只处理一个消息。 29 | 30 | 因此,actors 是并发地、异步地执行,而且什么都不共享。如果你有足够的物理处理器,你可以在每个处理器上运行一个 actor。如果你只有一个处理器,那么一些运行时可以处理它们之间的上下文切换。无论哪种方式,在 actors 上运行的代码都是一样的。 31 | 32 | --- 33 | ## 提示 59 在没有共享状态的情况下使用 Actors 进行并发 34 | --- 35 | 36 | ## 一个单独的 Actor 37 | 让我们用 actors 来实现我们的晚餐。在这种情况下,我们有三个(顾客、服务员和馅饼盒)。 38 | 39 | 整个消息流如下所示: 40 | 41 | - 我们会告诉顾客他们饿了 42 | 43 | - 作为回应,他们会向服务员要馅饼 44 | 45 | - 服务员会让馅饼盒给顾客拿一些馅饼 46 | 47 | - 如果馅饼盒里有一块馅饼,它会把它寄给顾客,并通知服务员把它加到帐单上 48 | 49 | - 如果没有馅饼,馅饼盒会告诉服务员,服务员会向顾客道歉。 50 | 51 | 我们选择使用 Nact 库在 JavaScript 中实现代码。我们在此基础上添加了一个小包装器,允许我们将 actors 编写为简单对象,其中键是它接收的消息类型,值是在接收到特定消息时运行的函数。(大多数 actor 系统都有类似的结构,但细节取决于宿主语言。) 52 | 53 | 让我们从顾客开始。客户可以收到三条信息: 54 | 55 | - 你饿了(由外部上下文发送) 56 | 57 | - 桌子上有馅饼(馅饼盒发送) 58 | 59 | - 对不起,没有馅饼(服务员发送的) 60 | 61 | 这里是代码: 62 | 63 | ```js 64 | const customerActor = { 65 | 'hungry for pie': (msg, ctx, state) => { 66 | return dispatch(state.waiter, { type: "order", customer: ctx.self, wants: 'pie' }) 67 | }, 68 | 69 | 'put on table': (msg, ctx, state) => 70 | console.log(`${ctx.self.name} sees "${msg.food}" appear on the table`), 71 | 72 | 'no pie left': (_msg, ctx, _state) => 73 | console.log(`${ctx.self.name} sulks...`) 74 | } 75 | ``` 76 | 77 | 有趣的案例是,当我们收到一个 ''饿了想要一个馅饼'' 的消息,然后给服务员发了一条消息,在这个时候,我们就会把这个消息发给服务员。(我们很快就会看到顾客是如何知道服务员的行为者的)。 78 | 下面是服务员的代码。 79 | 80 | ```js 81 | const waiterActor = { 82 | "order": (msg, ctx, state) => { 83 | if(msg.wants == "pie") { 84 | dispatch(state.pieCase, { type: "get slice", customer: msg.customer, waiter: ctx.self }) 85 | } else { 86 | console.dir(`Don't know how to order ${msg.wants}`); 87 | } 88 | }, 89 | 90 | "add to order": (msg, ctx) => 91 | console.log(`Waiter adds ${msg.food} to ${msg.customer.name}'s order`), 92 | 93 | "error": (msg, ctx) => { 94 | dispatch(msg.customer, { type: 'no pie left', msg: msg.msg }); 95 | console.log(`\nThe waiter apologizes to ${msg.customer.name}: ${msg.msg}`) 96 | } 97 | } 98 | ``` 99 | 100 | 当它收到来自客户的 "想要" 消息时,它会检查该请求是否为馅饼的请求。如果是,它就向馅饼箱发送一个请求,同时传递对自己和客户的引用。 101 | 102 | 馅饼盒子有状态:它所持有的所有馅饼片的数组。当它收到服务员发来的 "get slice"消息时,它会查看它是否有剩余的馅饼。如果有,它就会把这片馅饼传给顾客,告诉服务员更新订单,最后返回一个更新的状态,里面少了一块馅饼。下面是代码。 103 | 104 | ```js 105 | const pieCaseActor = { 106 | 'get slice': (msg, ctx, state) => { 107 | if(state.slices.length == 0) { 108 | dispatch(msg.waiter, { type: 'error', msg: "no pie left", customer: msg.customer }) 109 | return false 110 | } else { 111 | var slice = state.slices.shift() + "pie slice" 112 | dispatch(msg.customer, { type: 'put on table', food: slice }) 113 | dispatch(msg.waiter, { type: 'add to order', food: slice, customer: msg.customer }) 114 | return state 115 | } 116 | } 117 | } 118 | ``` 119 | 120 | 虽然你经常会发现 actors 是由其他 actors 动态启动的,但在我们的例子中,我们将保持简单,手动启动我们的 actors 。我们还将给每个角色传递一些初始状态。 121 | 122 | - 我们将给馅饼箱子提供它所包含的馅饼片的初始列表 123 | - 我们会给服务员提供一个参考,就是那个馅饼的箱子 124 | - 我们会给客人提供服务员的参照物 125 | 126 | ```js 127 | const actorSystem = start() 128 | 129 | let pieCase = start_actor( 130 | actorSystem, 131 | 'pie-case', 132 | pieCaseActor, 133 | { slices: ["apple", "peach", "cherry"] } 134 | ) 135 | 136 | let waiter = start_actor( 137 | actorSyatem, 138 | 'waiter', 139 | waiterActor, 140 | { pieCase: pieCase } 141 | ) 142 | 143 | let c1 = start_actor( 144 | actorSyatem, 145 | 'customer1', 146 | customerActor, 147 | { waiter: waiter } 148 | ) 149 | 150 | let c2 = start_actor( 151 | actorSyatem, 152 | 'customer2', 153 | customerActor, 154 | { waiter: waiter } 155 | ) 156 | ``` 157 | 158 | 最后,我们终于把它分开了。我们的顾客很贪心。顾客一要三片馅饼,顾客二要两片。 159 | 160 | ```js 161 | dispatch(c1, { type: 'hungry for pie', waiter: waiter }); 162 | dispatch(c2, { type: 'hungry for pie', waiter: waiter }); 163 | dispatch(c1, { type: 'hungry for pie', waiter: waiter }); 164 | dispatch(c2, { type: 'hungry for pie', waiter: waiter }); 165 | dispatch(c1, { type: 'hungry for pie', waiter: waiter }); 166 | 167 | sleep(500) 168 | .then(() => { 169 | stop(actorSystem); 170 | }) 171 | ``` 172 | 173 | 当我们运行它的时候,我们可以看到 actors 在交流,你看到的顺序很可能不一样。 174 | 175 | ``` 176 | $ node index.js 177 | customer1 sees "apple pie slice" appear on the table 178 | customer2 sees "peach pie slice" appear on the table 179 | Waiter adds apple pie slice to customer1's order 180 | Waiter adds peach pie slice to customer2's order 181 | customer1 sees "cherry pie slice" appear on the table 182 | Waiter adds cherry pie slice to customer1's order 183 | 184 | The waiter apologizes to customer1: no pie left 185 | customer1 sulks… 186 | 187 | The waiter apologizes to customer2: no pie left 188 | customer2 sulks… 189 | ``` 190 | 191 | ## 没有显式并发 192 | 在 actor 模型中,不需要写任何代码来处理并发,因为没有共享状态。也不需要编写显式的端到端 "做这个,做那个 "的逻辑,因为 actors 根据接收到的消息自行解决。 193 | 194 | 也没有提到底层架构。这组组件在单处理器、多核或多网络机器上的工作效果都很好。 195 | 196 | ## Erlang 创造了舞台 197 | Erlang 语言和运行时是 actor 实现的很好的例子(尽管 Erlang 的发明者没有读过最初的 Actor 论文)。Erlang 把 actor 称为进程,但它们不是常规的操作系统进程。相反,就像我们一直在讨论的 actor 一样,Erlang 进程是轻量级的(你可以在一台机器上运行数百万个),它们通过发送消息来进行通信。每个进程与其他进程是隔离的,所以不存在状态共享。 198 | 199 | 此外,Erlang 运行时实现了一个监督系统,它可以管理进程的生命周期,在出现故障时可能会重启一个进程或一组进程。而且 Erlang 还提供了热加载功能:你可以在不停止该系统的情况下替换正在运行的系统中的代码。而且Erlang 系统运行着一些世界上最可靠的代码,经常引用九死一生的可用性。 200 | 201 | 但是,Erlang(和它的后代 Elixir )并不是唯一的,大多数语言都有 actor 实现。考虑在你的并发实现中使用它们。 202 | 203 | ## 相关内容包括 204 | 205 | - 话题 28 [_解耦_](../Chapter5/解耦.md) 206 | - 话题 36 [_黑板_](./黑板.md) 207 | - 话题 30 [_转换编程_](../Chapter5/转换编程.md) 208 | 209 | ## 挑战 210 | 211 | - 你目前是否有使用相互排斥来保护共享的数据。为什么不试试用 actors 编写相同代码的原型? 212 | 213 | ## 练习 214 | ### 练习 22 215 | 216 | 食客的 actor 代码只支持点馅饼。将其扩展到让顾客点馅饼的模式,由单独的代理管理馅饼片和冰激凌。安排好事情,这样它就可以处理一个或另一个用完的情况。 217 | -------------------------------------------------------------------------------- /Chapter6/共享状态不正确.md: -------------------------------------------------------------------------------- 1 | # 共享状态不正确 2 | 3 | 4 | 您正在享用自己最喜欢的晚餐。 您吃完了主菜,然后询问服务员是否还有苹果派。 他转过头去在看到展示柜中还有一块,然后说是。 您点了它并满意地舒了口气。 5 | 6 | 同时,在餐厅的另一侧,另一个顾客向他们的服务员问了相同的问题。 她也看了看,确认还有一块,然后客户下了订单。 7 | 8 | 你们中的一个人即将会失望的。 9 | 10 | 把陈列柜的例子换成联合银行帐户,然后将工作人员变成销售点设备。 您和您的伴侣都决定同时购买一部新手机,但是帐户里面只有够买一部的钱。 某人(银行,商店或您)将非常不高兴。 11 | 12 | 问题就在共享状态。 餐馆中的每个服务员都在看陈列柜,而没有考虑其他情况。 每个销售点设备都会查看帐户余额,而不会考虑其他帐户。 13 | 14 | --- 15 | ## 提示 57 共享状态不正确 16 | --- 17 | 18 | ## 非原子更新 19 | 让我们看看我们的晚餐示例,就好像它是代码一样: 20 | 21 | ![示例](../assets/topic34_1.png) 22 | 23 | 这两个服务员同时工作(在现实生活中,是并行的)。让我们看看他们的代码: 24 | 25 | ```ruby 26 | if display_case.pie_count > 0 27 | promise_pie_to_customer() 28 | display_case.take_pie() 29 | give_pie_to_customer() 30 | end 31 | ``` 32 | 33 | 服务员 1 得到当前派的数量,发现是 1 个。他答应把派给顾客。但就在这时,服务员 2 跑了过来。她也看到派的数量是 1 个,于是向顾客做出同样的承诺。这时,其中一个抓住了最后一块派,另一个服务员进入了某种错误状态。 34 | 35 | 这里的问题不在于两个进程可以写到相同的内存。问题是两个进程都不能保证其对该内存的视图是一致的。实际上,当服务员执行 display_case.pie_count() 时,他们会将显示案例中的值复制到自己的内存中。如果显示案例中的值发生了变化,那么他们的内存(他们用来做决策的内存)现在已经过期了。 36 | 37 | 这都是因为取值,然后更新派的计数不是一个原子操作:中间的底层值会发生变化。 38 | 39 | 那么,如何才能让它成为原子化呢? 40 | 41 | ## 信号量和其他互斥形式 42 | 一个信号量只是一个一次只能由一个人拥有的东西。你可以创建一个信号量,然后用它来控制对其他资源的访问。在我们的例子中,我们可以创建一个信号量来控制对派的访问,并采用一个惯例,即任何想要更新派内容的人只能在持有该信号量的情况下才能更新。 43 | 44 | 假设食客决定用物理信号器来解决派的问题。他们在派的盒子上放了一个塑料信号。在任何服务员卖掉派之前,他们必须在手里拿着那个信号量。一旦他们的订单完成后(也就是把派送到餐桌前),他们就可以把小信号放回看守派宝藏的地方,准备调解下一个 45 | 订单。 46 | 47 | 我们用代码来看一下。非常经典地,抓取派的操作叫 P,释放派的操作叫 V。今天我们用的是锁/解锁、索赔/释放等术语。 48 | 49 | ```ruby 50 | case_semaphore.lock() 51 | 52 | if display_case.pie_count > 0 53 | promise_pie_to_customer() 54 | display_case.take_pie() 55 | give_pie_to_customer() 56 | end 57 | 58 | case_semaphore.unlock() 59 | ``` 60 | 61 | 这段代码假设一个 semaphore 已经被创建并存储在变量 case_semapohore 中。 62 | 63 | 让我们假设两个服务员同时执行这段代码。他们都试图锁定 semaphore,但只有一个成功。得到 semaphore 的那一个继续正常运行。没有得到 semaphore 的那一个会被暂停,直到 semaphore 可用(服务员等待...)。当第一个服务员完成订单后,他们会解锁信号灯,第二个服务员继续运行。这时,他们看到箱子里没有派,就向顾客道歉。 64 | 65 | 这种做法有一些问题。可能最主要的问题是,它之所以有效,是因为每个人在使用派箱的时候,都会同意使用semaphore 的惯例。如果有人忘记了(也就是说,有些开发人员写的代码不遵循约定),那么我们又会陷入混乱。 66 | 67 | ### 让资源成为事务性的 68 | 目前的设计很差,因为它把保护派箱的访问权的责任交给了使用派箱的人。让我们改变一下,把这种控制权集中起来。要做到这一点,我们必须改变 API,让服务员在检查计数的同时,也可以在一次调用中取一块派的分量。 69 | 70 | ```ruby 71 | slice = display_case.get_pie_if_available() 72 | 73 | if slice 74 | give_pie_to_customer() 75 | end 76 | ``` 77 | 78 | 为了实现这个目标,我们需要写一个方法,作为显示案例本身的一部分来运行。 79 | 80 | ```ruby 81 | if get_pie_if_available() 82 | if @slices.size > 0 83 | update_sales_data(:pie) 84 | return @slices.shift 85 | else 86 | false 87 | end 88 | end 89 | ``` 90 | 91 | 这段代码说明了一个常见的误区。我们把资源访问移到了一个中心位置,但我们的方法仍然可以从多个并发线程中调用,所以我们仍然需要用 semaphore 来保护它。 92 | 93 | ```ruby 94 | if get_pie_if_available() 95 | @case_semaphore.lock() 96 | 97 | if @slices.size > 0 98 | update_sales_data(:pie) 99 | return @slices.shift 100 | else 101 | false 102 | end 103 | 104 | @case_semaphore.unlock() 105 | end 106 | ``` 107 | 108 | 即使是这样的代码也可能是不正确的。如果 update_sales_data 引发异常,那么 semaphore 将永远不会被解锁,未来所有对派箱的访问将无限期地挂起。我们需要处理这个问题: 109 | 110 | ```ruby 111 | if get_pie_if_available() 112 | @case_semaphore.lock() 113 | 114 | try { 115 | if @slices.size > 0 116 | update_sales_data(:pie) 117 | return @slices.shift 118 | else 119 | false 120 | end 121 | } 122 | ensure { 123 | @case_semaphore.unlock() 124 | } 125 | end 126 | ``` 127 | 128 | 因为这是个很常见的错误,所以很多语言都提供了库来帮你处理。 129 | 130 | ```ruby 131 | if get_pie_if_available() 132 | @case_semaphore.protect() { 133 | if @slices.size > 0 134 | update_sales_data(:pie) 135 | return @slices.shift 136 | else 137 | false 138 | end 139 | } 140 | end 141 | ``` 142 | 143 | ## 多种资源交易 144 | 我们的餐厅刚刚安装了一个冰激凌冰柜。如果顾客点了派,服务员需要检查派和冰淇淋是否都有。 145 | 146 | 我们可以把服务员的代码改成这样。 147 | 148 | ```ruby 149 | slice = display_case.get_pie_if_available() 150 | scoop = freezer.get_ice_cream_if_available() 151 | 152 | if slice && scoop 153 | give_order_to_customer() 154 | end 155 | ``` 156 | 157 | 但这是行不通的 如果我们认领了一片派,但当我们想拿一勺冰淇淋时,却发现没有了,怎么办?我们现在只能拿着一些派,而我们却什么也做不了(因为我们的顾客一定有冰淇淋)。而事实上,我们拿着派的事实意味着它不在箱子里,所以它不能给其他不想要冰淇淋的顾客提供。 158 | 159 | 我们可以通过在盒子中添加一个方法来解决这个问题,让我们返回一片派。我们需要添加异常处理,以确保如果失败了,我们不会保留资源。 160 | 161 | ```ruby 162 | slice = display_case.get_pie_if_available() 163 | 164 | if slice 165 | try { 166 | scoop = freezer.get_ice_cream_if_available() 167 | if scoop 168 | try { 169 | give_order_to_customer() 170 | } 171 | rescue { 172 | freezer.give_back(scoop) 173 | } 174 | end 175 | } 176 | rescue { 177 | display_case.give_back(slice) 178 | } 179 | end 180 | ``` 181 | 182 | 同样,这也不太理想。现在的代码真的很难看:弄清楚它实际做什么是很难的:业务逻辑被埋没在所有的家务中。 183 | 184 | 之前我们通过将资源处理代码移到资源本身来解决这个问题。但在这里,我们有两个资源。我们应该把代码放在展示箱里还是放在冷冻箱里? 185 | 186 | 我们认为这两个选项的答案都是 "不"。实事求是的做法是说 "苹果派的模式 "是它自己的资源。我们会把这个代码移到一个新的模块中,然后客户端只需说 "给我拿个苹果派加冰激凌 "就可以了,不是成功就是失败。 187 | 188 | 当然,在现实世界中可能会有很多这样的复合菜,你不会想为每个菜都写新的模块。 反之,你可能会想要某种菜单项,其中包含对其组件的引用,然后有一个通用的 get_menu_item 方法,它可以与每个组件一起做资源的舞蹈。 189 | 190 | ## 非交易性更新 191 | 很多人都把共享内存作为并发问题的源头,但事实上,在你的应用程序代码共享可变资源的任何地方都可能出现问题:文件、数据库、外部服务等。每当你的代码的两个或多个实例可以同时访问某些资源时,你就会发现一个潜在的问题。 192 | 193 | 有时候,这个资源并不是那么明显。在写这本书的时候,我们更新了工具链,使用线程做更多的并行工作。这导致了构建失败,但以奇怪的方式和随机的地方导致了失败。所有错误中的一个共同点就是找不到文件或目录,尽管它们确实在正确的位置。 194 | 195 | 我们将其归结为代码中的几个地方临时改变了当前目录。在非并行版本中,这段代码将目录恢复回来已经足够好了。但在并行版本中,一个线程会改变目录,然后,当在这个目录中,另一个线程会运行。这个线程会期望在原来的目录中,但由于当前目录是线程之间共享的,所以情况并非如此。 196 | 197 | 这个问题的本质提示了另一个提示。 198 | 199 | --- 200 | ## 技巧 58 随机故障往往是并发问题 201 | --- 202 | 203 | ## 其他类型的独家访问 204 | 大多数语言都有库支持对共享资源的某种排他性访问。它们可以称之为mutexs(用于相互排斥)、监控器或semaphores。这些都是作为库来实现的。 205 | 206 | 然而,有些语言本身就内置了并发支持。例如,Rust 就强制执行了数据所有权的概念;一次只能有一个变量或参数持有对任何特定的可突变数据的引用。 207 | 208 | 你也可以说,功能语言由于其使所有数据不可变的倾向,使并发变得更简单。然而,它们仍然面临着同样的挑战,因为在某些时候,它们被迫进入到真实的、可变的世界。 209 | 210 | ## 医生,很痛。。。 211 | 如果你从这一节中没有其他的东西,那么请记住这一点:共享资源环境中的并发是很困难的,而自己管理它是充满挑战的。 212 | 213 | 这就是为什么我们推荐这个笑话的关键: 214 | 215 | 医生,我这样做的时候会很痛。 216 | 217 | 那就不要这样做。 218 | 219 | 接下来的几节建议大家用其他方法来获得并发的好处,而不是痛苦。 220 | 221 | ## 相关内容推荐 222 | - 话题 38 [_巧合的编程_](../Chapter7/巧合的编程.md) 223 | - 话题 28 [_解耦_](../Chapter5/解耦.md) 224 | - 话题 10 [_正交性_](../Chapter2/正交性.md) 225 | -------------------------------------------------------------------------------- /Chapter6/并发.md: -------------------------------------------------------------------------------- 1 | # 并发 2 | 3 | 4 | 就像我们都在同一页上一样,让我们从一些定义开始: 5 | 6 | _并发性_ 是指两段或多段代码的执行好像它们同时运行一样。_并行_ 是指它们确实同时运行。 7 | 8 | 要获得并发性,需要在运行代码时可以在代码的不同部分之间切换执行的环境中运行代码。这通常使用诸如光纤、线程和进程之类的东西来实现。 9 | 10 | 要实现并行,您需要能够同时执行两件事情的硬件。这可能是一个 CPU 中的多个核心,一台计算机中的多个 CPU,或者连接在一起的多台计算机。 11 | 12 | ## 一切都是并发 13 | 几乎不可能在一个大小合适、没有并发方面的系统中编写代码。它们可能是明确的,也可能埋在图书馆里。如果你想让你的应用程序能够处理现实世界中的异步事务,并发是一个要求:用户交互,数据获取,调用外部服务,所有这些都是同时进行的。如果你强迫这个进程是串行的,一件事发生,然后下一件事发生,依此类推,你的系统会感觉很迟钝,你可能没有充分利用它运行的硬件的能力。 14 | 在本章中,我们将讨论并发和并行: 15 | 16 | 开发人员经常谈论代码块之间的耦合。他们指的是依赖关系,以及这些依赖关系如何使事情难以改变。但还有另一种形式的耦合。当您的代码将一个序列强加给不需要解决手头问题的东西时,就会发生时间耦合。你相信“滴答”在“滴答”之前吗?如果你想保持灵活就不要了。您的代码是否按顺序访问三个后端服务?如果你想留住你的用户就不要了。在话题 33,[_断开时间耦合_](./断开时间耦合.md) 中,我们将研究识别这种时间耦合的方法。 17 | 18 | 为什么编写并发和并行代码这么困难?一个原因是我们学会了使用顺序系统编程,我们的语言具有顺序使用时相对安全的特性,但一旦两件事同时发生,就成为一种负担。这里最大的罪魁祸首之一是共享状态。这不仅仅意味着全局变量:只要两个或多个代码块包含对同一可变数据块的引用,就可以共享状态。话题 34,[_共享状态不正确_](./共享状态不正确.md) 这一节描述了许多解决方法,但最终它们都容易出错。 19 | 20 | 如果这让你感到悲伤,绝望!有更好的方法来构造并发应用程序。其中之一是使用 actor 模型 ,其中独立的进程(不共享数据)使用定义的简单语义通过 channel 进行通信。我们在话题 35 [_Actors和进程_](./actors和进程.md) 中讨论了这种方法的理论和实践。 21 | 22 | 最后,我们来看看话题36,[_黑板_](./黑板.md)。这些系统就像是对象存储和智能发布/订阅代理的组合。在他们最初的状态下,他们从未真正起飞。但今天,我们看到越来越多的中间件层实现了黑板式的语义。正确地使用,这些类型的系统提供了大量的解耦。 23 | 24 | 并发和并行代码过去是很奇特的。现在它是必需的。 25 | -------------------------------------------------------------------------------- /Chapter6/断开时间耦合.md: -------------------------------------------------------------------------------- 1 | # 断开时间耦合 2 | 3 | 4 | 您可能会问什么是 _时间耦合_。其实就是关于时间。 5 | 6 | 时间是软件体系结构中经常被忽略的方面。唯一困扰我们的是日程表上的时间,即直到发布之前剩下的时间,但这不是我们在这里谈论的。相反,我们正在谈论时间作为软件本身的设计元素的作用。时间对我们很重要,有两个方面:并发(事物同时发生)和排序(事物在时间上的相对位置)。 7 | 8 | 我们通常不会在考虑这两个方面的情况下进行编程。当人们第一次坐下来设计架构或编写程序时,事情往往是线性的。大多数人就是这样想的,先这样做,然后再这样做。但是以这种方式思考会导致时间耦合:时间耦合。必须始终在方法 B 之前调用方法 A;一次只能运行一份报告;您必须等待屏幕重新绘制,然后才能收到按钮点击的事件。滴答声必须先于滴答声发生。这种方法不是很灵活,也不是很现实。 9 | 10 | 我们需要考虑并发,并考虑将任何时间或订单依赖项解耦。这样,我们可以在许多开发领域(工作流程分析,体系结构,设计和部署)中获得灵活性并减少任何基于时间的依赖关系。结果将是系统更易于推理,可能会更快,更可靠地做出响应。 11 | 12 | ## 寻找并发 13 | 在许多项目中,作为设计的一部分,我们需要对应用程序工作流进行建模和分析。我们想找出可以同时发生的事情,以及必须严格执行的事情。一种做到这一点的方法是使用诸如活动图之类的符号来捕获工作流。 14 | 15 | 活动图由一组绘制为圆形框的动作组成。离开动作的箭头会导致另一个动作(可以在第一个动作完成后开始)或一条称为同步条的粗线。一旦完成进入同步栏的所有操作,您就可以沿着离开该栏的所有箭头进行操作。可以随时开始没有箭头的动作。 16 | 17 | 通过标识可以并行执行的活动,你可以使用活动图最大限度地提高并行。 18 | 19 | --- 20 | ## 提示 56 分析工作流程以提高并发 21 | --- 22 | 23 | 例如,我们可能正在为PiñaColada机器人制造商编写软件。 有人告诉我们这些步骤是: 24 | 25 | |步骤|步骤| 26 | |:--|--| 27 | |1 打开搅拌机 |1 关上搅拌机| 28 | |2 打开 PiñaColada 混合 |2 液化 1 分钟| 29 | |3 将混合物放入搅拌机 |3 打开搅拌机| 30 | |4 测量 1/2 杯白朗姆酒 |4 拿玻璃杯| 31 | |5 倒入朗姆酒 |5 获取粉红色的雨伞| 32 | |6 加 2 杯冰 |6 服务| 33 | 34 | 但是,如果调酒师按顺序一步步执行这些步骤,他们将失去工作。 即使它们顺序地描述了这些动作,它们中的许多动作也可以并行执行。 我们将使用 活动图 来捕获并推断潜在的并发性。 35 | 36 | ![活动图](../assets/topic33_1.png) 37 | 38 | 睁大眼睛可以看到依赖项真正存在的地方。在这种情况下,顶级任务(1、2、4、10 和 11)可以同时并发发生。任务 3、5 和 6 稍后可以并行发生。如果您参加的是 piñacolada 制作大赛,这些优化措施可能会有所不同。 39 | 40 | ## 并发的机会 41 | 活动图显示了潜在的并发领域,但是对于这些领域是否值得利用没有什么可说的。例如,在 piñacolada 的示例中,调酒师需要五只手才能一次执行所有可能的初始任务。 42 | 43 | 这就是设计部分的内容。当我们查看活动时,我们意识到数字 8 液化将需要一分钟。在此期间,我们的酒保可以拿起玻璃杯和雨伞(活动 10 和 11),并且可能还有时间服务其他客户。 44 | 45 | 这就是我们在设计并发时要寻找的东西。我们希望找到需要时间的活动,但是在我们的代码中却没有时间。查询数据库,访问外部服务,等待用户输入:所有这些操作通常会使我们的程序停止运行,直到完成。这些都是做比 CPU 挥霍拇指更有效的工作的机会。 46 | 47 | ## 并行的机会 48 | 记住区别:并发是一种软件机制,而并行是一种硬件问题。如果我们在本地或远程有多个处理器,那么如果我们可以在其中分配工作,则可以减少总的处理时间。 49 | 50 | 以这种方式拆分的理想方法是相对独立的工作-每个工作都可以继续进行而无需等待其他任何事情。一种常见的模式是进行大量工作,将其分成独立的块,并行处理每个块,然后合并结果。 51 | 52 | 实际上,一个有趣的例子是 Elixir 语言的编译器的工作方式。启动时,它将正在构建的项目拆分为模块,并并行编译每个模块。有时一个模块依赖于另一个模块,在这种情况下,其编译会暂停,直到另一个模块的构建结果可用为止。顶级模块完成时,意味着所有依赖项都已编译。结果是可以利用所有可用内核的快速编译。 53 | 54 | --- 55 | ### 更快的格式化 56 | 这本书是用纯文本写的。要生成打印的版本,电子书或其他任何内容,请通过处理器管道来输入该文本。有些人寻找特殊的结构(参考书目引文,索引条目,技巧的特殊标记等)。其他处理器对整个文档进行操作。 57 | 58 | 流水线中的许多处理器必须访问外部信息(读取文件,写入文件,通过外部程序进行管道传输)。所有这些相对较慢的工作为我们提供了利用并发的机会:实际上,管道中的每个步骤都是并发执行的,从上一步读取并写入下一个步骤。 59 | 60 | 另外,进程的某些部分是处理器密集型的。其中之一是数学公式的转换。由于各种历史原因,每个方程式最多可能需要 500 毫秒才能转换。为了加快速度,我们利用了并行性。由于每个公式彼此独立,因此我们以各自并行的方式进行转换,并在结果可用时将其收集回书中。 61 | 62 | 结果,这本书在多核机器上的构建速度要快得多。 63 | 64 | (而且,是的,我们在此过程中发现了许多并发错误……) 65 | 66 | --- 67 | 68 | ## 识别机会是容易的一部分 69 | 返回您的应用程序。 我们已经确定了可以从并发和并行中受益的地方。 现在到了棘手的部分:我们如何安全地实现它。 这是本章其余部分的主题。 70 | 71 | ## 相关内容包括 72 | - 话题 26 [_如何平衡资源_](../Chapter4/如何平衡资源.md) 73 | - 话题 28 [_解耦_](../Chapter5/解耦.md) 74 | - 话题 10 [_正交性_](../Chapter2/正交性.md) 75 | - 话题 36 [_黑板_](./黑板.md) 76 | 77 | ## 挑战 78 | - 早上准备工作时,您并行执行多少个任务? 您能在 UML 活动图中表达这一点吗? 您能找到增加并发性的更快准备的方法吗? 79 | -------------------------------------------------------------------------------- /Chapter6/黑板.md: -------------------------------------------------------------------------------- 1 | # 黑板 2 | 3 | > _写在墙上…_ 4 | > 5 | > _-- 丹尼尔5(参考)_ 6 | 7 | 你可能不会把 优雅 与警探联系在一起,而是想象出某种甜甜圈和咖啡的陈词滥调。但是,考虑一下警探们如何利用黑板来协调和解决一起谋杀案的调查。 8 | 9 | 假设总督察一开始在会议室里设置了一个大黑板。在上面,她写下了一个问题。 10 | 11 | H. Dumpty(男,鸡蛋)。意外?谋杀? 12 | 13 | Humpty 真的是掉下去的,还是被人推倒的?每个警探都可以通过添加事实、目击者的证词、可能出现的任何法医证据等,为这个潜在的谋杀之谜做出贡献。随着数据的积累,警探可能会注意到其中的联系,并将观察到的情况或推测也发布出来。这个过程在所有的班次中,由许多不同的人和特工继续进行,直到结案。图为黑板样本。 14 | 15 | ![黑板](../assets/topic36_1.png) 16 | 17 | 有人发现了Humpty的赌债和电话记录之间的联系。也许是他接到了恐吓电话。 18 | 19 | 黑板法的一些主要特点是: 20 | 21 | - 侦探们都不需要知道任何其他侦探的存在 ---- 他们会观察黑板上的新信息,并补充他们的发现。 22 | 23 | - 侦探们可能受训于不同的学科,可能有不同的教育水平和专业知识,甚至可能不在同一个分局工作。他们有共同的破案愿望,但仅此而已。 24 | 25 | - 不同的警探在破案过程中可能会来去匆匆,可能会有不同的轮班。 26 | 27 | - 黑板上可以放的东西没有限制,可以是图片,可以是句子,也可以是物证等。 28 | 29 | 这是一种自由放任的并发。侦探是独立的进程、代理、actors 等。有些人把事实储存在黑板上。其他人会把事实从黑板上去掉,也许会合并或处理它们,然后在黑板上添加更多的信息。黑板逐渐帮助他们得出结论。 30 | 31 | 基于计算机的黑板系统最初是为人工智能应用而发明的,在人工智能应用中,需要解决的问题有大型复杂的语音识别、基于知识的推理系统等。 32 | 33 | Gelertner 里的 Linda 是最早的黑板系统之一。它以键入的图元组的形式存储事实。应用程序可以将新的图元组写入 Linda 中,并使用一种模式匹配的形式查询现有的图元组。 34 | 35 | 后来出现了类似黑板的分布式系统,如 JavaSpaces 和 T Spaces。使用这些系统,你可以在黑板上存储活动的 Java 对象(而不仅仅是数据),并通过字段的部分匹配(通过模板和通配符)或子类型来检索它们。例如,假设你有一个类型 Author,它是 Person 的一个子类型。你可以通过使用一个 Author 模板来搜索包含 Person 对象的黑板,该模板的 lastName 值为 "Shakespeare"。你会得到比尔-莎士比亚这个作者,但不是弗雷德-莎士比亚这个园丁。 36 | 37 | 我们认为,这些系统从来没有真正起飞,部分原因是由于当时还没有发展出对并发合作处理的需求。 38 | 39 | ## 行动中的黑板 40 | 假设我们正在编写一个程序来接受和处理按揭或贷款申请。管理这个领域的法律非常复杂,联邦、州和地方政府都有自己的发言权。贷款人必须证明他们已经披露了某些事情,并且必须要求提供某些信息,但不能问某些其他问题,等等。 41 | 42 | 除了适用法律的混乱,我们还有以下问题需要解决。 43 | 44 | - 数据到达的顺序无法保证。例如,查询信用检查或产权查询可能需要大量的时间,而姓名和地址等项目可能马上就可以得到。 45 | 46 | - 数据收集工作可能由不同的人完成,分布在不同的办公室,在不同的时区。 47 | 48 | - 有些数据收集可能是由其他系统自动完成的。这些数据也可能是异步到达的。 49 | 50 | - 尽管如此,某些数据仍然可能依赖于其他数据。例如,你可能要等到拿到所有权或保险证明后才能开始查询汽车的所有权。 51 | 52 | - 新数据的到来可能会带来新的问题和政策。假设信用检查回来的报告不是那么光彩夺目;现在你需要这五张额外的表格,也许还需要血样。 53 | 54 | 你可以尝试使用工作流程系统来处理每一种可能的组合和情况。许多这样的系统都存在,但它们可能很复杂,而且程序密集。随着法规的变化,工作流程必须重新组织:人们可能不得不改变他们的程序,硬接线的代码可能需要重新编写。 55 | 56 | 黑板与封装了法律要求的规则引擎相结合,是解决这里所发现的困难的优雅解决方案。数据到达的顺序无关紧要:当一个事实被发布时,它可以触发相应的规则。反馈也很容易处理:任何一组规则的输出都可以发布到黑板上,并触发更多适用的规则。 57 | 58 | --- 59 | ## 提示 60 使用黑板来协调工作流 60 | --- 61 | 62 | ## 信息系统可以像黑板一样 63 | 在我们写这本书第二版的时候,许多应用都是使用小型的、解耦的服务来构建的,它们都通过某种形式的消息传递系统进行通信。这些消息传递系统(如 Kafka 和 NATS)的作用远不止于简单地将数据从 A 发送到 B,特别是,它们提供了持久性(以事件日志的形式)和通过模式匹配的形式检索消息的能力。这意味着你可以把它们作为一个黑板系统和/或作为一个平台,在这个平台上运行一堆 actors。 64 | 65 | ## 但是,这并不是那么简单...... 66 | actor 和/或 黑板 和/或微服务的架构方法可以从你的应用程序中移除一整类潜在的并发问题。但这种好处是有代价的。这些方法比较难推理,因为很多动作是间接的。你会发现保持一个消息格式和/或 API 的中央存储库是有帮助的,特别是如果这个存储库可以为你生成代码和文档。你还需要有好的工具来追踪消息和它们在系统中的处理结果。一个有用的技术是在启动一个特定的业务函数时添加一个唯一的跟踪 ID,然后将其传播给所有参与的 actors)。然后你就能够从日志文件中重构发生的事情。 67 | 68 | 最后,这类系统的部署和管理可能会比较麻烦,因为有更多的活动部分。在某种程度上,这一点被系统更细化的事实所抵消,可以通过更换单个 actors 而不是整个系统来更新。 69 | 70 | ## 相关内容包括 71 | 72 | - 话题 28 [_解耦_](../Chapter5/解耦.md) 73 | - 话题 10 [_正交性_](../Chapter2/正交性.md) 74 | - 话题 33 [_断开时间耦合_](./断开时间耦合.md) 75 | - 话题 29 [_杂耍现实世界_](../Chapter5/杂耍现实世界.md) 76 | - 话题 35 [_Actors和进程_](../Chapter/actors和进程.md) 77 | 78 | ## 练习 79 | 80 | ### 练习 23 (可能的答案) 81 | 对于以下每一种应用,黑板式系统是否合适?为什么? 82 | 83 | - _图像处理_ 84 | 85 | 你想让多个并行进程抓取图像的大块,处理它们,然后将完成的大块放回去。 86 | 87 | - _群组日历_ 88 | 89 | 你有很多人分散在全球各地,在不同的时区,说着不同的语言,试图安排一个会议。 90 | 91 | - _网络监控工具_ 92 | 93 | 系统收集性能统计,收集故障报告。你要实现一些代理,利用这些信息来寻找系统中的故障。 94 | 95 | ## 挑战 96 | - 在现实世界中,你是否使用黑板系统 -- 冰箱上的留言板,或者工作中的大白板?是什么让它们有效?信息发布的格式是否一致?这有什么关系吗? 97 | 98 | -------------------------------------------------------------------------------- /Chapter7/代码测试.md: -------------------------------------------------------------------------------- 1 | # 代码测试 2 | 3 | 这本书的第一版是在更原始的时代写的,当时大多数开发者都不写测试----他们认为,反正2000年的时候,世界就要结束了,何必费劲呢。 4 | 5 | 在那本书中,我们有一节是关于如何构建易于测试的代码。这是一种偷偷摸摸地说服开发者真正写测试的方法。 6 | 7 | 这是个比较开明的时代。如果还有开发人员不写测试,至少他们知道自己应该写测试。 8 | 9 | 但还是有一个问题。当我们问开发者为什么要写测试时,他们看我们的眼神,就好像我们只是问他们是否还在用打卡的方式进行编码,他们会说 "确保代码能正常工作",并在结尾处加上一个不为人知的 "假人"。而我们认为这是不对的。 10 | 11 | 那么,我们认为测试的重要性是什么?以及我们认为你应该如何去做? 12 | 13 | 让我们先从这句话开始说起。 14 | 15 | --- 16 | ## 提示 66 测试不是为了找 BUG 17 | --- 18 | 19 | 我们相信,测试的主要好处是在你思考和编写测试的时候,而不是在你运行测试的时候。 20 | 21 | ## 关于测试的思考 22 | 这是一个星期一的早晨,你坐下来开始写一些新的代码。你必须写一些东西来查询数据库,返回一个每周在你的 "世界上最有趣的洗碗视频 "网站上观看 10 个以上视频的人的列表。 23 | 24 | 你启动你的编辑器,从编写执行查询的函数开始。 25 | 26 | ```elixir 27 | def return_avid_viewers do 28 | # ... hmmm ... 29 | end 30 | ``` 31 | 32 | 停!你怎么知道你要做的事是好事? 33 | 34 | 答案是,你无法知道。谁都不可能知道。但思考测试可以让它更有可能。下面是这样做的方法。 35 | 36 | 首先想象一下,你已经写完了函数,现在要对它进行测试。你会怎么做呢?嗯,你会想使用一些测试数据,这可能意味着你想在你控制的数据库中工作。现在,有些框架可以为你处理这个问题,在测试数据库中运行测试,但在我们的例子中,这意味着我们应该将数据库实例传递到我们的函数中,而不是使用一些全局的数据库实例,因为这样我们可以在测试时改变它。 37 | 38 | ```elixir 39 | def return_avid_users(db) do 40 | ``` 41 | 42 | 然后我们要考虑如何填充这些测试数据。这个需求想要 "每周观看10 个以上视频的人的列表" 所以我们在数据库模式中寻找可能有帮助的字段。我们在 who-watched-what 表中找到了两个可能的字段:open_video 和 completed_video。为了写出我们的测试数据,我们需要知道该用哪个字段。但是我们不知道这个需求是什么意思,我们的业务联系也在外部。那我们就在字段的名称中传递一个假的。这意味着我们可以测试一下我们所拥有的东西,以后有可能再去更改它。 43 | 44 | ```elixir 45 | def return_avid_users(db, qualifying_field_name) do 46 | ``` 47 | 48 | 我们开始思考我们的测试,不用写一行代码,就已经有了两个发现,并利用它们来改变我们的 API 方法。 49 | 50 | ## 测试驱动编码 51 | 在前面的例子中,对测试的思考使我们减少了代码中的耦合(通过传递数据库连接,而不是使用全局连接),并增加了灵活性(将我们测试的字段名称作为参数传递)。为我们的方法编写测试的思考使我们从外部看问题,就好像我们是代码的客户,而不是代码的作者。 52 | 53 | --- 54 | ## 提示 67 测试是你代码的第一个用户 55 | --- 56 | 57 | 我们认为这可能是测试提供的最大好处:测试是指导你编码的重要反馈。一个与其他代码紧密耦合的函数或方法是很难测试的,因为你必须在运行你方法之前设置好所有的环境。所以让你的东西可测试,也会降低它的耦合性。 58 | 59 | 而在测试东西之前,你必须先了解它。这听起来很傻,但在现实中,我们都是基于对自己所要做的事情的模糊理解而发起了一段代码。我们向自己保证,我们会边走边想办法。哦,以后我们也会添加所有支持边界条件的代码。哦,还有错误处理。而代码最终要比它应该的长度长五倍,因为它充满了条件逻辑和特殊情况。但把测试的光照在那段代码上,事情就会变得更清晰了。如果你在开始编写代码之前就考虑测试边界条件,以及这将如何工作,你很可能会在逻辑中找到简化函数的规律。如果你想好了你需要测试的错误条件,你就会相应地构造你的函数。 60 | 61 | ## 测试驱动的开发 62 | 有一个学派说,既然在前面想好了测试的种种好处,为什么不在前面也去写测试呢?他们实践的东西叫做测试驱动开发或 TDD。你也会看到这种叫做 "测试优先开发" [62] 。 63 | 64 | TDD的基本周期是。 65 | 66 | 1. 决定你要添加的一小块功能。 67 | 68 | 2. 编写一个测试,一旦实现了这个功能就会通过。 69 | 70 | 3. 运行你所有的测试,并验证唯一失败的就是你刚才写的那个测试。 71 | 72 | 4. 写出最少的代码来让测试通过,并验证测试现在运行得很干净。 73 | 74 | 5. 重构你的代码:看看是否有办法改进你刚才写的东西(测试或函数)。确保测试完成后仍然能通过。 75 | 76 | 我们的想法是,这个周期应该很短:几分钟的时间,这样你就可以不断地写测试,然后让它们发挥作用。 77 | 我们看到 TDD 对于刚开始做测试的人来说有很大的好处。如果你遵循 TDD 工作流,你将保证你的代码总是有测试。而这意味着你会一直在思考你的测试。 78 | 79 | 然而,我们也看到人们成为 TDD 的奴隶。这表现在很多方面。 80 | 81 | - 他们花了大量的时间来确保自己的测试覆盖率始终是100%。 82 | 83 | - 他们有大量的冗余测试。例如,在第一次写一个类之前,很多 TDD 的坚持者会先写一个失败的测试,简单地引用类的名称,然后写一个失败的测试。它失败了,然后他们写一个空的类定义,它就通过了。但是现在你的测试完全没有任何作用;下一次写的测试也会引用这个类,所以第一次写的测试就没有必要了。如果以后类的名字变了,需要修改的东西就更多了。而这只是一个琐碎的例子。 84 | 85 | - 他们的设计倾向于从底层开始,然后往上走。(见自下而上 VS 自上而下 VS 你应该做的事情)。 86 | 87 | --- 88 | ### 自下而上 VS 自上而下 VS 你应该做的事情 89 | 90 | 早在计算机还很年轻的时候,有两种设计流派:自上而下和自下而上。自上而下的人说,你应该从你要解决的整体问题开始,把它分解成一小部分。然后再把每个问题分解成更小的部分,以此类推,直到最后你有了足够小的部分,可以用代码来表达。 91 | 92 | 自下而上的人在构建代码的时候,就像建造房子一样。他们从最底层开始,产生一层代码,给他们提供一些抽象的、更接近他们要解决的问题。然后,他们又增加了另一层,再加上更高层次的抽象。他们一直坚持下去,直到最后一层是解决了问题的抽象。"把它变成这样..........." 93 | 94 | 这两派实际上都不可行,因为这两派都忽略了软件开发中最重要的一个方面:我们在开始时并不知道自己在做什么。自上而下的人假设他们可以在前面表达出整个需求:他们不能。自下而上的人假设他们可以建立一个抽象的列表,最终会把他们带到一个单一的顶层解决方案,但是当他们不知道自己的方向在哪里时,如何决定层的功能呢? 95 | 96 | ## 提示 68 构建端到端,而不是自上而下或自下而上 97 | 98 | 我们坚信,构建软件的唯一方法就是循序渐进。构建端到端功能的小块,边做边学。在你继续完善代码的过程中应用这些学习,让客户参与到每一步,并让他们指导整个过程。 99 | 100 | --- 101 | 102 | 通过各种手段练习 TDD。但是,如果你这样做了,不要忘记每隔一段时间就停下来看看大局。很容易被绿色的 "测试通过 "的消息所诱惑,写了很多代码,但实际上并不能让你更接近于解决方案。 103 | 104 | ## 回到代码 105 | 基于组件的开发一直以来都是软件开发的一个崇高目标。其想法是,通用的软件组件应该像普通集成电路(IC)一样容易获得和组合。但只有当你所使用的元件是已知的可靠的,并且有共同的电压、互连标准、时序等等,这才行得通。 106 | 107 | 芯片的设计是要经过测试的--不只是在工厂里,不只是在安装的时候,在现场部署的时候也要经过测试。更复杂的芯片和系统可能有一个完整的内置自测试(BIST)功能,可以在内部运行一些基础级的诊断,或者是测试访问机制(TAM),提供一个测试线束,允许外部环境提供刺激并收集芯片的响应。 108 | 109 | 我们在软件上也可以做同样的事情。就像我们的硬件同事一样,我们需要从一开始就把可测试性建立在软件中,并在试图将每一个部件连接在一起之前,对其进行彻底的测试。 110 | 111 | ## 单元测试 112 | 硬件的芯片级测试大致相当于软件中的单元测试--对每个模块单独进行测试,以验证其行为。一旦我们在可控的(甚至是伪造的)条件下进行了测试,我们就能更好地感受到一个模块在大的世界中的反应。 113 | 114 | 软件的单元测试是锻炼模块的代码。通常情况下,单元测试会建立某种人工环境,然后在被测模块中调用例程。然后,它对返回的结果进行检查,可以是对照已知值,也可以是对照以前运行相同测试的结果(回归测试)。 115 | 116 | 稍后,当我们将 "软件IC"组装成一个完整的系统时,我们就可以确信各个部分都能按预期工作,然后我们就可以使用同样的单元测试设施来测试整个系统。我们在 [_无情测试和持续测试_](../Chapter9/实用入门套件.md) 中谈到了这种大规模的系统检查。 117 | 118 | 然而,在走到这一步之前,我们需要决定在单元级测试什么。从历史上看,程序员会把一些随机的数据扔到代码中,看一下打印语句,然后称其为测试。我们可以做得更好。 119 | 120 | ## 针对契约的测试 121 | 我们喜欢把单元测试看成是针对契约测试(见 话题 23, [_契约设计_](../Chapter4/契约设计.md) )。我们要编写测试用例,确保给定的单元遵守合同。这将告诉我们两件事:一是代码是否符合合同,二是合同是否意味着我们认为的意思。我们希望在广泛的测试用例和边界条件下,测试模块是否提供了它所承诺的功能。 122 | 123 | 这在实践中意味着什么?让我们从一个简单的数字例子开始:一个平方根例程。它的文档化的契约很简单。 124 | 125 | ```js 126 | pre-conditions: 127 | argument >= 0; 128 | post-conditions: 129 | ((result * result) - argument).abs <= epsilon * argument; 130 | ``` 131 | 132 | 这告诉我们要测试什么。 133 | 134 | - 传入一个负参数,确保它被拒绝。 135 | 136 | - 传入一个 0 的参数,确保它被接受(这是边界值)。 137 | 138 | - 传入零与最大可表达参数之间的值,并验证结果的平方与原始参数的差值小于参数的某一小部分(epsilon)。 139 | 140 | 有了这个契约,假设我们的例程自己做了前后条件检查,我们就可以写一个基本的测试脚本来测试平方根函数。 141 | 142 | 然后我们可以调用这个例程来测试我们的平方根函数: 143 | ```python 144 | assertWithinEpsilon(my_sqrt(0), 0) 145 | assertWithinEpsilon(my_sqrt(2.0), 1.4142135614) 146 | assertWithinEpsilon(my_sqrt(64.0), 8.0) 147 | assertWithinEpsilon(my_sqrt(1.0e7), 3162.3776602) 148 | assertWithinEpsilon fn => my_sqrt(-4.0) end 149 | ``` 150 | 151 | 这是一个相当简单的测试;在现实世界中,任何一个不重要的模块都有可能依赖于其他一些模块,那么我们该如何去测试这个组合呢? 152 | 153 | 假设我们有一个使用 DataFeed 和 LinearRegression 的模块 A。按照顺序,我们将测试。 154 | 155 | 1. DataFeed 的合同,完整的 156 | 157 | 2. LinearRegression 的合同,完整的 158 | 159 | 3. A 的合同,其依附于其他合同,但不直接暴露出其他合同的内容 160 | 161 | 这种测试方式需要你先对模块的子组件进行测试。一旦子组件被验证了,就可以对模块本身进行测试。 162 | 163 | 如果 DataFeed 和 LinearRegression 的测试通过了,但 A 的测试失败了,那么我们就可以很确定问题出在 A中,或者说问题出在 A 对其中一个子组件的使用上。这种技术是减少调试工作量的好方法:我们可以快速地集中在模块 A 中可能的问题源头,而不需要浪费时间重新检查它的子组件。 164 | 165 | 为什么我们要这么麻烦?最重要的是,我们要避免产生一个 "定时炸弹"--在项目中不被注意到的情况下,在项目后期的某个尴尬时刻爆炸。通过强调对合同的测试,我们可以尽量避免那些下游的灾难。 166 | 167 | ## 设计到测试 168 | --- 169 | ### 专项测试 170 | 不要和 "奇怪的黑客" 混淆,在编码和调试时的临时测试中,我们最终可能会在即时创建一些特殊的测试。这些可能是像 console.log() 一样简单,也可能是在调试器、IDE 环境或 REPL 中交互输入的一段代码。 171 | 172 | 在调试结束后,你需要将这个临时测试正式化。如果代码坏了一次,很可能会再次坏掉。不要把你创建的测试扔掉,要把它添加到现有的单元测试库中。 173 | 174 | --- 175 | 176 | ## 建立一个测试窗口 177 | 即使是最好的测试,也不可能找到所有的 BUG;生产环境中潮湿、温暖的条件下,似乎有一些东西能把它们带出来。 178 | 这就意味着,一旦软件部署完毕,你往往需要在实际数据流经其脉络的情况下对其进行测试。与电路板或芯片不同,我们在软件中没有测试引脚,但我们可以提供模块内部状态的各种视图,而不需要使用调试器(这在生产应用中可能不方便或不可能)。 179 | 180 | 包含跟踪消息的日志文件就是这样一种机制。日志消息应该是有规律的、一致的格式;你可能希望自动解析它们,以推断出程序所采取的处理时间或逻辑路径。格式不正确或不一致的诊断程序就是这么多的 "喷出"--它们很难阅读,而且不切实际地进行解析。 181 | 182 | 另一种进入运行中的代码的机制是 "热键" 序列或神奇的 URL。当这个特殊的组合键被按下,或 URL 被访问时,就会弹出一个诊断控制窗口,显示状态信息等。这是你通常不会透露给最终用户的东西,但对于帮助台来说,它可以非常方便。 183 | 184 | 一般来说,你可以使用功能开关来为特定的用户或用户类别启用额外的诊断功能。 185 | 186 | ## 测试文化 187 | 你所编写的所有软件都要经过测试,如果不是由你和你的团队来测试,那么最终的用户也会对其进行测试,所以你最好计划好对其进行彻底的测试。稍作预想,就可以大大减少维护成本和帮助台的呼叫。 188 | 189 | 你真的只有几个选择。 190 | 191 | - 首先进行测试 192 | 193 | - 测试期间 194 | 195 | - 永不测试 196 | 197 | Test First,包括测试驱动设计,可能是大多数情况下你最好的选择,因为它可以确保测试的发生。但有时这并不是那么方便或有用,所以在编码过程中测试可以是一个很好的退路,在这里你写一些代码,摆弄它,为它写测试,然后转到下一个位子。最糟糕的选择通常被称为 "Test Later",但你在跟谁开玩笑呢?"Test Later" 真的是 "Test Never" 的意思。 198 | 199 | 一个测试文化意味着所有的测试都能通过。忽略了一堆 "总是失败" 的测试,就容易忽略所有的测试,恶性循环就开始了(见话题 3,[_软件熵_](../Chapter1/软件熵.md) )。 200 | 201 | 对待测试代码要像对待任何生产代码一样小心翼翼。保持它的解耦、干净和健壮。不要依赖不可靠的东西(参见 话题 38, [_巧合编程_](./巧合编程.md) ),比如 GUI 系统中小部件的绝对位置,或者服务器日志中的确切时间戳,或者错误信息的确切措辞。对这些东西进行测试会导致测试的结果是脆弱的。 202 | 203 | --- 204 | ## 提示 70 测试你的软件,否则你的用户会帮你测试 205 | --- 206 | 207 | 毫无疑问,测试是编程的一部分。它不是留给其他部门或员工的事情。 208 | 209 | 测试、设计、编码,都是编程的一部分。 210 | 211 | --- 212 | ### 告解 213 | 我(Dave)曾告诉人们,我不再写测试了。部分原因是为了动摇那些把测试变成了宗教的人的信心。部分原因是我说这句话是真的。 214 | 215 | 我已经写了 45 年的代码,写了 30 多年的自动化测试。对测试的思考是我对待编码的方式中内置的。这让我感觉很舒服。而我的个性坚持认为,当某件事情开始觉得舒服的时候,我应该转到别的事情上。 216 | 217 | 在这种情况下,我决定停止写测试几个月,看看它对我的代码有什么影响。令我惊讶的是,答案是"不多"。所以我花了一些时间来研究原因。 218 | 219 | 我相信答案是(对我来说)测试的大部分好处来自于思考测试及其对代码的影响。而且,在做了这么久之后,我可以在不写测试的情况下进行这种思考。我的代码仍然是可以测试的,只是没有经过测试。 220 | 221 | 但这忽略了一个事实,即测试也是与其他开发者交流的一种方式,所以我现在确实在与他人共享的代码上写了测试。 222 | 223 | Andy 说,我不应该包括这个侧边栏。他担心这会引诱没有经验的开发者不做测试。这是我的折中方案。 224 | 225 | 你应该写测试吗?是的,应该。但是,在你做了 30 年之后,你可以自由尝试一下,看看对你的好处在哪里。 226 | 227 | --- 228 | 229 | ## 相关内容包括 230 | - 话题 27 [_别开过头了_](../Chapter4/别开过头了.md) 231 | - 话题 50 [_实用入门套件_](../Chapter9/实用入门套件.md) 232 | -------------------------------------------------------------------------------- /Chapter7/命名.md: -------------------------------------------------------------------------------- 1 | # 命名 2 | 3 | 4 | > _智慧的开端是对事物合适的命名。_ 5 | > 6 | > _-- 孔子_ 7 | 8 | 一个名字里都有什么?当我们在编程时,答案是“一切!” 9 | 10 | 我们为应用程序、子系统、模块、函数、变量创建名称,我们不断地创建新的东西并赋予它们名称。这些名字非常非常重要,因为它们揭示了你的意图和信仰。 11 | 12 | 我们相信事物应该根据它们在代码中所扮演的角色来命名。这意味着,无论何时你创造了什么,你都需要停下来思考“我创造这个的动机是什么?” 13 | 14 | 这是一个很有说服力的问题,因为它能让你从解决问题的思维定势中解脱出来,让你看到全局。当你考虑一个变量或函数的角色时,你在想它有什么特别之处,它能做什么,以及它与什么交互。通常,我们发现自己意识到我们要做的事情毫无意义,都是因为我们找不到合适的名字。 15 | 16 | 名字很有意义这一观点背后有一些科学依据。事实证明,大脑能够很快地阅读和理解单词:比许多其他活动都要快。这意味着,当 17 | 我们试图理解某事时,词语具有一定的优先权。这可以用 Stroop 效应来证明 18 | 19 | 请看下面的面板。它有一个颜色名称或阴影的列表,每个都以颜色或阴影显示。但是名字和颜色不一定匹配。下面是挑战的第一部分:大声说出每种颜色的名称。 20 | 21 | ![stroop](../assets/topic44_1.png) 22 | 23 | 这个面板有两个版本。一种使用不同的颜色,另一种使用灰色阴影。如果你看到的是黑白的并且想要颜色版本,或者你在区分颜色方面有困难并且想要尝试灰度版本,请跳到 https://pragprog.com/the-pragmatic-programmer/stroop-effect 24 | 25 | 现在重复这个,但是大声说出画出这个词的颜色。更难,是吗?读的时候很容易流利,但要想认颜色就难了。 26 | 27 | 你的大脑会把书面文字当作是需要检查的东西。我们要确保我们使用的名字符合这一点。 28 | 29 | 让我们来看看几个例子。 30 | 31 | - 我们要验证访问我们的网站的人,该网站是销售用旧显卡制作的珠宝的。 32 | 33 | ```js 34 | let user = authenticate(credentials) 35 | ``` 36 | 37 | 这个变量是 user,因为它永远是 user。但是,这真的有什么意义吗?customer,或者 buyer 呢?这样我们在编码的时候就会不断的提醒我们这个人要做什么,以及这个人对我们有什么意义。 38 | 39 | - 我们有一个实例方法可以对订单进行折扣。 40 | 41 | ```java 42 | public void deductPercent(dual amount) 43 | // ... 44 | ``` 45 | 46 | 这里有两点。首先,deducturePercent 是做什么的,而不是为什么要做。然后,参数的名称, amount 充其量是个误导:它是一个绝对量,还是一个百分比? 47 | 48 | 也许这样会更好。 49 | 50 | ```java 51 | public void applyDiscount(Percentential discount) 52 | // ... 53 | ``` 54 | 55 | 现在方法的名字让它的意图更加清晰了。我们还将参数从 double 改为 Percentential ,这是我们定义的类型。我们不了解你,但在处理百分比时,我们从不知道值应该在 0 到 100 或 0.0 到 1.0 之间。使用类型记录了函数的期望值。 56 | 57 | - 我们有一个模块可以用斐波那契数列做一些有趣的事情。其中之一就是计算数列中的数字。停下来想一想,你会怎么称呼这个函数。 58 | 59 | 我们问的大多数人都会称它为 fib 。看起来似乎很合理,但请记住它通常会在其模块的上下文中调用,所以调用Fib.fib(n)。不如用 of 或 nth 来代替调用它。 60 | 61 | ```java 62 | Fib.of(0) # => 0 63 | Fib.nth(20) # => 4181 64 | ``` 65 | 66 | 在给事物起名时,你会不断地寻找澄清的方法,而这种澄清的行为会让你在写代码的过程中对你的代码有更好的理解。 67 | 68 | 然而,并不是所有的名字都要成为文学奖的候选人。 69 | 70 | --- 71 | ### 证明规则的例外 72 | 虽然我们在代码上力求清晰,但品牌塑造完全是另一回事。 73 | 74 | 有一个既定的传统,那就是项目和项目团队应该有一个晦涩、"聪明 "的名字。Pokemon、Marvel 人物、可爱的哺乳动物、《权力的游戏》角色的名字,你说了算。 75 | 76 | 从字面上看,就是这样。 77 | 78 | --- 79 | 80 | ## 尊重文化 81 | 大多数计算机入门文章都会告诫你千万不要使用单字母变量,如 i、j 或 k。 82 | 83 | 我们认为他们错了。算是吧。 84 | 85 | 事实上,这取决于特定的编程语言或环境的文化。在 C 语言中,i、j 和 k 是传统的循环增量变量,s 是用来表示字符串,以此类推。如果你在那个环境下编程,你就会习惯于看到这样的环境,违反这个规范会让人觉得很刺耳(因此也是错误的)。另一方面,在不同的环境中使用该规范也是错误的。你永远不会做像这个 Clojure 的例子一样,把一个字符串分配给变量i,这样的事情是很可怕的。 86 | 87 | ```clojure 88 | (let [i "Hello World"] (println i)) 89 | ``` 90 | 91 | 有些语言社区喜欢使用 camelCase,用内嵌式大写字母,而另一些语言社区则喜欢用 snake_case,用内嵌式下划线来分隔单词。语言本身当然也会接受这两种,但这并不意味着它是正确的。尊重当地的文化。 92 | 93 | 有些语言允许在名称中使用 Unicode 的子集。在使用诸如 "ɹǝsn" 或 "εξρχεται"这样的名字之前,请先了解社区的期望。 94 | 95 | ## 一致性 96 | 爱默生以写过 "愚蠢的一致性是小脑袋里的哈巴狗........... "而闻名于世,但爱默生并不在程序员团队中。 97 | 98 | 每个项目都有自己的词汇库:对团队有特殊意义的行话词汇。对于一个创建在线商店的团队来说,"秩序" 的含义是一回事,而对于一个负责记录宗教团体血统的团队来说,"秩序" 的含义则大相径庭。团队中的每一个人都要知道这些词的含义,并且坚持使用这些词,这一点很重要。 99 | 100 | 一种方法是鼓励大家多交流。如果每个人都结对编程,而且结对频繁切换,那么行话就会渗透性地传播。 101 | 102 | 另一个方法是在上面有一个项目术语表,列出对团队有特殊意义的术语。这是一个非正式的文档,可能是在 wiki 上维护,也可能只是在某个地方的墙上挂着索引卡。 103 | 104 | 一段时间后,项目术语会有自己的生命力。当大家对这些词汇熟悉了之后,你就可以把这些行话作为一种速记,准确、简洁地表达出很多意思。(这正是模式化的语言 )。 105 | 106 | ## 重命名更难 107 | 无论你在前期投入多少努力,事情都会发生变化。代码会被重构,用法会发生变化,意义也会发生微妙的改变。如果你在更新名称的时候不警惕,你会很快陷入比无意义的名称更糟糕的噩梦:误导性的名称。你是否曾让人解释过代码中不一致的地方,比如说 "名为 getData 的例程真的是把数据写到了归档文件中"? 108 | 109 | 就像我们在话题 3 [_软件熵_](../Chapter1/软件熵.md) 中讨论的那样,当你发现问题时,就立刻修复它--也就是现在立刻马上 now。当你看到一个不再表达意图的名称,或者是误导性的或令人困惑的名称时,就把它修好。你有完整的回归测试,所以你会发现任何你可能错过的情况。 110 | 111 | --- 112 | ## 提示 74 命名好;必要时再重命名 113 | --- 114 | 115 | 如果由于某种原因,你无法更改现在的错误名称,那么你就有了更大的问题:违反 ETC(见话题 8,[_好设计的本质_](../Chapter2/好设计的本质.md))。先解决这个问题,然后改掉违规的名字。让重命名变得容易,而且要经常做。 116 | 117 | 否则你就得向团队里的新成员解释,getData 真的是把数据写到文件里,并且您必须直面这一点。 118 | 119 | ## 相关内容包括 120 | - 话题 3 [_软件熵_](../Chapter1/软件熵.md) 121 | - 话题 40 [_重构_](./重构.md) 122 | - 话题 45 [_需求坑_](../Chapter8/需求坑.md) 123 | 124 | ## 挑战 125 | - 当你发现一个函数或方法的名字过于通用时,试着重命名,以表达它真正的作用。现在,它是一个更容易被重构的目标。 126 | 127 | - 在我们的例子中,我们建议使用更具体的名称,如 buyer 等,而不是更传统和通用的 user。对于你来说,还有什么其他的名字可以更好的习惯性使用? 128 | 129 | - 你的系统中的名称是否与域中的用户术语一致?如果不一致,原因是什么?这是否会导致团队的认知失调,造成斯特罗普效应式的认知失调? 130 | 131 | - 你的系统中的名称是否难以更改?你能做什么来修复那个特定的破窗? 132 | -------------------------------------------------------------------------------- /Chapter7/在某处保持安全.md: -------------------------------------------------------------------------------- 1 | # 在某处保持安全 2 | 3 | > _好篱笆造就好邻居。_ 4 | > 5 | > _-- 罗伯特·弗罗斯特,《修墙》_ 6 | 7 | 在第一版关于代码耦合的讨论中,我们提出了一个大胆而天真的说法。"我们不需要像间谍或异见者一样偏执。" 我们错了。事实上,你确实需要这样的偏执狂,每天都要如此。 8 | 9 | 在我们写这篇文章的时候,每天的新闻都充满了毁灭性的数据泄露、系统被劫持和网络欺诈的故事。数以亿计的记录一次被盗,数十亿美元的损失和修复费用--这些数字每年都在快速增长。在绝大多数情况下,这并不是因为攻击者非常聪明,甚至隐隐约约约有能力。 10 | 11 | 而是因为开发者的粗心大意。 12 | 13 | ## 其他 90% 14 | 编码的时候,你可能会经历几次 "它能用了!"和 "为什么不能用了?"的循环,偶尔也会出现 "不可能发生这样的事情..........." 在这个上坡的过程中,经过几次上坡和颠簸之后,很容易对自己说:"哎呀,都成功了!"并宣布代码完成了。当然,这还没有完成。你已经完成了90%,但现在你要考虑的是另外的90%。 15 | 16 | 接下来你要做的事情就是分析代码中可能出错的地方,并将这些地方加入到你的测试套件中。你会考虑到一些事情,比如传入不好的参数,泄露或不可用的资源;诸如此类的事情。 17 | 18 | 在过去的好日子里,这种对内部错误的评估可能已经足够了。但今天,这仅仅是个开始,因为除了内部原因造成的错误之外,你还需要考虑外部行为者如何故意搞砸系统。但也许你会抗议说:"哦,没有人会在乎这个代码,它不重要,甚至没有人知道这个服务器......" 外面的世界很大,而且大部分都有联系。不管是地球另一端的无聊孩子,国家支持的恐怖主义,犯罪团伙,企业间谍,甚至是报复性的前男友,他们都在外面,都在瞄准你。一个没有补丁的、过时的系统在开放网络上的存活时间是以分钟为单位,甚至更短。 19 | 20 | 通过隐蔽性来实现安全是行不通的。 21 | 22 | ## 安全的基本原则 23 | 务实的程序员都有一种健壮的偏执狂,不仅对我们自身的缺陷和局限性有一定的认识,同时也认识到外部攻击者会抓住任何机会来破坏我们的系统。你的特定开发和部署环境会有自己的安全需求,但有一些基本原则你应该时刻铭记在心。 24 | 25 | 1. 最大限度地减少攻击面积 26 | 27 | 2. 最低限度的特权原则 28 | 29 | 3. 安全默认值 30 | 31 | 4. 加密敏感数据 32 | 33 | 5. 保持安全更新 34 | 35 | 下面我们就来逐一看一看。 36 | 37 | ### 最大限度地减少攻击面积 38 | 系统的攻击面积是指攻击者可以输入数据、提取数据或调用执行服务的所有访问点的总和。以下是几个例子。 39 | 代码的复杂性导致了攻击向量 40 | 41 | 代码的复杂性使攻击面变大,有更多的机会产生意想不到的副作用。把复杂的代码看成是使表面积变得更加多孔,容易被感染。再说一次,简单的、更小的代码更好。更少的代码意味着更少的bug,更少的安全漏洞机会。更简单、更紧密、更复杂的代码更容易推理,更容易发现潜在的弱点。 42 | 43 | _输入数据是攻击的载体_ 44 | 45 | 永远不要相信来自外部实体的数据,在将数据传递给数据库、视图渲染或其他处理之前,一定要对其进行消毒处理。"[65]有些语言可以帮助解决这个问题。比如在 Ruby 中,持有外部输入的变量会被玷污,这就限制了对其进行什么操作。例如,这段代码显然是使用 wc 实用程序来报告一个文件的名称在运行时提供的文件中的字符数。 46 | 47 | ```ruby 48 | puts "Enter a file name to count:" 49 | name = gets 50 | system("wc -c #{name}") 51 | ``` 52 | 53 | 恶意的用户可以用这样的方式进行破坏。 54 | 55 | ```bash 56 | Enter a file name to count: 57 | test.dat; rm -rf / 58 | ``` 59 | 60 | 但是,将 SAFE 级别设置为 1 会对外部数据造成污点,这意味着它不能在危险的情况下使用。 61 | 62 | ```bash 63 | >> $SAFE = 1 64 | puts "Enter a file name to count:" 65 | name = gets 66 | system("wc -c #{name}") 67 | $ ruby taint.rb 68 | Enter a file name to count: 69 | test.dat; rm -rf / 70 | code/safety/taint.rb:5:in `system': Insecure operation - system (SecurityError) 71 | from code/safety/taint.rb:5:in `main' 72 | ``` 73 | 74 | _未经认证的服务是一种攻击手段_ 75 | 76 | 就其本质而言,世界上任何地方的任何用户都可以调用未经认证的服务,因此,如果不做任何其他处理或限制,你至少立即就会给拒绝服务攻击创造了机会。最近有不少高度公开的数据泄露事件都是由于开发者不小心把数据放到了云中的未经认证的、可公开读取的数据存储中。 77 | 78 | _经过认证的服务是一个攻击载体_ 79 | 80 | 将授权用户的数量控制在绝对最低限度。剔除不使用、老旧或过时的用户和服务。已发现许多联网设备包含简单的默认密码或未使用、未受保护的管理账户。如果一个具有部署凭证的账户被泄露,你的整个产品就会被泄露。 81 | 82 | _输出数据是一个攻击载体_ 83 | 84 | 有一个关于系统尽职尽责地报告错误信息密码被另一个用户使用的系统的故事(可能是远古的)。不要泄露信息。确保你所报告的数据适合于该用户的授权。截断或掩盖潜在的风险信息,如社会保险或其他政府身份号码。 85 | 86 | _调试信息是一种攻击的载体_ 87 | 88 | 没有什么比在你的本地 ATM 机、机场自助机或崩溃的网页上看到完整的栈跟踪数据更让人心动的了。旨在使调试更容易的信息也可以使破解更容易。确保任何 "测试窗口"(这里讨论过)和运行时的异常报告都能避免被间谍的眼睛看到。 89 | 90 | --- 91 | ## 提示 72 保持简单,尽量减少攻击面 92 | --- 93 | 94 | ### 最低限度的特权原则 95 | 另一个关键的原则是,总是在最短的时间内使用最少的权限。换句话说,不要自动抢占最高权限级别的权限,比如root 或 Administrator。而如果需要那个最高级别的权限,就拿下它,做最少的工作,并迅速放弃权限,以减少风险。这个原则可以追溯到70年代初。 96 | 97 | > _系统的每一个程序和每一个有权限的用户都应该使用最少的权限来完成工作。_ 98 | > 99 | > _-- Jerome Saltzer, Communications of the ACM, 1974._ 100 | 101 | 例如,Unix 衍生系统中的登录程序最初是以 root 权限执行的。但是,当它完成了正确的用户身份验证后,它就会把高级权限降为用户的高级权限。 102 | 103 | 这并不仅仅适用于操作系统的权限级别。你的应用是否实现了不同级别的访问权限?是否是钝器,比如 "管理员 "与 "用户"?" 如果是这样,可以考虑更细化,将敏感资源划分为不同的类别,而单个用户只对其中的某些类别拥有权限。 104 | 105 | 这种技术与最小化表面积的想法是相同的,即通过时间和权限级别来减少攻击载体的范围。在这种情况下,少即是多。 106 | 107 | ### 安全默认值 108 | 你的应用程序上的默认设置,或者你的网站上的用户,应该设置为最安全的值。这些值可能不是最友好或最方便的值,但最好是让每个人根据自己的情况,在安全和方便性之间进行权衡。 109 | 110 | 例如,密码输入的默认值可能是隐藏输入的密码,用星号("*")代替每个字符。如果你是在拥挤的公共场所输入密码,或者在大庭广众之下输入密码,这是一个合理的默认设置。但有些用户可能希望看到密码被拼写出来,也许是为了方便使用。如果几乎没有什么风险被人偷看,这对他们来说是一个合理的选择。 111 | 112 | ### 加密敏感数据 113 | 不要将个人身份信息、财务数据、密码或其他凭证留在纯文本中,无论是在数据库还是其他外部文件中。如果数据被暴露,加密可以提供额外的安全保障。 114 | 115 | 在话题 19,[_版本控制_](../Chapter4/版本控制.md) 中,我们强烈建议将项目所需的一切都放在版本控制下。嗯,几乎是所有的东西。这里有一个主要的例外。 116 | 117 | 不要在版本控制中检查机密、API 密钥、SSH 密钥、加密密码或其他凭据。 118 | 119 | 密钥和秘密需要单独管理,通常通过配置文件或环境变量作为构建和部署的一部分。 120 | 121 | --- 122 | ### 反密码模式 123 | 安全性的根本问题之一是,很多时候,好的安全性往往与常识或惯例背道而驰。例如,你可能认为严格的密码要求会增加你的应用程序或网站的安全性。那你就错了。 124 | 125 | 严格的密码策略实际上会降低你的安全性。下面是一个非常糟糕的想法的简短清单,以及 NIST 的一些建议。 126 | 127 | - 不要将密码长度限制在64个字符以下。NIST建议将256个字符作为最佳的最大长度。 128 | 129 | - 不要截断用户选择的密码。 130 | 131 | - 不要限制特殊字符,如 []();&%$# 或 / 。参见本节前面关于 Bobby Tables 的说明。如果你的密码中的特殊字符会危及你的系统,那么你的问题就更大了。NIST说要接受所有打印的ASCII字符、空格和Unicode。 132 | 133 | - 不要向未经认证的用户提供密码提示,或提示特定类型的信息(如 "你的第一只宠物的名字是什么?")。 134 | 135 | - 不要禁用浏览器中的粘贴功能。瘫痪浏览器和密码管理器的功能并不会使你的系统更安全,事实上,它驱使用户创建更简单、更短的密码,更容易被破解。美国的 NIST 和英国的国家网络安全中心都明确要求验证器允许粘贴功能,原因就在于此。 136 | 137 | - 不要强行规定其他的组成规则。例如,不要强制规定任何特定的大写和小写、数字或特殊字符的组合,或禁止重复字符等。 138 | 139 | - 不要武断地要求用户在一定时间后更改密码。只有在有正当理由的情况下,例如,如果出现漏洞,才可以这样做。 140 | 141 | 你要鼓励使用长的、随机的、具有高度熵的密码。人为的限制会限制熵,并鼓励用户养成不良的密码习惯,使你的用户账户容易被接管。 142 | 143 | --- 144 | 145 | ### 保持安全更新 146 | 更新计算机系统可能是一个巨大的痛苦。你需要那个安全补丁,但作为一个副作用,它会破坏你的应用程序的某些部分。你可以决定等待,并将更新推迟到以后再进行。这是一个可怕的想法,因为现在你的系统容易受到已知漏洞的攻击。 147 | 148 | --- 149 | ## 提示 73 快速应用安全补丁 150 | --- 151 | 152 | 这个提示会影响到每一台联网的设备,包括手机、汽车、电器、个人笔记本、开发者机、构建机器、生产服务器和云图象等所有联网设备。一切都会影响到。如果你认为这并不重要,只要记住,历史上最大的数据泄露事件(到目前为止)都是由系统更新落后造成的。 153 | 154 | 不要让它发生在你身上。 155 | 156 | ## 常识与加密 157 | 重要的是要记住,当涉及到密码学的事情时,常识可能会让你失望。当涉及到密码时,第一条也是最重要的规则是永远不要自己动手。即使对于密码这样简单的东西,常见的做法也是错误的(见侧边栏《密码反模式》)。一旦你进入了密码世界,即使是最微小的、看起来最不起眼的错误也会危及到一切:你的自制的巧妙新加密算法很可能在几分钟内就会被专家破解。你不希望自己做加密。 158 | 159 | 正如我们在其他地方说过的那样,只依靠可靠的东西:经过严格审核、彻底检查、维护良好、经常更新,最好是开源的库和框架。 160 | 161 | 除了简单的加密任务之外,还要认真审视一下你的网站或应用程序的其他安全相关功能。以认证为例。 162 | 163 | 为了实现自己的密码或生物识别认证登录,你需要了解哈希和 salts 是如何工作的,破解者如何使用 Rainbow 表等东西,为什么你不应该使用 MD5 或 SHA1,以及其他一系列的问题。即使你把这些都弄明白了,但到最后,你还是有责任保护好数据,并保持数据的安全,无论出现什么新的法律和法律义务,你都要遵守。 164 | 165 | 或者,你可以采取务实的方法,通过使用第三方认证供应商,让别人来负责。如果您是大型企业组织的一部分,这可能是您在内部运行的现成服务,或者从云中的服务提供商那里获得的服务。身份验证服务通常可以从电子邮件、电话或社交媒体提供商那里获得,这可能适合你的应用,也可能不适合你的应用。无论如何,这些人整天都在维护他们的系统安全,而且他们比你更擅长于此。 166 | 167 | 在外部注意安全。 168 | 169 | ## 相关内容包括 170 | - 话题 25 [_断言式编程_](../Chapter4/断言式编程.md) 171 | - 话题 38 [_巧合编程_](./巧合编程.md) 172 | - 话题 24 [_死程序不说谎_](../Chapter4/死程序不说谎.md) 173 | - 话题 23 [_契约设计_](../Chapter4/契约设计.md) 174 | - 话题 45 [_需求坑_](../Chapter8/需求坑.md) 175 | -------------------------------------------------------------------------------- /Chapter7/基于属性的测试.md: -------------------------------------------------------------------------------- 1 | # 基于属性的测试 2 | 3 | > _Доверяй, но проверяй_ 4 | > _(信任,但要核实)_ 5 | > 6 | > _-- 俄罗斯谚语_ 7 | 8 | 我们建议为你的函数编写单元测试。你要做的是根据你对所测试的东西的了解,思考可能会有问题的典型事物。 9 | 10 | 不过,这段话中潜伏着一个小的但潜在的重大问题。如果你写的是原始代码,而你写的是测试,是否有可能在两者中都表达了一个不正确的假设?代码通过了测试,因为根据你的理解,它做了它应该做的事情。 11 | 12 | 绕过这个问题的一个方法是让不同的人写测试和被测试的代码,但我们不喜欢这样做:正如我们在话题 41 [_代码测试_](./代码测试.md) 中说的那样,对测试的思考最大的好处之一就是它对你写代码的指导方式。当测试的工作从编码中分离出来,你就失去了这一点。 13 | 14 | 相反,我们更倾向于另一种方式,即由不同意你的先入为主的计算机为你做一些测试。 15 | 16 | ## 契约、反变量和属性 17 | 在话题 23 [_契约设计_](../Chapter4/契约设计.md) 中,我们讲到了这样一个概念,即代码有它所满足的契约:当你给它输入时,你满足了条件,它就会对它所产生的输出做出一定的保证。 18 | 19 | 还有一些代码的不变性,即当它通过一个函数时,对某些状态保持真实的东西。例如,如果你对一个列表进行排序,结果元素数将与原始的元素数相同--长度是不变的。 20 | 21 | 一旦我们弄清楚了我们的契约和不变性(我们将把这些契约和不变性集合在一起并称为属性),我们就可以用它们来实现测试的自动化。我们最终要做的就是基于属性的测试。 22 | 23 | --- 24 | ## 提示 71 使用基于属性的测试来验证你的假设 25 | --- 26 | 27 | 作为一个人为的例子,我们可以为我们的排序列表建立一些测试。我们已经建立了一个属性:排序后的列表与原始的大小相同。我们还可以声明,结果中的任何元素都不能大于后面的元素。 28 | 29 | 现在我们可以用代码来表达。大多数语言都有某种基于属性的测试框架。这个例子是在 Python 中,使用的是 Hypothesis 工具和 pytest,但原理是相当普遍的。 30 | 31 | 下面是测试的完整源码。 32 | 33 | ```python 34 | from hypothesis import given 35 | from hypothesis.strategies as some 36 | @given(some.lists(some.integers())) 37 | def 38 | test_list_size_is_inviriant_accross_sorting(a_list): 39 | original_length = len(a_list) 40 | a_list.sort() 41 | assert len(a_list) == original_length 42 | @given(some.lists(some.text())) 43 | def test_sorted_result_is_orderes(a_list): 44 | a_list.sort() 45 | for i in range(len(a_list) - 1): 46 | assert a_list[i] <= a_list[i + 1] 47 | ``` 48 | 49 | 下面是我们运行它时的结果。 50 | 51 | ```bash 52 | > pytest sort.py 53 | ======== test session starts ========= 54 | ... 55 | plugins: hypothesis-4.14.0 56 | sort.py .. 57 | [100%] 58 | ======== 2 passed in 0.95 seconds ======= 59 | ``` 60 | 61 | 没有太多戏剧性的东西。但是,在幕后,Hypothesis 把我们的两个测试都跑了一百次,每次都通过了不同的列表。列表会有长短不一,内容也会不一样。这就好像我们用 200个 随机的名单,分别做了 200 个单独的测试一样。 62 | 63 | ## 测试数据生成 64 | 和大多数基于属性的测试库一样,Hypothesis 给你提供了一种迷你语言来描述它应该生成的数据。这种语言是围绕着对 hypothesis.strategies 模块中的函数的调用来进行的,我们把它别名为 some,只是因为它读起来比较好听。 65 | 66 | 如果我们写道 67 | 68 | ```python 69 | @given(some.integers()) 70 | ``` 71 | 72 | 我们的测试函数会运行多次。每一次,它将被传递一个不同的整数。在这里,我们写道 73 | 74 | ```python 75 | @given(some.integers(min_value=5, max_value=10).map(lambda x: x * 2)) 76 | ``` 77 | 78 | 那么我们就可以得到 10 到 20 之间的偶数。 79 | 80 | 你也可以组成类型,这样 81 | 82 | ```pytyhon 83 | @given(some.listes(some.integers(min_value=1), max_size=100)) 84 | ``` 85 | 86 | 将是最多 100 个元素长的自然数的列表。 87 | 88 | 这不应该是一个关于任何特定框架的教程,所以我们将跳过一堆很酷的细节,而是看一个实际的例子。 89 | 90 | ## 寻找坏的假设 91 | 我们正在编写一个简单的订单处理和库存控制系统(因为总能多出一个)。它用一个 Warehouse 对象来模拟库存水平。我们可以查询一个仓库,查看是否有东西在库存,从库存中取出东西,并得到当前的库存水平。 92 | 93 | 下面是代码: 94 | 95 | ```python 96 | class Warehouse: 97 | def __init__(self, stock): 98 | self.stock = stock 99 | 100 | def in_stock(self, item_name): 101 | return (item_name in self.stock) and (self.stock[item_name] > 0) 102 | 103 | def take_from_stock(self, item_name, quantity): 104 | if(quantity <= self.stock[item_name]): 105 | self.stock[item_name] -= quantity 106 | else: 107 | raise Exception("Oversold {}".format(item_name)) 108 | 109 | def stock_count(self, item_name): 110 | return self.stock[item_name] 111 | ``` 112 | 113 | 我们写了一个基本通过了的单元测试。 114 | 115 | ```python 116 | def test_warehouse(): 117 | wh = Warehouse({"shoes": 10, "hats": 2, "umbrellas": 0}) 118 | 119 | assert wh.in_stock("shoes") 120 | assert wh.in_stock("hats") 121 | assert not wh.in_stock("umbrellas") 122 | 123 | wh.take_from_stock("shoes", 2) 124 | assert wh.in_stock("shoes") 125 | 126 | wh.take_from_stock("hats", 2) 127 | assert not wh.in_stock("hats") 128 | ``` 129 | 130 | 然后我们写了一个函数来处理从仓库订购物品的请求。它返回一个 tuple,其中第一个元素是 "ok" 或 "not available",后面是物品和请求数量。我们还写了一些测试,结果都通过了。 131 | 132 | ```python 133 | def order(warehouse, item, quantity): 134 | if warehouse.in_stock(item): 135 | warehouse.take_from_stock(item, quantity) 136 | return ("ok", item, quantity) 137 | else: 138 | return ("not available", item, quantity) 139 | def test_order_in_stock(): 140 | wh = Warehouse({"shoes": 10, "hats": 2, "umbrellas": 0}) 141 | status, item, quantity = order(wh, "hats", 1) 142 | assert status == "ok" 143 | assert item == "hats" 144 | assert quantity == 1 145 | assert wh.stock_count("hats") == 1 146 | 147 | def test_order_not_in_stock(): 148 | wh = Warehouse({"shoes": 10, "hats": 2, "umbrellas": 0}) 149 | status, item, quantity = order(wh, "umbrellas", 1) 150 | assert status == "not available" 151 | assert item == "umbrellas" 152 | assert quantity == 1 153 | assert wh.stock_count("umbrellas") == 0 154 | 155 | def test_order_unknown_item(): 156 | wh = Warehouse({"shoes": 10, "hats": 2, "umbrellas": 0}) 157 | status, item, quantity = order(wh, "bagel", 1) 158 | assert status == "not available" 159 | assert item == "bagel" 160 | assert quantity == 1 161 | ``` 162 | 163 | 从表面上看,一切看起来都很好。但在我们出货之前,让我们先添加一些属性测试。 164 | 165 | 我们知道的一点是,库存不能在我们的交易中出现和消失。这意味着,如果我们从仓库里拿了一些物品,那么我们拿的数量加上当前在仓库里的数量应该和原来在仓库里的数量是一样的。在下面的测试中,我们在测试中,我们在 "帽子 "或 "鞋子 "中随机选择物品参数,数量从 1 到 4 中选取,进行测试。 166 | 167 | ```python 168 | @given(item = some.sampled_from(["shoes", "hats"]), quantity = some.integers(min_value = 1, max_value = 4)) 169 | def test_stock_level_plus_quantity_equals_original_stock_level(item, quantity): 170 | wh = Warehouse({"shoes": 10, "hats": 2, "umbrellas": 0}) 171 | initial_stock_level = wh.stock_count(item) 172 | (status, item, quantity) = order(wh, item, quantity) 173 | if status == "ok": 174 | assert wh.stock_count(item) + quantity == initial_stock_level 175 | ``` 176 | 177 | 我们来运行它: 178 | 179 | ```bash 180 | $ pytest stock.py 181 | ... 182 | stock.py:72: 183 | ------------------------- 184 | stock.py:76: in test_stock_level_plus_quantity_equals_original_stock_level 185 | (status, item, quantity) = order(wh, item, quantity) 186 | stock.py:40: in order 187 | warehouse.take_from_stock(item, quantity) 188 | ------------------------- 189 | self = , item_name = 'hats' 190 | quantity = 3 191 | 192 | def take_from_stock(self, item_name, quantity): 193 | if quantity <= self.stock[item_name]: 194 | self.stock[item_name] -= quantity 195 | else: 196 | > raise Exception("Oversold {}".format(item_name)) 197 | E Exception: Oversold hats 198 | 199 | stock.py:16: Exception 200 | ---------------------------- Hypothesis ---------------------------- 201 | Falsifying example: 202 | test_stock_level_plus_quantity_equals_original_stock_level(item='hats', quantity=3) 203 | ``` 204 | 205 | warehouse.take_from_stock 炸开了:我们试图从仓库里取出三顶帽子,但仓库里只有两顶库存。 206 | 我们的属性测试发现了一个错误的假设:我们的 in_stock 函数只检查至少有一个给定商品的库存。相反,我们需要确保我们有足够的库存来填补订单。 207 | 208 | ```python 209 | def in_stock(self, item_name, quantity): 210 | return (item_name in self.stock) and (self.stock[item_name] >= quantity) 211 | ``` 212 | 213 | 我们也一并更改 order 函数: 214 | 215 | ```python 216 | def order(warehouse, item, quantity): 217 | if warehouse.in_stock(item, quantity): 218 | warehouse.take_from_stock(item, quantity) 219 | return ("ok", item, quantity) 220 | else: 221 | return ("not available", item, quantity) 222 | ``` 223 | 224 | 现在我们的属性测试全都通过了。 225 | 226 | ## 基于属性的测试往往会给你带来惊喜 227 | 在上一个例子中,我们用一个基于属性的测试来检查库存水平是否调整得当。这个测试发现了一个bug,但它不是与库存水平调整有关。相反,它发现了我们的 in_stock 函数中的一个 bug。 228 | 229 | 这就是基于属性的测试的威力和挫折感。它之所以强大,是因为你设置了一些生成输入的规则,设置了一些验证输出的断言,然后让它开动起来。你永远不知道会发生什么。测试可能会通过。一个断言可能会失败。或者代码可能完全失败,因为它无法处理给它的输入。 230 | 231 | 令人沮丧的是,要找出失败的原因很棘手。 232 | 233 | 我们的建议是,当一个基于属性的测试失败时,找出它传递给测试函数的参数,然后用这些值来创建一个单独的、常规的单元测试。这个单元测试对你来说有两个作用。首先,它可以让你专注于问题的解决,而不需要基于属性的测试框架对代码进行额外的调用。第二,这个单元测试可以作为回归测试。因为基于属性的测试会生成随机值,这些值会被传递到你的测试中,所以不能保证下次运行测试时,你的测试会使用同样的值。拥有一个强制使用这些值的单元测试,可以确保这个 bug 不会被忽略。 234 | 235 | ## 基于属性的测试也可以帮助你的设计 236 | 当我们谈到单元测试时,我们说过,其中一个主要的好处是它让你思考你的代码的方式:单元测试是你的 API 的第一个客户端。 237 | 238 | 基于属性的测试也是如此,但方式略有不同。它们让你从不变性和契约的角度来思考你的代码;你知道什么是不能改变的,什么是必须真实的。这种额外的洞察力对你的代码有一种神奇的效果,它可以消除边缘案例,并突出显示那些让数据处于不一致状态的函数。 239 | 240 | 我们认为,基于属性的测试与单元测试是相辅相成的:它们解决了不同的问题,并且各自带来了各自的好处。如果你目前还没有使用它们,那就试试吧。 241 | 242 | ## 相关内容包括 243 | - 话题 25 [_断言式编程_](../Chapter4/断言式编程.md) 244 | - 话题 23 [_契约设计_](../Chapter4/契约设计.md) 245 | - 话题 45 [_实用入门套件_](../Chapter9/实用入门套件.md) 246 | 247 | ## 练习 248 | ### 练习 30(尽可能回答) 249 | 再看仓库的例子。还有没有其他属性可以测试的? 250 | 251 | ### 练习 31(尽可能回答) 252 | 贵公司装运机械。每台机器都装在一个板条箱里,每个板条箱都是长方形的。板条箱的大小不一。你的工作是编写一些代码,将尽可能多的板条箱包装在一个适合运输车的单层中。你的代码的输出是所有板条箱的列表。对于每个板条箱,该列表给出了在卡车上的位置,以及宽度和高度。可以测试输出的属性是什么? 253 | 254 | ## 挑战 255 | 思考一下你目前正在研究的代码。有哪些属性:契约和不变性?你能不能用一个基于属性的测试框架来自动验证这些? 256 | -------------------------------------------------------------------------------- /Chapter7/巧合编程.md: -------------------------------------------------------------------------------- 1 | # 巧合编程 2 | 3 | 4 | 你看过老的黑白战争片吗?那些疲惫的士兵在灌木丛中小心翼翼地前进。前面有一块空地:有地雷吗,或者说可以安全通过吗?没有任何迹象表明这是一个雷区--没有标志、铁丝网或弹坑。士兵用刺刀戳了戳前面的地面,然后抽搐着,期待着爆炸。但并没有爆炸。于是,他痛苦地在田野里走了一会儿,边走边戳,边戳边看。最终,他确信这块地是安全的,他挺直了身子,骄傲地向前走去,然后就被炸成了碎片。 5 | 6 | 这名士兵最初对地雷的探测什么也没发现,但这只是运气好而已。他被引导到了一个错误的结论--结果是灾难性的。 7 | 8 | 作为开发者,我们也是在雷区工作。每天都有成百上千的陷阱,就等着我们去抓。记住这个士兵的故事,我们应该警惕错误的结论。我们应该避免靠巧合来编程---依靠运气和偶然的成功,而要有意识地进行编程。 9 | 10 | ## 如何通过巧合来编程 11 | 假设 Fred 接到了一个编程任务。弗雷德输入一些代码,试了一下,似乎可以 work。弗雷德又输入了一些代码,试了一下,看上去似乎还能正常工作。这样写了几个星期后,程序突然停止工作了,经过几个小时的尝试,他仍然不知道为什么。Fred 很可能花了相当多的时间去追着这段代码,却始终无法修复它。无论他怎么做,似乎都无法正常工作。 12 | 13 | 弗雷德不知道为什么代码会失败,因为他不知道为什么这段代码一开始就能正常工作。鉴于 Fred 所做的有限的 "测试",它似乎还能正常工作,但那只是一个巧合。在虚假的信心的支撑下,弗雷德冲锋陷阵,一蹶不振。现在,大多数聪明的人可能认识像弗雷德这样的人,但我们更清楚。我们不依赖巧合,不是吗? 14 | 15 | 有时候,我们可能会。有时候,我们很容易把一个快乐的巧合和一个有目的的计划混淆起来。让我们来看看几个例子。 16 | 17 | ### 执行中的意外 18 | 意外实现是指仅仅因为当前代码是这样写的,所以才会发生的事情。你最终依靠的是未记录的错误或边界条件。 19 | 20 | 假设你调用的例程有错误的数据。例程以特定的方式响应,而你根据这个响应进行代码编写。但作者并没有打算让例程以这种方式工作,甚至没有考虑过。当例程被 "修复 "后,你的代码可能会被破坏。在最极端的情况下,你所调用的例程可能根本不是为了做你想要的事情而设计的,但它似乎还能正常工作。以错误的顺序,或在错误的上下文中调用东西,是一个相关的问题。 21 | 22 | 这里看起来 Fred 是在拼命想用某种特定的 GUI 渲染框架在屏幕上把东西弄出来。 23 | 24 | ```js 25 | paint() 26 | invalidate() 27 | validate() 28 | revalidate() 29 | repaint() 30 | render() 31 | paintimmediately() 32 | ``` 33 | 34 | 但是,这些例程从来没有被设计成这样被调用;虽然它们看起来很好用,但这真的只是一个巧合。 35 | 36 | 雪上加霜的是,当场景最终真的被绘制出来的时候,弗雷德也不会尝试着回去把这些虚假的调用拿出来。"现在可以用了,最好还是不要管那些..........." 37 | 38 | 很容易被这种思路所迷惑。为什么要冒着风险去乱来呢?嗯,我们可以想到几个原因。 39 | 40 | - 它可能不是真的在工作--它可能只是看起来像在工作。 41 | 42 | - 你所依赖的边界条件可能只是一个意外。在不同的情况下(不同的屏幕分辨率,更多的CPU内核),它可能会有不同的行为。 43 | 44 | - 未记录的行为可能会随着库的下一次发布而改变。 45 | 46 | - 额外的和不必要的调用会使你的代码变慢。 47 | 48 | - 附加的调用也会增加引入新的bug的风险。 49 | 50 | 你写的代码别人会调用,良好的模块化和将实现隐藏在小的、文档化的接口后面的基本原则都是有帮助的。一个明确规定的约定(见 话题 23, [_契约设计_](../Chapter4/契约设计.md) )可以帮助消除误解。 51 | 52 | 对于你所调用的例程,只依靠文档化的行为。如果你无法做到,不管出于什么原因,那就把你的假设记录好。 53 | 54 | ### 足够接近还不够 55 | 我们曾经在一个大型项目中工作过,报告的数据来自于野外的大量硬件数据采集单元。由于各种后勤和历史原因,每个单元都被设置为当地时间。 由于对时区的解释相互冲突,以及夏令时政策的不一致,结果几乎都是错误的,但只差一个。该项目的开发者们养成了一个习惯,只需加一或减一就能得到正确的答案,理由是在这种情况下只差一。然后下一个函数就会看到这个值偏离了另一个方向,就把它改回来。 56 | 57 | 但是,有时它只是“暂时”关闭的事实是一个巧合,掩盖了更深层,更根本的缺陷。如果没有适当的时间处理模型,那么整个大型代码库将随着时间的流逝而发展成数量庞大的 +1 和 -1 语句。最终,没有一个是正确的,该项目被取消了。 58 | 59 | ### 幻影模式 60 | 人类被设计为可以查看模式和原因,即使只是巧合。例如,俄罗斯领导人总是在秃头和头发毛茸茸之间交替:俄罗斯秃头(或显然是秃头)国家领导人接替了一个非秃头(“毛茸茸”)的领导人,反之亦然,近200年了。 61 | 62 | 但是,尽管您不会编写依赖于下一任俄罗斯领导人是秃头还是长头发的代码,但在某些领域,我们一直都这样认为。赌徒想象彩票号码,骰子游戏或轮盘赌中的图案,而实际上这些是统计上独立的事件。在金融领域,股票和债券交易同样充满巧合,而不是实际的,可辨别的模式。 63 | 64 | 显示每 1000 个请求的间歇性错误的日志文件可能是难以诊断的竞争条件导致,或者可能是一个普通的旧错误。似乎在您的计算机上通过但在服务器上没有通过的测试可能表明这两种环境之间存在差异,或者可能只是巧合。 65 | 66 | 不要假设它,证明它。 67 | 68 | ### 上下文事故 69 | 您也可以遇到“上下文事件”。假设您正在编写实用程序模块。仅仅因为您当前正在为 GUI 环境编写代码,该模块是否必须依赖存在的 GUI?您依靠说英语的用户吗?识字的用户?您还不能保证还要依靠什么? 70 | 71 | 您是否依靠当前目录可写?在某些环境变量或配置文件上?在服务器上的时间准确无误的范围内?您是否依赖网络可用性和速度? 72 | 73 | 当您从网上找到的第一个答案中复制代码时,您确定上下文是相同的吗?还是在构建“货物崇拜”代码,仅模仿没有内容的表格? 74 | 75 | 找到一个恰好符合的答案,不等于正确的答案。 76 | 77 | ### 隐含的假设 78 | 巧合可能会在各个层面产生误导--从需求的产生到测试。测试特别容易出现错误的因果关系和巧合的结果。很容易假设 X 导致 Y,但正如我们在 20 [_调试_](../Chapter3/调试.md) 中所说:不要假设,要证明。 79 | 80 | 在各个层面上,人们在操作时都会有很多假设,但这些假设很少被记录下来,而且在不同的开发人员之间往往会发生冲突。不以既定事实为基础的假设是所有项目的祸根。 81 | 82 | --- 83 | ## 提示 62 不要用巧合去编程序 84 | --- 85 | 86 | ## 如何刻意编程 87 | 我们希望花更少的时间来编写代码,在开发周期中尽可能早地捕捉和修复错误,并在一开始就创造出更少的错误。如果我们能够有意识地进行编程,会有帮助。 88 | 89 | - 始终意识到自己在做什么。Fred 让事情慢慢的失控,直到最后被煮沸,就像 [_这里_](../Chapter1/石汤和煮青蛙.md) 的青蛙一样。 90 | 91 | - 你能不能把代码,详细的解释一下,给一个比较初级的程序员讲解一下?如果不能,也许你是在依靠巧合。 92 | 93 | - 不要蒙着眼睛写代码。试图构建一个你并不完全理解的应用程序,或者使用你不熟悉的技术,都是在邀请你被巧合误导。如果你不确定它为什么会成功,你就不知道它为什么会失败。 94 | 95 | - 从计划出发,不管这个计划是在你的脑海中,还是在鸡尾酒餐巾纸背面,或者是在白板上。 96 | 97 | - 只依靠可靠的东西。不要依赖意外或假设。如果你在特殊情况下无法分辨,那就做最坏的假设。 98 | 99 | - 把你的假设记录下来。话题 23,[_契约设计_](../Chapter4/契约设计.md),可以帮助你明确自己心中的假设,也可以帮助你把假设传达给别人。 100 | 101 | - 不要只测试你的代码,也要测试你的假设。不要猜测;实际去尝试。写一个断言来测试你的假设(见话题 25,[_断言式编程_](../Chapter4/断言式编程.md) )。如果你的断言是正确的,那么你的代码中的文档就有了改进。如果你发现你的假设是错的,那就算你自己幸运。 102 | 103 | - 优先考虑你的努力。把时间花在重要的方面;更可能的是,这些都是难的部分。如果你的基础知识或基础设施不正确,那么辉煌的钟声和口哨将变得无关紧要。 104 | 105 | - 不要成为历史的奴隶。不要让现有的代码决定了未来的代码。如果所有的代码不再合适,那么所有的代码都可以被替换。即使是在一个程序中,也不要让你已经做的事情限制了你接下来要做的事情--准备好重构(参见话题 40,[_重构_](../Chapter7/重构.md) )。这个决定可能会影响到项目进度。我们的假设是,影响要小于不做修改的成本。 106 | 107 | 所以,下次当某件事情看似可行,但你不知道为什么,要确保它不是偶然的。 108 | 109 | ## 相关内容推荐 110 | - 话题 23 [_契约设计_](../Chapter4/契约设计.md) 111 | - 话题 9 [_重复的恶魔_](../Chapter2/重复的恶魔.md) 112 | - 话题 43 [_在某处保持安全_](../Chapter7/在某处保持安全.md) 113 | - 话题 4 [_石汤和煮青蛙_](../Chapter1/石汤和煮青蛙.md) 114 | - 话题 34 [_共享状态不正确_](../Chapter6/共享状态不正确.md) 115 | 116 | ## 练习 117 | ### 练习 24 (可能的答案) 118 | 来自供应商的数据馈送给你一个代表键-值对的数组。DepositAccount 的 key 将持有相应值中的账号的字符串。 119 | 120 | ```elixir 121 | [ 122 | ... 123 | {:DepositAccount, "564-904-143-00"} 124 | ... 125 | ] 126 | ``` 127 | 128 | 在测试中,它在 4 核的开发者笔记本和 12 核的构建机上都能完美地工作,但在容器中运行的生产服务器上,却总是收到错误的账号。这到底是怎么回事? 129 | 130 | ### 练习 25 (可能的答案) 131 | 你正在为语音提示的自动拨号器编码,并需要管理一个联系人信息数据库。国际电联规定,电话号码不应超过15位数,所以你将联系人的电话号码存储在一个保证至少有15位数的数字字段中。你在整个北美地区进行了彻底的测试,一切似乎都很顺利,但突然间你收到了来自世界其他地区的投诉。为什么呢? 132 | 133 | ### 练习 26 (可能的答案) 134 | 你写了一个应用程序,可以为一个能容纳 5000 人的游轮餐厅的普通菜谱进行缩放。但你收到的投诉是,换算不精确。你检查了一下,代码使用的是 16 杯兑一加仑的换算公式。是的,对吧? 135 | -------------------------------------------------------------------------------- /Chapter7/当你编程时.md: -------------------------------------------------------------------------------- 1 | # 当你编程时 2 | 3 | 4 | 传统的观点认为,一旦项目进入编码阶段,大部分工作都是机械地将设计转成可执行的语句。我们认为,这种态度是软件项目失败的最大原因,很多系统最终都是丑陋的、低效的、结构不良的、不可维护的,或者说是纯粹的错误。 5 | 6 | 编码不是机械的。如果是这样,人们在 20 世纪 80 年代初寄予厚望的 CASE 工具早就取代了程序员。每时每刻都需要做一些决定,这些决定需要仔细思考和判断,如果要想让程序能够长期、准确、高效地运行,就必须要做出这些决定。 7 | 8 | 不是所有的决定都是有意识的。你可以更好地驾驭你的本能和非意识的想法,当你学习话题 37,[_聆听你的蜥蜴脑_](./聆听你的蜥蜴脑.md)。我们将看到如何更仔细地倾听,并研究如何积极回应这些有时令人讨厌的想法。 9 | 10 | 但是,倾听你的本能并不意味着你就可以自动驾驶飞行。那些不主动思考代码的开发者是在巧合的情况下进行编程--代码可能会成功,但没有特别的原因。在话题 38 [_巧合编程_](./巧合编程.md) 中,我们提倡大家更积极地参与到编码过程中来。 11 | 12 | 虽然我们编写的大部分代码都能快速执行,但我们偶尔也会开发出一些有可能让最快的处理器也会疲于奔命的算法。在话题 39 [_算法速度_](./算法速度.md) 中,我们讨论了估计代码速度的方法,并给出了一些提示,说明如何在潜在问题发生之前发现潜在问题。 13 | 14 | 务实的程序员会对所有的代码进行批判性的思考,包括我们自己的代码。我们不断地在我们的程序和设计中看到改进的空间。在话题 40 [_重构_](./重构.md) 中,我们将探讨一些技巧,帮助我们不断地修复现有的代码。 15 | 16 | 测试不是为了寻找 bug,而是为了获得代码的反馈:设计、API、耦合等方面。这意味着,测试的主要好处是在你思考和编写测试的时候发生,而不仅仅是在你运行测试的时候。我们将在话题 41 [_代码测试_](./代码测试.md) 中探讨这个想法。 17 | 18 | 当然,当你测试你自己的代码时,你可能会带着自己的偏见来完成任务。在话题 42 [_基于属性的测试_](./基于属性的测试.md) 中,我们将看到如何让计算机为你做一些大范围的测试,以及如何处理不可避免的 bug。 19 | 20 | 关键是你写的代码要有可读性,并且易于推理。外面的世界是个残酷的世界,充斥着一些不良行为者,他们积极地试图侵入你的系统并造成伤害。我们将讨论一些非常基本的技巧和方法来帮助你,这就是话题 43, [_在某处保持安全_](./在某处保持安全.md) 21 | 22 | 最后,软件开发中最难的事情之一是 话题 44,[_命名_](./命名.md) 我们必须给很多东西起名字,在很多方面,我们选择的名字定义了我们所创造的现实。当你在编码的时候,你需要注意到任何潜在的语义漂移。 23 | 24 | 我们中的大多数人在驾驶汽车时,基本上都是靠自动驾驶,我们不会明确地命令脚踩油门,或者手臂转动方向盘,我们只是想 "减速,然后右转"。然而,好的、安全的司机会不断地审视情况,检查潜在的问题,并将自己置于良好的位置,以备不测。编码工作也是如此--这可能在很大程度上是例行公事,但保持清醒的头脑可以很好地避免灾难的发生。 25 | -------------------------------------------------------------------------------- /Chapter7/算法速度.md: -------------------------------------------------------------------------------- 1 | # 算法速度 2 | 3 | 4 | 在话题 15 估计 中,我们讲到了估计的事情,比如走遍全城需要多长时间,或者一个项目需要多长时间才能完成。然而,还有另一种估算,实用型程序员几乎每天都在使用:估算算法使用的资源--时间、处理器、内存等。 5 | 6 | 这种估算往往是至关重要的。给定一个选择,你会选择哪一种方法来做某件事,你会选择哪一种?你知道你的程序在1000 条记录下运行多久,但如何扩展到 1000 万条记录?代码中的哪些部分需要优化? 7 | 8 | 事实证明,这些问题往往可以用常识、一些分析,以及一种叫做 "大O" 的近似值的书写方法来回答。 9 | 10 | ## 我们所说的估算算法是什么意思? 11 | 大多数非琐碎的算法都会处理某种可变的输入---排序 n 个字符串、反转一个 m * n 的矩阵,或者用 n 位密钥解密消息。通常情况下,这种输入的大小会影响算法:输入越大,运行时间越长,或者使用的内存越多。 12 | 13 | 如果这种关系始终是线性的(这样,时间就会与 n 的值成正比增加),这部分就不重要了。然而,大多数重要的算法都不是线性的。好消息是,很多都是亚线性的。比如说,二分查找,在找到匹配的时候不需要看每一个候选者。坏消息是,其他的算法比线性算法差得多;运行时间或内存需求的增加速度远比 n 要大得多。 一个需要一分钟处理 10 个项目的算法可能要花上一辈子的时间来处理 100 个项目。 14 | 15 | 我们发现,每当我们写任何包含循环或递归调用的东西,我们都会下意识地检查运行时间和内存需求。这很少是一个正式的过程,而是快速确认我们在这种情况下所做的事情是合理的。然而,我们有时确实会发现自己要进行更详细的分析。这时,Big-O 记号法就派上了用场。 16 | 17 | ## Big-O 记号法 18 | Big-O 记号,写成 O(),是一种处理近似值的数学方法。当我们写出一个特定的排序例程对 n 条记录进行排序时,需要大概 O( n * n ) 我们只是说,最坏的情况下所花费的时间会随着 n 的平方而变化,记录的数量增加一倍,时间大约会增加四倍。把它看作是在 O 的顺序上的意思。 19 | 20 | O () 记号把我们所测量的东西(时间、内存等)的值设为上限。如果我们说一个函数需要 O(n * n) 时间,那么我们知道它所需要的时间的上界不会比 n * n 大, 有时我们会得出相当复杂的 O() 函数,但由于最高阶的项会随着 n 的值增加而支配着数值,所以惯例是去掉所有的低阶项,而不需要显示任何常数乘法。 21 | 22 | O(n * n/2 + 3n) 跟 O(n * n/2) 跟 O(n * n) 都一样 23 | 24 | 这实际上是 O (n) 记号法的一个特点,一个 O (n * n) 算法可能比另一个 O (n * n) 算法快 1000 倍,但你不会从记号法中知道。Big-O 永远不会给你实际的时间或内存或其他什么的数字:它只是告诉你这些值会随着输入的变化而变化。 25 | 26 | 图 3,各种算法的运行时间,显示了你会遇到的几种常见的 O () 记号,以及每类算法的运行时间比较图。很明显,一旦过了 O (n2) 这关,事情很快就开始失控了。 27 | 28 | ||| 29 | |:--:|:--:| 30 | |O(1)|常量(访问数组中的元素,简单语句| 31 | |O(lgn)|对数(二进制搜索)。对数的基数并不重要,所以这相当于 O (logn)| 32 | |O(n)|线性(顺序搜索)| 33 | |O(nlgn)|比线性差,但也差不了多少。(快速排序、堆排序的平均运行时间)| 34 | |O (n * n)|平方(选择和插入排序)| 35 | |O(n * n * n)|立方(两个 n * n 矩阵的乘积)| 36 | |O(c的n次方)|指数法(巡回推销员问题,集分法)| 37 | 38 | ![算法速度](../assets/topic39_1.png) 39 | 40 |
41 |

图3.各种算法的运行时间

42 |
43 | 44 | 例如,假设你有一个例程,处理 100条 记录需要 1 秒。处理 1000 条记录需要多长时间?如果你的代码是 O (1),那么它仍然需要一秒钟。如果是 O (lg(n)),那么你可能要等 3 秒左右。 O (n) 会显示线性增加到十秒,而 O (nlg(n)) 则需要 33 秒左右。如果你运气不好,有一个 O (n * n) 的例程,那么在它做它的事情的时候,你可以再等 100秒。如果你使用的是指数级别的算法 O (2 的 n 次方),你可能会想喝杯咖啡--你的例程应该在 10256 年内完成。让我们知道宇宙是如何结束的。 45 | 46 | O() 符号不只适用于时间,你可以用它来表示算法使用的任何其他资源。例如,通常情况下,能够对内存消耗进行建模是非常有用的(参见练习中的例子) 47 | 48 | ## 常识性估算 49 | 你可以用常识估算出许多基本算法的顺序。 50 | 51 | _简单的循环_ 52 | 53 | 如果一个简单的循环从 1 到 n 运行,那么算法很可能是 O(n) - 时间随 n 线性增加。 54 | 55 | _嵌套循环_ 56 | 57 | 如果你在另一个循环中嵌套一个循环,那么你的算法就会变成 O(m * n),其中 m 和 n 是两个循环的极限。这种情况通常发生在简单的排序算法中,比如冒泡排序,外循环依次扫描数组中的每个元素,内循环计算出该元素在排序结果中的位置。这样的排序算法往往是O(n * n)。 58 | 59 | _二进制切分算法_ 60 | 61 | 如果你的算法在每次循环时将其考虑的东西集减半,那么它很可能是对数,O(lg(n)) 对一个排序列表的二进制搜索,遍历二进制树,找到机器字中的第一个集位,都可以是O(lg(n))。 62 | 63 | _分而治之_ 64 | 65 | 将输入进行分区,对两部分独立工作,然后合并结果的算法可以是 O(nlg(n))。最经典的例子是快速排序,它的工作原理是将数据分成两半,然后递归排序。虽然从技术上讲是 O(n * n),但由于它的行为在被送入排序的输入时性能会下降,所以快速排序的平均运行时间是 O(lg(n))。 66 | 67 | _组合式_ 68 | 69 | 每当算法开始关注事物的重复运算时,它们的运行时间可能会失控。这是因为 permutations 涉及到因式(有5!= 5 * 4 * 3 * 2 * 1 = 120 个从 1 到 5 的数位数的 permutations)。对 5 个元素的组合式算法进行计时:运行 6 个元素的算法需要 6 次,运行 7 个元素的算法需要 42 次。例子包括许多公认的硬问题的算法--旅行推销员问题,把东西最好地打包到一个容器里,把一组数字分区,使每一组的总和相同,等等。通常情况下,启发式算法被用来减少这些类型的算法在特定问题领域的运行时间。 70 | 71 | ## 算法速度在实践中的应用 72 | 在你的职业生涯中,你不太可能花很多时间来写排序例程。你可以使用库中的那些,可能会比你不费吹灰之力就能写出的东西都要好。然而,我们之前描述的基本类型的算法会一次又一次地出现。每当你发现自己写了一个简单的循环,你就知道你有一个 O(n) 算法。如果这个循环包含了一个内循环,那么你就会看到 O(m*n)。你应该问自己,这些值能有多大。如果这些数字是有边界的,那么你就知道代码要运行多长时间。如果这些数字取决于外部因素(比如一夜之间批处理运行的记录数,或者是人名列表中的名字数),那么你可能要停下来考虑一下大值可能对你的运行时间或内存消耗的影响。 73 | 74 | --- 75 | ## 提示 63 估计你的算法的顺序 76 | --- 77 | 78 | 有一些方法你可以采取一些方法来解决潜在的问题。如果你有一个算法是 O(n * n),试着找一个分而治之的方法,可以把你的算法降到 O(nlg(n))。 79 | 80 | 如果你不确定你的代码需要多长时间,或者不确定它将使用多少内存,可以尝试运行它,改变输入记录的数量或其他可能影响运行时间的东西。然后将结果绘制出来。你应该很快就会对曲线的形状有一个很好的想法。它是向上弯曲,是一条直线,还是随着输入量的增加而变平?三四个点应该可以让你有一个想法。 81 | 82 | 此外,还要考虑你在代码本身所做的事情。一个简单的 O(n * n) 循环可能比一个复杂的 O(nlg(n)) 循环在较小的值上表现得更好,特别是如果 O(nlg(n) 算法有一个昂贵的内循环。 83 | 84 | 在所有这些理论中间,不要忘记还有一些实际的考虑。对于小的输入集,运行时间看起来可能会线性增加。但是,如果给代码喂入数百万条记录,时间会突然退化,因为系统开始颤动。如果你用随机输入键测试一个排序例程,你可能会在第一次遇到有序输入时感到惊讶。务实的程序员尽量把理论基础和实践基础都涵盖了。在做了这么多估计之后,唯一重要的时机就是你的代码在生产环境中运行的速度,用真实的数据来衡量。这就引出了我们的下一个提示。 85 | 86 | --- 87 | ## 提示 64 测试你的估计值 88 | --- 89 | 90 | 如果要获得准确的时序很难,可以使用代码分析器来计算算法中不同步骤的执行次数,并将这些数字与输入的大小进行对比。 91 | 最好的不一定是最好的 92 | 93 | 你还需要务实地选择合适的算法--最快的算法不一定是最好的。在一个小的输入集中,直接的插入排序算法和快速排序算法表现得一样好,而且编写和调试所需的时间也更短。如果你选择的算法具有较高的设置成本,你也需要小心。对于小的输入集,这样的设置可能会使运行时间相形见绌,使算法不合适。 94 | 95 | 同时要警惕过早的优化。在投入宝贵的时间去尝试改进算法之前,确定一个算法真的是瓶颈,这总是一个好主意。 96 | 97 | ## 相关内容包括 98 | - 话题 15 估计 99 | 100 | ## 挑战 101 | - 每个开发者都应该对算法的设计和分析有一定的感悟。Robert Sedgewick 在这方面写了一系列通俗易懂的书(Algorithms [SW11] An Introduction to the Analysis of Algorithms [SF13]等)。我们建议将他的书加入到你的收藏中,并将其作为阅读的重点。 102 | 103 | - 对于那些喜欢比 Sedgewick 提供的更多细节的人来说,可以阅读 Donald Knuth 的《计算机编程的艺术》(Art of Computer Programming)一书,这本书分析了各种算法。计算机编程的艺术》(The Art of Computer Programming: 基础算法[Knu97] 《计算机程序设计的艺术》第2卷:Seminumerical Algorithms [Knu97a] 《计算机程序设计的艺术》第3卷:排序和搜索[Knu98] 《计算机程序设计的艺术》第4A卷:组合算法,第1部分[Knu14]。 104 | 105 | - 在接下来的第一个练习中,我们来看看对长整数的数组进行排序。如果密钥比较复杂,而且密钥比较的开销很高,会有什么影响?密钥结构是否会影响排序算法的效率,还是说排序速度最快的总是最快的? 106 | 107 | ## 练习 108 | ### 练习 27(可能的答案) 109 | 我们用 Rust 编码了一组简单的排序例程。在你现有的各种机器上运行它们。你的数字是否遵循了预期的曲线?你可以推断出你的机器的相对速度是多少?各种编译器优化设置的影响是什么? 110 | 111 | ### 练习 28(可能的答案) 112 | 在常识性估算部分,我们声称一个二分算法的时间是 O(lg(n)),你可以证明它吗? 113 | 114 | ### 练习 29(可能的答案) 115 | 在图 3,各种算法的运行时间中, 我们声称 O(lg(n)) 跟 O(log10n) 是一样的(或对数到任何基数),你能解释为什么吗? 116 | -------------------------------------------------------------------------------- /Chapter7/聆听你的蜥蜴脑.md: -------------------------------------------------------------------------------- 1 | # 聆听你的蜥蜴脑 2 | 3 | 4 | > _只有人类才能直视一件事,掌握了所有的信息,才能做出准确的预测,甚至可能瞬间做出准确的预测,然后说不是这样的。_ 5 | > 6 | > _-- 加文-德-贝克尔(Gavin de Becker),《恐惧的礼物》。_ 7 | 8 | 加文-德-贝克尔一生的工作就是帮助人们保护自己。他的书《恐惧的礼物:及其他保护我们免受暴力的生存信号》一书就包含了他想传达的信息。这本书的一个关键主题是,作为成熟的人类,我们已经学会了忽略我们更动物的一面,我们的本能,我们的蜥蜴脑。他声称大多数在街上被攻击的人在被攻击前都会意识到不舒服或紧张。这些人只是告诉自己这感觉像是在犯傻。然后,那个身影从黑暗的门口出现.....。 9 | 10 | 本能只是对装在我们无意识的大脑中的模式的一种反应。有些是与生俱来的,有些则是通过重复学习到的。随着你作为程序员的经验的积累,你的大脑就会建立起一层层的隐性知识:有用的东西,没用的东西,错误的可能原因,所有你在生活中注意到的东西。这就是你大脑中的那一部分,当你停下来和别人聊天时,即使你没有意识到自己做了,也会点击保存文件键。 11 | 12 | 无论它们的来源是什么,本能都有一个共同点:它们没有言语。本能让你感觉到,而不是思考。因此,当本能被触发时,你不会看到一个闪闪发光的灯泡,周围包裹着一面横幅。相反,你会变得紧张,或者说是心猿意马,或者觉得这样做实在是太累了。 13 | 14 | 诀窍是首先要注意到它的发生,然后找出原因。让我们先来看看几种常见的情况,在这些情况下,你内心的蜥蜴正试图告诉你一些事情。然后我们将讨论如何让你的本能大脑摆脱保护性的包装。 15 | 16 | ## 对空白页的恐惧 17 | 每个人都害怕空荡荡的屏幕,害怕孤独地闪烁的光标被一堆虚无的东西包围。启动一个新项目(甚至是现有项目中的新模块)可能是一种不安的体验。我们中的许多人宁愿推迟开始的初始化。 18 | 19 | 我们认为有两个问题会造成这种情况,而且这两个问题的解决方法都是一样的。 20 | 21 | 一个问题是,你的蜥蜴脑试图告诉你一些东西;有某种怀疑就潜伏在感知的表面之下。而这一点很重要。 22 | 23 | 作为一个开发者,你一直在尝试一些事情,看看哪些成功了,哪些没有成功。你一直在积累经验和智慧。当你感觉到有一种絮絮叨叨的疑惑,或者面对一项任务时感到有些不情愿,这可能是经验在向你说话。聆听它的声音。你可能无法准确地找出问题的症结所在,但给它一点时间,你的疑虑很可能会结晶成更坚实的东西,成为你可以寻址的东西。让你的直觉为你的表现做出贡献。 24 | 25 | 另一个问题是比较朴素的:你可能只是害怕自己犯错。 26 | 27 | 而这是一种合理的恐惧。我们开发人员在代码中投入了大量的精力;我们可以把代码中的错误当作是对自己能力的反映。也许还有一种冒牌货综合症的因素;我们可能认为这个项目超出了我们的能力范围。我们无法看到自己要怎么到达终点;我们会走得太远,然后被迫承认自己迷失了。 28 | 29 | ## 与自己战斗 30 | 有时候,代码就这样从你的大脑中飞进编辑器里:想法就这样不费吹灰之力地变成了碎片。 31 | 32 | 而在其他的日子里,编码的感觉就像在泥泞的山路上行走。每走一步都需要付出巨大的努力,每走三步,你就会后退两步。 33 | 但是,作为一个专业的人,你要坚持不懈地走下去,踏着泥泞的脚步:你还有工作要做。不幸的是,这可能与你应该做的事情完全相反。 34 | 35 | 你的代码试图告诉你一些事情。它在说这可能比想象中的更难。也许结构或设计是错误的,也许你解决的问题是错误的,也许你只是在创造一个蚂蚁农场的 BUG。不管是什么原因,你的蜥蜴大脑正在感知来自代码的反馈,它拼命地想让你去听。 36 | 37 | --- 38 | ## 提示 61 聆听你内心蜥蜴的声音 39 | --- 40 | 41 | ## 如何跟蜥蜴沟通 42 | 我们谈了很多关于倾听你的本能,倾听你的无意识,蜥蜴脑的直觉。技巧都是一样的。 43 | 44 | 首先,停止你正在做的事情。给自己一点时间和空间,让你的大脑自己组织一下。停止思考代码,做一些相对无意识的事情,暂时远离键盘,做一些相当无意识的事情。散散步,吃个午饭,和别人聊天。也许睡一觉就可以了。让这些想法在你的大脑中层层渗透:你不能强迫它。最终它们可能会在你的意识层面上浮现出来,你就会有一个 "啊哈!" 的时刻。 45 | 46 | 如果这还不行,可以尝试将问题外化。对你写的代码进行涂鸦,或者向同事(最好是一个不是程序员的同事),或者向你的橡皮鸭解释。把你大脑的不同部分暴露在这个问题上,看看有没有人对困扰你的事情有更好的处理方案。我们已经记不清有多少次谈话中,我们中的一个人在向另一个人解释一个问题时,突然说了一句 "哦!当然了!",然后就去解决这个问题。 47 | 48 | 但是,也许你已经尝试过这些事情了,却还是卡住了。是时候行动起来了。我们需要告诉你的大脑,你要做的事情并不重要。而我们要做的是通过原型设计。 49 | 50 | ## 游戏时间到了! 51 | 安迪和戴夫都花了好几个小时找了一个空的编辑器缓冲区。我们会输入一些代码,然后看一下天花板,然后再喝一杯,再输入一些代码,然后去读一个关于一只有两条尾巴的猫的有趣故事,然后再输入一些代码,然后做选择/删除,然后重新开始。再来一次。再来一次。 52 | 53 | 这些年来,我们找到了一个脑力黑客似乎很管用。告诉自己你需要做一个原型的东西。如果你面对的是一个空白的屏幕,那么就寻找你想探索的项目的某个方面。也许你正在使用一个新的框架,想看看它是如何进行数据绑定的。或者也许是一个新的算法,你想探索它是如何在边缘情况下工作的。或者,也许你想尝试几种不同风格的用户交互方式。 54 | 55 | 如果你正在研究现有的代码,而它又在推倒重来,那就把它藏在某个地方,改成类似的原型。 56 | 57 | 做下面的事情。 58 | 59 | 1. 把 "我正在做原型 "写在一张便签上,贴在你的屏幕边上。 60 | 61 | 2. 提醒自己,原型是注定要失败的。并提醒自己即使不失败,原型也会被扔掉。这样做是没有坏处的。 62 | 63 | 3. 在你空白的编辑器缓冲区中,创建一个注释,用一句话描述你要学什么或做什么。 64 | 65 | 4. 开始编码。 66 | 67 | 如果你开始有疑虑,就看一下便签。 68 | 69 | 如果在编码的过程中,那个唠叨的疑虑突然结晶成了一个坚实的担忧,那么就解决它。 70 | 71 | 如果到了实验的最后,你还是觉得不放心,那就重新开始走一走、谈一谈、歇一歇。 72 | 73 | 但是,根据我们的经验,在第一个原型的某个阶段,你会惊讶地发现自己会随着音乐哼唱,享受着创造代码的感觉。紧张的情绪会 74 | 烟消云散,取而代之的是一种紧迫感:让我们把这个事情做好吧!" 75 | 76 | 在这个阶段,你知道该怎么做了。删除所有的原型代码,扔掉便签,用光鲜亮丽的新代码填满那个空的编辑器缓冲区。 77 | 78 | ## 不仅仅是你的代码 79 | 我们工作的很大一部分是处理现有的代码,这些代码往往是由其他人编写的。那些人的直觉会和你不同,所以他们做出的决定也会不同。不一定更差,只是不同。 80 | 81 | 你可以机械地阅读他们的代码,慢条斯理地阅读他们的代码,对那些看起来很重要的东西做笔记。这是一个苦差事,但它是有效的。 82 | 83 | 或者你可以尝试一下实验。当你发现有些事情做的方式似乎很奇怪,就把它记下来。继续这样做,寻找模式,如果你能看到是什么驱使他们以这样的方式写代码,你可能会发现理解它的工作变得容易了很多。你就能有意识地应用他们默认应用的模式。 84 | 85 | 而且你可能会在这个过程中学习到一些新的东西。 86 | 87 | ## 不仅仅是代码 88 | 在编码的时候,学会倾听你的直觉是一个重要的技能。但它也适用于大局观。有时一个设计只是感觉不对,或者一个需求的解释让你感到不安。请停下来分析一下这些感觉。如果你在一个支持的环境中,大声表达出来。探讨一下它们。很有可能在那个黑暗的门口潜伏着什么东西。听从你的直觉,在问题跳出来之前就避开它。 89 | 90 | ## 相关内容包括 91 | - 话题 22 [_工程日记_](../Chapter3/工程日记.md) 92 | - 话题 46 [_解决不可能的难题_](../Chapter8/解决不可能的难题.md) 93 | - 话题 13 [_原型和便签_](../Chapter2/原型和便签.md) 94 | 95 | ## 挑战 96 | - 是否有一些你知道自己应该做的事情,但因为感觉有点恐怖或困难而推迟了?应用本节中的技巧。把时间定格在一个小时,也许是两个小时,然后向自己保证,当钟声响起时,你会删除你做的事情。你学到了什么? 97 | -------------------------------------------------------------------------------- /Chapter7/重构.md: -------------------------------------------------------------------------------- 1 | # 重构 2 | 3 | 4 | > _我看到的周围都在变化和衰退..._ 5 | > 6 | > _-- 莱特(H. F. Lyte),《与我同行》_ 7 | 8 | 随着程序的发展,有必要重新考虑早期的决策并重新编写代码的某些部分。这个过程是完全自然的。代码需要发展变化;并不是一成不变的。 9 | 10 | 不幸的是,软件开发最常见的隐喻是建筑结构。贝特朗·迈耶(Bertrand Meyer)的经典著作《面向对象的软件构造》 [Mey97] 使用了“软件构造”一词,甚至谦虚的作者也在 2000 年代初编辑了 IEEE 软件的 软件构造 专栏。[57] 11 | 12 | 但是,使用建筑作为指导隐喻意味着以下步骤: 13 | 14 | 1. 建筑师制定蓝图。 15 | 16 | 2. 承包商挖掘地基,建造上层建筑,金属线和铅垂线,并进行最后修饰。 17 | 18 | 3. 租户从此搬入并过着幸福快乐的生活,打电话给房屋维护以解决任何问题。 19 | 20 | 嗯,软件不是那样工作的。软件不像是建筑,更像是园艺 — 它比混凝土更有机。您根据初始计划和条件在花园中种了很多东西。有些茁壮成长,另一些注定最终会变成肥料。您可以使植物彼此相对移动,以利用光照和阴影,风雨的相互作用。杂草丛生的植物会被剔除或是修剪,发生冲突的颜色可能会移到更美观的位置。您除草,并为那些需要一些额外帮助的植物施肥。您不断监视花园的健康状况,并根据需要进行调整(针对土壤,植物,布局)。 21 | 22 | 商界人士对建筑的比喻感到满意:它比园艺更科学,可重复,管理的报告层次严格,等等。但是我们并没有在建造摩天大楼,也没有受到物理和现实世界边界的束缚。 23 | 24 | 园艺比喻更接近软件开发的现实。某个例程的规模可能太大了,或者正试图完成太多任务,因此需要将其分为两个部分。无法按计划进行的操作需要除草或修剪。 25 | 26 | 重写,重做和重新构造代码统称为 重组。但是,该活动的一部分已经被实践为 重构。 27 | 28 | 马丁·福勒(Martin Fowler)将重构定义为: 29 | 30 | > 用于重组现有代码主体,改变其内部结构而不改变其外部行为的纪律技术。 [58] 31 | 32 | 此定义的关键部分是: 33 | 34 | 1. 该活动是有纪律的,而不是免费的 35 | 36 | 2. 外部行为不会改变,这不是添加功能的时候 37 | 38 | 重构并不是一种特殊的,高礼仪的,一次又一次的活动,就像在整个花园里耕种以便重新种植一样。相反,重构是一项日常活动,采取低风险的小步骤,[59]更像是除草和耙草。它是一种有针对性的,精确的方法,可以使代码易于更改,而不是对代码库进行全面的免费重写。 39 | 40 | 为了确保外部行为没有改变,您需要进行良好的自动化单元测试,以验证代码的行为。 41 | 42 | ## 您应该何时重构? 43 | 当您学到一些东西时,您可以进行重构;当您现在比去年,昨天甚至十分钟前了解的东西更好时。 44 | 45 | 也许您遇到了绊脚石,因为代码不再适合,或者您注意到应该真正合并的两件事,或者其他任何事情都使您感到“错误”,请不要犹豫更改它。没有像现在这样的时间。任何数量的情况都可能导致代码有资格进行重构: 46 | 47 | _重复_ 48 | 49 | 您发现违反了 DRY 原则(话题 9,[_重复的罪恶_](../Chapter2/重复的罪恶.md) ) 50 | 51 | _非正交设计_ 52 | 53 | 您发现了一些可以变得更正交的代码或设计(话题 10,[_正交性_](../Chapter2/正交性.md) )。 54 | 55 | _过时的知识_ 56 | 57 | 事情发生变化,需求随波逐流,您对问题的了解也会增加。代码需要跟上。 58 | 59 | _用法_ 60 | 61 | 随着系统在现实环境中被真实的人使用,您意识到某些功能现在比以前想象的要重要,而“必须具备”的功能可能就不那么重要了。 62 | 63 | _性能_ 64 | 65 | 您需要将功能从系统的一个区域移至另一区域以提高性能。 66 | 67 | _测试合格_ 68 | 69 | 是。说真的我们确实说过,重构应该是一个小规模的活动,并得到良好测试的支持。因此,当您添加了少量代码并通过了一次额外的测试后,现在您将有很大的机会深入并整理刚编写的内容。 70 | 71 | 重构代码(围绕功能移动和更新早期决策)实际上是痛苦管理中的一项练习。面对现实吧,更改源代码可能会非常痛苦:它确实有效,也许最好让自己呆一会儿。许多开发人员不愿意仅仅因为代码不正确而进入并重新打开一段代码。 72 | 73 | ### 现实世界中的并发症 74 | 因此,您去找同事或客户说:“此代码有效,但是我还需要一周时间才能完全重构它。” 75 | 76 | 我们无法打印他们的回复。 77 | 78 | 时间压力经常被用作不重构的借口。但是,这种借口不能成立:现在无法重构,并且当有更多依赖关系需要解决时,将有更多的时间投入来解决问题。那会不会有更多的时间呢?根据我们的经验。 79 | 80 | 您可能想通过使用医学类比向其他人解释此原理:将需要重构的代码视为“增长”。移除它需要侵入性手术。您现在可以进入,并在它还很小的时候将其取出。或者,您可以等待它的成长和传播-但是将其移除会变得更加昂贵且更加危险。等待更长的时间,您可能会完全失去患者的生命。 81 | 82 | --- 83 | ## 提示 65 尽早重构,经常重构 84 | --- 85 | 86 | 随着时间的推移,代码中的附带损害可能同样致命(请参阅话题 3,[_软件熵_](../Chapter1/软件熵.md) )。与大多数事情一样,重构在问题较小的情况下更容易实现,这是编码时正在进行的活动。您不需要“一个星期来重构”一段代码,而是完全重写。如果需要这种中断级别,那么您很可能无法立即执行此操作。相反,请确保将其放置在计划中。确保受影响的代码的用户知道已计划将其重写,以及这将如何影响他们。 87 | 88 | ## 您如何重构? 89 | 重构始于 Smalltalk 社区,当我们编写本书的第一版时,重构才刚刚开始吸引更多的读者,这可能要归功于有关重构的第一本主要书籍(《重构:改进现有代码的设计》[Fow18](现在在第二版中))。 90 | 91 | 本质上,重构就是重新设计。您或团队中其他人设计的任何内容都可以根据新事实,更深刻的理解,不断变化的需求等进行重新设计。但是,如果您不顾一切地放弃大量代码,则可能会发现自己比开始时的处境更糟。 92 | 93 | 显然,重构是一项需要缓慢,故意和谨慎进行的活动。马丁·福勒(Martin Fowler)提供了以下简单技巧,说明如何进行重构而不造成弊大于利:[60] 94 | 95 | 1. 请勿尝试同时重构和添加功能。 96 | 97 | 2. 开始重构之前,请确保您具有良好的测试。尽可能频繁地运行测试。这样,您将很快知道您所做的更改是否破坏了任何内容。 98 | 99 | 3. 采取简短而刻意的步骤:将字段从一类移到另一类,将两种相似的方法融合为一个超类。重构通常涉及进行许多局部更改,从而导致大规模更改。如果您的步调较小,并且在每个步骤之后进行测试,则可以避免长时间的调试。[61] 100 | 101 | --- 102 |

自动重构

103 | 104 | 在第一版中,我们指出:“这项技术尚未出现在 Smalltalk 领域之外,但这很可能会改变……。”确实如此,因为许多 IDE 中都可以使用自动重构,并且大多数主流语言都可以使用自动重构。 105 | 106 | 这些 IDE 可以重命名变量和方法,将一个较长的例程拆分为较小的例程,自动传播所需的更改,并拖放以帮助您移动代码,等等。 107 | 108 | --- 109 | 110 | 我们将在话题 41,[_代码测试_](./代码测试.md) 和 [_无情的连续测试_](../Chapter9/实用入门套件.md) 中的大规模测试中更多地讨论该级别的测试,但是福勒先生保持良好回归测试的观点是安全重构的关键。 111 | 112 | 如果您不仅需要重构,还需要重写和更改外部行为或接口,那么故意破坏构建可能会有所帮助。也就是说,此代码的旧客户端应无法编译。然后,您可以快速找到旧客户端并进行必要的更改以使它们更新。 113 | 114 | 因此,下次您看到一段不尽如人意的代码时,请修复该代码及其所依赖的所有内容。处理痛苦:如果现在很痛,但以后还会再痛苦,那么您最好还是克服它。记住话题 3 [_软件熵_](../Chapter1/软件熵.md) 的教训:不要在破窗户的屋子里生活。 115 | 116 | ## 相关内容包括 117 | - 话题 9 [_重复的恶魔_](../Chapter2/重复的恶魔.md) 118 | - 话题 27 [_别开过头了_](../Chapter4/别开过头了.md) 119 | - 话题 3 [_软件熵_](../Chapter1/软件熵.md) 120 | - 话题 12 [_示踪子弹_](../Chapter2/示踪子弹.md) 121 | - 话题 44 [_命名_](./命名.md) 122 | - 话题 47 [_敏捷的本质_](../Chapter8/敏捷的本质.md) 123 | -------------------------------------------------------------------------------- /Chapter8/敏捷的本质.md: -------------------------------------------------------------------------------- 1 | # 敏捷的本质 2 | 3 | 4 | > _你一直在用那个词,我认为它并不是你想的那个意思。_ 5 | > 6 | > _-- 伊尼戈·蒙托亚,《公主新娘》_ 7 | 8 | 敏捷 是一个形容词:这是您做事的方式。 您可以成为一名敏捷开发人员。 您可以加入采用敏捷实践的团队,也可以敏捷地响应变化和挫折。 敏捷是您的风格,而不是您。 9 | 10 | --- 11 | ## 提示 83 敏捷不是名词。 敏捷是您的工作方式 12 | --- 13 | 14 | 在我们撰写本文时,敏捷软件开发宣言诞生已有近20年,我们看到许多开发人员成功地运用了其价值。 我们看到许多出色的团队正在寻找方法来采用这些价值观,并用它们来指导自己的工作,以及他们如何改变自己的工作。 15 | 16 | 但我们也看到了敏捷性的另一面。我们看到团队和公司急于寻求现成的解决方案。在一个框框里面敏捷。我们看到很多顾问和公司都很乐意向他们推销他们想要的东西。我们看到公司采用了更多的管理层次,更多的正式报告,更多的专业开发人员,以及更多的花哨的工作头衔,而这些头衔只是意味着 "拿着剪贴板和秒表的人。" 17 | 18 | 我们感觉到很多人已经忘记了敏捷的真正意义,我们希望看到乡亲们能够返璞归真。 19 | 20 | 请记住宣言中的价值观。 21 | 22 | 我们正在通过做软件开发的过程中发现更好的方法,并帮助别人去做软件开发。通过这项工作,我们认识到了价值。 23 | 24 | - 个人和相互作用,而不是过程和工具 25 | 26 | - 工作软件超过综合文件 27 | 28 | - 客户合作超过合同谈判 29 | 30 | - 应对变化而不是按计划行事 31 | 32 | 也就是说,虽然右边的东西有价值,但我们更看重左边的东西。 33 | 34 | 任何向你推销右边的东西比左边的东西更重要,显然是不重视我们和其他宣言作者所做的事情。 35 | 36 | 而任何向你推销一整个解决方案的人肯定都没有读过引言。这些价值观是由不断发掘出更好的软件生产方式的行为所激发和启发的。这不是一个静态的文档。它是对一个生成过程的建议。 37 | 38 | ## 永远不可能有一个敏捷的过程 39 | 事实上,每当有人说 "这样做,你就会敏捷 "时,从定义上看他们就错了。 40 | 41 | 因为无论是在物理世界还是在软件开发中,敏捷都是对变化做出反应,对你出发后遇到的未知数做出反应。一只奔跑的羚羊不会走在一条直线上。一个体操运动员在应对环境的变化和脚下的小错误时,一秒钟要做数百次修正。 42 | 43 | 团队和个人开发者也是如此。当你开发软件的时候,没有一个单一的计划可以遵循。四个价值观中有三个告诉你,它们都是关于收集和响应反馈。 44 | 45 | 这些价值观并没有告诉你应该做什么。它们告诉你,当你自己决定要做什么的时候,要注意什么。 46 | 47 | 这些决定总是与背景有关:它们取决于你是谁、你的团队的性质、你的应用、你的工具、你的公司、你的客户、外部世界;大量的因素,有些是重要的,有些是微不足道的。任何固定的、静态的计划都无法在这种不确定性中生存。 48 | 49 | ## 那么,我们该怎么做? 50 | 没有人能告诉你该怎么做。 但是我们认为我们可以告诉您一些您做事的精神。 一切都归结为您如何处理不确定性。 宣言建议您通过收集反馈并采取行动来做到这一点。 因此,这是我们以敏捷方式工作的方法: 51 | 52 | 1.找到自己的位置。 53 | 54 | 2.朝着自己想去的地方迈出最小的有意义的一步。 55 | 56 | 3.评估最终结果,并修复所有损坏的问题。 57 | 58 | 重复这些步骤,直到完成。 并在您执行的所有操作中递归使用它们。 59 | 60 | 有时,当您收集反馈时,即使是最琐碎的决定也变得很重要。 61 | 62 | 现在,我的代码需要获得帐户所有者。 63 | 64 | ```js 65 | let user = accountOOwner(accountID) 66 | ``` 67 | 68 | 额。。。 user 是一个无用的命名,我把它改成 owner。 69 | 70 | ```js 71 | let owner = accountOOwner(accountID) 72 | ``` 73 | 74 | 但是现在感觉有点多余。我实际上在这里想做什么?用户故事说我要给这个人发送电子邮件,所以我需要找到他们的电子邮件地址。也许我根本不需要整个帐户所有者。 75 | 76 | ```js 77 | let email = emailOfAccountOOwner(accountID) 78 | ``` 79 | 80 | 通过在非常低的级别(变量的命名)应用反馈循环,我们实际上改善了整个系统的设计,减少了此代码与处理帐户的代码之间的耦合。 81 | 82 | 反馈循环也适用于项目的最高级别。当我们开始按照客户的要求工作,迈出第一步之后,并意识到我们没有必要要做什么,最好的解决方案甚至不涉及软件,这都是我们最成功的工作。 83 | 84 | 此循环适用于单个项目的范围之外。团队应该将其应用于审查他们的过程以及它的工作状况。不持续尝试其流程的团队不是敏捷团队。 85 | 86 | ## 这推动了设计 87 | 在话题 8 [_好设计的本质_](../Chapter2/好设计的本质.md) 中,我们断言设计的度量标准是变更设计结果的难易程度:良好的设计会产生比不良设计更容易改变的东西。 88 | 89 | 关于敏捷性的讨论解释了为什么会这样。 90 | 91 | 您进行了更改,发现自己不喜欢它。 清单中的第 3 步表示,我们必须能够解决所遇到的问题。 为了使我们的反馈环路高效,此修复必须尽可能轻松。 如果不是这样,我们很想耸耸肩,不做任何改动。 我们将在话题 3 [_软件熵_](../Chapter1/软件熵.md) 中讨论这种影响。 为了使整个敏捷事物正常工作,我们需要实践良好的设计,因为良好的设计使事物易于更改。 如果更改容易,我们可以毫不犹豫地在每个级别进行调整。 92 | 93 | 那就是敏捷性。 94 | 95 | ## 相关内容包括 96 | - 话题 27 [_别开过头了_](../Chapter4/别开过头了.md) 97 | - 话题 40 [_重构_](../Chapter/重构7.md) 98 | - 话题 49 [_椰子不要切碎_](../Chapter9/椰子不要切碎.md) 99 | 100 | ## 挑战 101 | 这个简单的反馈回路并不只是针对软件。想一想你最近做的其他决定。如果事情没有按照你的方向发展,你能不能通过思考如何撤销这些决定来改进它们中的任何一个?你能想一想,通过收集反馈意见并采取相应的行动来改进你所做的事情吗? 102 | -------------------------------------------------------------------------------- /Chapter8/解决不可能的难题.md: -------------------------------------------------------------------------------- 1 | # 解决不可能的难题 2 | 3 | 4 | > _Phrygia 的国王 Gordius 曾经打过一个没人能解开的结。 有人说,能解开这个结的人将统治整个亚洲。 随后亚历山大大帝(Alexander the Great)出现了,他用刀将这个结砍成了碎片。 只是对需求有一些不同的解释,仅此而已...。 他最终确实也统治了亚洲大部分地区。_ 5 | 6 | 时不时地,当一个非常棘手的难题出现时,你会发现自己陷入了一个项目中间:一些你根本无法处理的工程,或者一些比你想象的要难写得多的代码。也许看起来不可能。但这真的像看起来那么难吗? 7 | 8 | 想想现实世界中的谜题吧,那些看起来像是圣诞礼物或是在车库里出售的木头、锻铁或塑料的小玩意。你要做的就是把戒指取下来,或者把 T 形的东西装进盒子里,或者别的什么。 9 | 10 | 于是,你拉开了戒指,或者尝试着把 T 的东西放进盒子里,很快就发现,显而易见的解法就是不行。这个问题是不能用这种方式解决的。但即使它是显而易见的,但这并不能阻止人们尝试同样的事情--一遍又一遍地认为一定有办法。 11 | 12 | 当然,没有办法。解决的办法在其他地方。解决这个难题的秘诀是找出真正的(而不是想象中的)制约因素,并在其中找到解决方案。有些约束是绝对的;有些约束只是先入为主的概念。绝对的约束必须得到尊重,无论它们看起来多么令人厌恶或愚蠢。 13 | 14 | 另一方面,正如亚历山大所证明的,有些表面上的约束可能根本不是真正的约束。许多软件问题也可能是一样偷偷摸摸的。 15 | 16 | ## 自由的程度 17 | 流行的流行语 "跳出框框思考" 鼓励我们认识到那些可能不适用的限制,并忽略它们。但这句话并不完全准确。如果说 "框框"是约束和条件的边界,那么技巧就是找到框框,可能比你想的要大得多。 18 | 19 | 解谜的关键是,既要认识到施加在你身上的约束,又要认识到你有哪些自由度,因为在这些自由度中你会找到你的解法。这就是为什么有些谜题如此有效的原因;你可能会过于轻易地否定潜在的解法。 20 | 21 | 例如,你能不能把下面的谜题中的所有点连起来,然后只用三条直线就能回到起点,而不需要把笔从纸上抬起来,也不需要重新回到原点。 22 | 23 | 。 。 24 | 。 。 25 | 26 | 你必须挑战任何先入为主的观念,并评估它们是否是真实的、硬性的约束。 27 | 28 | 问题不在于你的思维是否在框内或框外。问题在于找到框--识别出真正的制约因素。 29 | 30 | --- 31 | ## 提示 82 不要在框内思考--找到框内的约束 32 | --- 33 | 34 | 当面对一个棘手的问题时,列举出你面前所有可能的途径。无论听起来多么不实用或愚蠢,都不要否定任何东西。现在去翻看一下清单,解释为什么不能走某条路。你确定吗?你能证明它吗? 35 | 36 | 考虑一下特洛伊木马--一个解决棘手问题的新奇方案。你如何让军队进入一个有城墙的城市而不被发现?你可以打赌,"从正门进入" 最初被认为是自杀。 37 | 38 | 对自己的制约因素进行分类,分清主次。木工们在开始做一个项目时,先把最长的木头切掉,然后再把剩下的小块木头切掉。同理,我们要先找出限制性最强的约束,把剩下的约束都装进约束中。 39 | 40 | 对了,书的末尾有一个四柱之谜的解法。 41 | 42 | ## 走出自己的路! 43 | 有时候,你会发现自己在解决一个问题时,似乎比你想象中的要难得多。也许你会觉得自己走错了路,觉得一定有比这更容易的方法! 也许你现在在计划表上跑得很晚,甚至绝望于永远无法让系统发挥作用,因为这个特定的问题 "不可能"。 44 | 45 | 这是一个理想的时机,可以暂时做一些别的事情。去做一些不同的事情。去遛狗。睡一觉就可以了。 46 | 47 | 你有意识的大脑已经意识到了问题,但你有意识的大脑真的很笨(无意冒犯)。所以,是时候给你的真正的大脑,那个潜伏在你的意识下面的神奇的联想神经网一些空间了。你会惊讶地发现,当你刻意分散自己的注意力时,答案往往会在你的脑海中出现。 48 | 49 | 如果这听起来对你来说太神秘了,其实不然。《今日心理学》报道说。 50 | 51 | > _说白了,分心的人比有意识的人在解决一个复杂问题的任务上做得更好。_ 52 | 53 | 如果你暂时还不愿意放下问题,那么接下来最好的办法可能就是找人解释。通常情况下,简单地谈一谈就能让你开窍。 54 | 55 | 让他们问你一些问题,例如:"你为什么要解决这个问题? 56 | 57 | - 你为什么要解决这个问题? 58 | 59 | - 解决了它有什么好处? 60 | 61 | - 你遇到的问题是否与边缘案例有关?你能消除这些问题吗? 62 | 63 | - 有没有一个更简单、相关的问题你可以解决? 64 | 65 | 这是另一个实践中的 "橡皮鸭 "的例子。 66 | 67 | ## 财富偏爱有准备的人 68 | 据报道,路易-巴斯德曾说: 69 | 70 | > Dans les champs de l'observation le hasard ne favorise que les esprits prits préparés. 71 | > 72 | > _(当谈到观察时,命运会眷顾有准备的人。)_ 73 | 74 | 这对于解决问题也是如此。为了拥有那些 "尤里卡" 的时刻,你的非意识的大脑需要有大量的原始材料;之前的经验可以帮助你找到答案。 75 | 76 | 一个很好的方法就是在你的日常工作中,给你的大脑提供反馈,让它知道什么是有效的,什么是无效的。而我们在这里描述的一个很好的方法就是记一本工程日记(话题 22,[_工程日记_](../Chapter3/工程日记.md) )。 77 | 78 | 永远记住《银河系搭车指南》封面上的建议:不要惊慌失措。 79 | 80 | ## 相关内容包括 81 | - 话题 37 [_聆听你的蜥蜴脑_](../Chapter7/聆听你的蜥蜴脑.md) 82 | - 话题 5 [_足够好的软件_](../Chapter1/足够好的软件.md) 83 | - 话题 45 [_需求坑_](./需求坑.md) 84 | - Andy 写了一整本关于这类事情的书[《Pragmatic Thinking and Learning: Refactor Your Wetware》](https://pragprog.com/book/ahptl/pragmatic-thinking-and-learning) 85 | 86 | ## 挑战 87 | - 仔细看看您今天遇到的任何难题。 你能剪掉 Gordius 打的结吗? 您必须这样做吗? 您必须要做吗? 88 | - 登录到当前项目时,您是否受到了一系列限制? 它们是否仍然适用,并且对它们的解释仍然有效吗? 89 | -------------------------------------------------------------------------------- /Chapter8/需求坑.md: -------------------------------------------------------------------------------- 1 | # 需求坑 2 | 3 | 4 | > _真正的完美不在于无须加入任何其他元素,而在于不再需要削除任何细节。_ 5 | > 6 | > _-- 安东尼·德·圣-埃克苏佩里 《风沙星辰》 1939_ 7 | 8 | 很多书和教程都把需求收集 作为项目的早期阶段。"聚集" 这个词似乎意味着一群快乐的分析家,在轻轻地演奏着《牧歌交响曲》背景乐时,寻找着散落在周围地面上的智慧碎片。"收集" 意味着这些要求已经存在,你只需要找到它们,把它们放进你的篮子里,就可以愉快地上路了。 9 | 10 | 但事实并非如此。需求很少存在于表面上。通常情况下,它们被深埋在假设、误解和政治性的层层深渊之下。更糟糕的是,它们往往根本就不存在。 11 | 12 | ## 需求神话 13 | 在软件的早期,计算机的价值(按每小时的摊销成本计算)比使用计算机的人更有价值。我们通过尝试在第一时间就把事情做对,从而节省了资金。这个过程的一部分就是要明确说明我们要让机器做什么。我们会先得到一个需求规范,然后将其转化为设计文档,再转化为流程图和伪代码,最后转化为代码。但在将其输入计算机之前,我们要花时间进行桌面检查。 14 | 15 | 这要花很多钱。这种成本意味着人们只有在知道自己想要什么的时候才会尝试自动化。而且由于早期的机器相当有限,它们所解决的问题的范围受到了限制:在你开始工作之前,实际上是有可能了解整个问题的。 16 | 17 | 但这不是真实的世界。真实的世界是混乱的、冲突的、未知的。在那个世界里,任何事物的精确规格都是罕见的,甚至是完全不可能的。 18 | 19 | --- 20 | ## 提示 75 没有人确切的知道他们到底想要什么 21 | --- 22 | 23 | 这就是我们程序员的作用所在。我们的工作就是帮助人们了解他们想要什么。事实上,这可能是我们最有价值的属性。而这一点是值得重复的。 24 | 25 | --- 26 | ## 提示 76 程序员帮助人们了解他们想要的东西 27 | --- 28 | 29 | ## 编程即治疗 30 | 让我们把找我们写软件的人称为我们的客户。 31 | 32 | 典型的客户是带着需求来找我们的。这个需求可能是战略性的,但也可能是战术性的问题:对当前问题的回应。这个需求可能是对现有系统的改变,也可能是对新系统的要求。这种需求有时会用商业术语来表达,有时也会用技术术语来表达。 33 | 34 | 新的开发人员经常犯的错误就是拿着这个需求声明去实现这个需求的解决方案。 35 | 36 | 根据我们的经验,这个最初的需求陈述并不是绝对的需求。客户可能没有意识到这一点,但这其实是一种探索的邀请。 37 | 38 | 让我们举一个简单的例子。 39 | 40 | 你为一家纸质和电子书的出版商工作。你被赋予了一个新的要求。 41 | 42 | 所有 50 美元以上的订单都应该免运费。 43 | 44 | 停一停,想象一下自己在那个位置上的情景。你首先想到的是什么? 45 | 46 | 你很有可能会有这样的疑问。 47 | 48 | - 50 美元是否包括税费? 49 | - 这 50 美元是否包括当前的运费? 50 | - 这 50 美元必须是纸质书,还是也可以包括电子书? 51 | - 提供什么样的运输方式?优先邮递?陆运? 52 | - 国际订单如何处理? 53 | - 50 美元的限额今后多久会有变化? 54 | 55 | 这就是我们所做的事情。当给到一些看似简单的东西时,我们会通过寻找边缘案例并询问边缘案例来烦他们。 56 | 57 | 客户很可能已经想到了其中的一些情况,只是假设实现的时候会这样做。问这个问题只是把这些信息冲掉了。 58 | 59 | 但其他的问题很可能是客户之前没有考虑过的事情。这就是事情变得有趣的地方,也是一个好的开发者要学会外交的地方。 60 | 61 | 你:我们想知道50美元的总费用是多少?这是否包括我们通常会收取的运费? 62 | 63 | 客户:当然包括了。这是他们会付给我们的总价。 64 | 65 | 你:这对我们的客户来说很好,也很容易理解。我能看出其中的吸引力。但我可以看到一些不那么肆无忌惮的客户想玩这个系统。 66 | 67 | 客户:怎么会这样? 68 | 69 | 你:好吧,假设他们买一本书 25 美元,然后选择隔夜运输,最贵的选项。那很可能是 30 美元左右,整个订单就会变成 55 美元。然后我们就可以免运费,他们只需要花 25 美元买一本 25 美元的书,就可以得到隔夜运费。 70 | 71 | (这时,有经验的开发者就停了下来。交付事实,让客户做决定,) 72 | 73 | 客户: Ouch. 这肯定不是我的本意,我们会在这些订单上赔钱。有什么选择? 74 | 75 | 这就开始了一个探索。你的角色是解释客户说的话,并将其含义反馈给他们。这既是一个智力的过程,也是一个创造性的过程:你在用你的脚去思考,你所贡献的解决方案很可能比你或客户单独提出的方案要好。 76 | 77 | ## 需求是一个过程 78 | 在上一个例子中,开发者把需求和结果反馈给客户。这就开始了探索。在这个探索过程中,你很可能会在客户玩弄不同的解决方案时,得到更多的反馈。这就是所有需求收集的现实。 79 | 80 | --- 81 | ## 提示 77 需求是在反馈循环中学习的 82 | --- 83 | 84 | 你的工作是帮助客户了解他们所提出的要求的后果。你要做的是通过产生反馈,并让他们利用这些反馈来完善自己的想法。 85 | 86 | 在前面的例子中,反馈很容易用语言表达出来。但有时情况并非如此。而且有时候,说实话,你对这个领域的了解还不够具体。 87 | 88 | 在这种情况下,务实的程序员就会依靠 "这就是你的意思吗" 的反馈方式。我们会制作出模型和原型,然后让客户来玩。最理想的情况是,我们制作的东西足够灵活,在与客户讨论的过程中,我们可以改变它们,让我们用 "那不是我的意思" 来回应 "那么更像这样吗?"。 89 | 90 | 有时候,这些模拟图可以在一个小时左右的时间里拼凑出来。很明显,它们只是为了表达一个想法而拼凑出来的。 91 | 92 | 但事实上,我们所做的所有工作实际上都是某种形式的模拟。即使是在项目结束时,我们仍然在解释客户想要的东西。事实上,到了那个时候,我们可能会有更多的客户:QA人员、运营、营销,甚至可能还有测试组的客户。 93 | 94 | 所以,务实的程序员把所有的项目都看成是需求收集工作。这就是为什么我们更喜欢短期的迭代;那些以客户直接反馈结束的项目。这可以让我们的工作步入正轨,并确保如果我们走错了方向,就能最大限度地减少时间损失。 95 | 96 | ## 站在客户的立场上行走 97 | 有一个简单的技巧,可以让你进入客户的内心,但并不经常使用:成为客户。你是否在为服务台开发一个系统?花几天时间和一个有经验的支持人员一起监视电话。你是在自动化人工库存控制系统吗?在仓库工作一个星期。 98 | 99 | 除了让你深入了解系统的真正用途之外,你会惊讶于 "我可以在你工作的时候坐一个星期吗?"的要求有助于建立信任,并与客户建立起沟通的基础。只要记住不要妨碍到你的工作就可以了! 100 | 101 | --- 102 | ## 提示 78 与用户合作,像用户一样思考问题 103 | --- 104 | 105 | 收集反馈也是开始与客户群建立友好关系的时候,了解他们对你所建立的系统的期望和希望。更多信息,请参见话题51,[_让用户满意_](../Chapter9/让用户满意.md)。 106 | 107 | ## 需求与政策 108 | 让我们想象一下,在讨论人力资源系统的时候,客户说:"只有员工的主管和人事部门可以查看该员工的记录。" 这句话真的是要求吗?也许在今天看来是这样的,但它将商业政策嵌入了绝对的声明中。 109 | 110 | 商业政策?是要求?这是一个比较细微的区别,但对于开发者来说,这将会产生深远的影响。如果需求陈述为 "只有主管和人员可以查看员工记录",那么开发者可能最终会在每次应用访问这些数据时,都要编码一个显式测试。但是,如果声明是 "只有授权用户才能访问员工记录",那么开发者很可能会设计并实现某种访问控制系统。当策略发生变化(而且会发生)时,只需要更新该系统的元数据。事实上,用这种方式收集需求,自然就会导致你的系统有很好的事实支持元数据。 111 | 112 | 事实上,这里有一个一般的规则: 113 | 114 | --- 115 | ## 提示 79 政策就是元数据 116 | --- 117 | 118 | 落实通例,以政策信息为例,系统需要支持的事情类型。 119 | 120 | ## 需求与现实 121 | 在1999年1月的《连线》杂志上的一篇文章中,制作人和音乐家 Brian Eno 描述了一种不可思议的技术--终极调音板。它可以做任何可以做的声音。然而,它非但没有让音乐人制作出更好的音乐,或以更快的速度或更低的成本制作出更多的唱片,反而妨碍了创作过程。 122 | 123 | 要知道为什么,你必须看看录音工程师是如何工作的。他们凭着直觉来平衡声音。多年来,他们在耳朵和指尖之间形成了一个与生俱来的反馈回路--滑动推子、旋转旋钮等等。然而,新调音台的界面并没有利用这些能力。相反,它强迫用户在键盘上打字或点击鼠标。它所提供的功能非常全面,但却以不熟悉和陌生的方式进行了包装。工程师们需要的功能有时被隐藏在晦涩难懂的名字后面,或者是通过非直观的基本设施组合来实现。 124 | 125 | 这个例子也说明了我们的信念:成功的工具要适应使用它们的人。成功的需求收集就考虑到了这一点。而这就是为什么早期的反馈,用原型或示踪弹,会让你的客户说:"是的,它做了我想要的,但不是我想要的方式。" 126 | 127 | ### 文档化需求 128 | 我们相信,最好的需求文档,也许是唯一的需求文档,就是工作代码。 129 | 130 | 但这并不意味着你可以不记录你对客户需求的理解,就可以离开。这只是意味着这些文档不是一个可交付的文档:它们不是你交给客户签字的东西。相反,它们只是一个帮助指导实施过程的路标。 131 | 132 | ### 需求文档不是为客户准备的 133 | 在过去,Andy 和 Dave 都曾参与过一些项目,这些项目的要求非常详细。这些实质性的文件是在客户最初两分钟的需求解释的基础上进行了扩展,制作出了满满是图表和表格的厚厚的杰作。在实施过程中几乎没有任何含糊不清的地方。如果有了足够强大的工具,文档实际上可以成为最终的程序。 134 | 135 | 创建这些文档是一个错误,原因有两个。首先,正如我们已经讨论过的那样,客户在前期并不真正知道他们想要什么。所以,当我们把他们说的东西,扩展成几乎是一个法律文件的时候,我们就是在流沙上建造了一个无比复杂的城堡。 136 | 137 | 你可能会说:"但是我们把文件拿给客户,然后他们就会签字。我们会得到反馈。" 而这就引出了这些需求说明书的第二个问题:客户从来不看这些需求说明书。 138 | 139 | 客户使用程序员的原因是,虽然客户的动机是为了解决一个高层次的、有点模糊的问题,但程序员却对所有的细节和细微之处感兴趣。而需求文档是为开发人员写的,其中包含的信息和细微之处有时让客户难以理解,经常让客户感到厌烦。 140 | 141 | 提交一份 200 页的需求文档,客户很可能会仔细阅读,以决定它是否足够重要,他们可能会阅读前几段(这就是为什么前两段总是以管理摘要为标题的原因),他们可能会翻阅剩下的内容,有时会在有一个整齐的图表时停下来。 142 | 143 | 这样做并不是让客户失望。但给他们一个大的技术文档,就像给一般的开发者一份荷马史诗的《伊利亚特》,要求他们从里面编写视频游戏的代码一样。 144 | 145 | ### 需求文件是为了做计划 146 | 所以我们不相信一刀切的、厚重到足以让人眩晕的需求文档。但是我们知道需求必须要写下来,因为团队中的开发人员需要知道他们要做什么。 147 | 148 | 这需要采取什么样的形式呢?我们偏向于那些可以装在真实(或虚拟)索引卡上的东西。这些简短的描述通常被称为用户故事。它们从该功能的用户的角度描述了应用中的一小部分应该在什么时候做的事情。 149 | 150 | 当以这种方式写出来时,这些需求可以放在一个板子上,并且可以移动,以显示状态和优先级。 151 | 152 | 你可能会认为,一张索引卡无法容纳实现应用程序的某个组件所需的信息。你的想法是对的。而这也是重点的一部分。通过保持这个需求声明的简短,你鼓励开发人员提出澄清问题。你在每一段代码的创建之前和创建过程中,都会加强客户和编码人员之间的反馈过程。 153 | 154 | ## 过度规范化 155 | 制作需求文档的另一大危险是过于具体。好的需求是抽象的。就需求而言,能准确反映业务需求的最简单的陈述是最好的。这并不意味着你可以含糊其辞--你必须把底层的语义不变性作为需求,并把具体的或当前的工作实践作为策略记录下来。 156 | 157 | 需求不是架构,也不是设计。也不是用户界面。需求就是需要。 158 | 159 | --- 160 | ## 提示 80 抽象比细节更持久 161 | --- 162 | 163 | ## 只需再来一个威化薄荷..... 164 | 许多项目的失败都归咎于范围的增加,也就是所谓的功能臃肿、蠕动壮举主义或需求蠕动。这是由话题 4 [_石汤和煮青蛙_](../Chapter1/石头汤和煮青蛙.md) 中的 "煮青蛙综合症 "的一个方面。我们可以做什么来防止需求的爬行呢? 165 | 166 | 答案(又是)是反馈。如果你和客户在迭代中不断的反馈,那么客户会亲身体验到 "只需多一个功能" 的影响。他们会看到另一张故事卡在黑板上出现,他们会帮助选择另一张卡进入下一个迭代,以腾出空间。反馈是双向的。 167 | 168 | ## 维持一个词汇表 169 | 一旦你开始讨论需求,用户和领域专家就会使用某些对他们有特定意义的术语。例如,他们可能会区分 "client" 和 "customer"。这时,在系统中随意使用这两个词都是不合适的。 170 | 171 | 创建和维护一个项目词汇表,定义项目中使用的所有特定术语和词汇。项目的所有参与者,从终端用户到支持人员,都应该使用该词汇表,以确保一致性。这意味着,词汇表需要广泛地使用--这是对在线文档的一个很好的论证(稍后会有更多的说明)。 172 | 173 | --- 174 | ## 提示 81 使用项目词汇表 175 | --- 176 | 177 | 在一个项目中,如果用户和开发者用不同的名字来指代同一个东西,或者更糟糕的是,用同一个名字来指代不同的东西,这是很难成功的。 178 | 179 | ## 相关内容包括 180 | - 话题 7 [_沟通_](../Chapter1/沟通.md) 181 | - 话题 23 [_契约设计_](../Chapter4/契约设计.md) 182 | - 话题 5 [_足够好的软件_](../Chapter1/足够好的软件.md) 183 | - 话题 46 [_解决不可能的难题_](./解决不可能的难题.md) 184 | - 话题 42 [_基于属性的测试_](../Chapter7/基于属性的测试.md) 185 | - 话题 13 [_原型和便签_](../Chapter2/原型和便签.md) 186 | - 话题 11 [_可逆性_](../Chapter2/可逆性.md) 187 | - 话题 43 [_在外部保持安全_](../Chapter7/在外部保持安全.md) 188 | - 话题 44 [_命名_](../Chapter7/命名.md) 189 | - 话题 51 [_让用户满意_](../Chapter9/让用户满意.md) 190 | 191 | ## 练习 192 | ### 练习 32 (尽可能回答) 193 | 以下哪项可能是真正的要求?重述那些不是,使其更有用(如果可能的话)。 194 | 1. 响应时间必须小于 500ms。 195 | 2. 模态窗口将有一个灰色背景。 196 | 3. 应用程序将被组织成若干个前端进程和一个后端服务器。 197 | 4. 如果用户在数字字段中输入非数字字符,系统将闪烁字段背景,不接受。 198 | 5. 这个嵌入式应用的代码和数据必须在 32Mb 以内。 199 | 200 | ## 挑战 201 | - 你能用上你所编写的软件吗?能否对需求有一个很好的感觉,而不需要 "能够自己使用软件"? 202 | - 选一个你目前需要解决的非计算机相关的问题。生成一个非计算机解决方案的需求。 203 | -------------------------------------------------------------------------------- /Chapter8/项目之前.md: -------------------------------------------------------------------------------- 1 | # 项目之前 2 | 3 | 4 | 在项目开始的时候,你和团队需要学习需求。仅仅是被告知要做什么或听用户的意见是不够的:阅读话题 45 [_需求坑_](./需求坑.md),学习如何避免常见的陷阱和坑。 5 | 6 | 传统的智慧和约束管理是 话题 46,[_解决不可能的难题_](./解决不可能的难题.md)。无论你是在执行需求、分析、编码还是测试,困难的问题都会出现。大多数时候,这些问题并不像最初看起来那么难。 7 | 8 | 尽管敏捷宣言以 "个人和互动高于流程和工具 "为开端,但几乎所有的 "敏捷 "项目都是以讽刺性的讨论哪种流程和哪种工具开始的。但是,无论它考虑得多么周密,也无论它包括哪些 "最佳实践",没有任何方法可以取代思考。你不需要任何特定的流程或工具,你需要的是 话题 47,[_敏捷的本质_](./敏捷的本质.md)。 9 | 10 | 在项目开始之前就弄清楚了这些关键问题,你就可以更好地避免 "分析瘫痪",真正开始并完成你的成功项目 11 | -------------------------------------------------------------------------------- /Chapter9/傲慢与偏见.md: -------------------------------------------------------------------------------- 1 | # 傲慢与偏见 2 | 3 | > _你已经让我们高兴了很久了。_ 4 | > 5 | > _-- 简-奥斯汀,《傲慢与偏见》_ 6 | 7 | 务实的程序员不会推卸责任。相反,我们乐于接受挑战,乐于让我们的专业知识广为人知。如果我们对一个设计或一段代码负责,我们就会做一份我们引以为豪的工作。 8 | 9 | --- 10 | ## 提示 97 签署你的工作 11 | --- 12 | 13 | 早期的工匠们以能在自己的作品上签名为荣。你也应该如此。 14 | 15 | 然而,项目团队还是由人组成的,这个规则可能会带来麻烦。在一些项目上,代码所有权 的想法会造成合作问题。人们可能会变得有地域性,或者不愿意在共同的基础元素上工作。这个项目可能会像一堆与世隔绝的小领地一样结束。你会对自己的代码产生偏见,对同事产生偏见。 16 | 17 | 这不是我们想要的。你不应该嫉妒地捍卫你的代码,抵御干扰者;同样的道理,你应该尊重别人的代码。黄金法则("己所不欲勿施于人")和开发人员之间相互尊重的基础是使这一提示发挥作用的关键。 18 | 19 | 匿名性,尤其是在大型项目中,可能会滋生出马虎、错误、懒惰和糟糕的代码。这就很容易让人把自己看成是车轮上的一个齿轮,在无休止的状态报告中产生蹩脚的借口,而不是好的代码。 20 | 21 | 虽然代码必须是属于自己的,但它不一定是由个人拥有的。事实上,Kent Beck的极限编程推荐代码的共有权(但这也需要额外的实践,比如说对编程,以防范匿名的危险)。 22 | 23 | 我们希望看到的是主人翁的自豪感。"这是我写的,而且我支持我的工作。" 你的签名应该被认为是质量的一个指标。人们应该在一段代码上看到你的名字,并期望它是扎实的、写得好的、经过测试的、有文档的。一个真正 24 | 专业的工作。由一个真正的专业人员编写的。 25 | 26 | 一个务实的程序员。 27 | 28 | 谢谢你。 29 | -------------------------------------------------------------------------------- /Chapter9/务实的团队.md: -------------------------------------------------------------------------------- 1 | # 务实的团队 2 | 3 | > _在 L 组,Stoffel 管理着六个一流的程序员,这是一个管理上的挑战,与放养猫差不多。_ 4 | > 5 | > _-- 《华盛顿邮报》杂志,1985 年 6 月 9 日_ 6 | 7 | 即使在 1985 年,关于放养猫的笑话也在变老。到本世纪初的第一版时,它已经是非常古老的了。但它依然存在,因为它有一个真理之环。程序员有点像猫:聪明,意志坚强,固执己见,独立,经常受到网络的崇拜。 8 | 9 | 到目前为止,在这本书中,我们已经研究了帮助个人成为更好的程序员的实用技术。这些方法是否也适用于团队,即使是意志坚强、独立的团队?答案是响亮的“是的!“作为一个实用主义者是有好处的,但是如果一个人在一个实用主义的团队中工作,这些好处就会成倍增加。 10 | 11 | 在我们看来,团队是一个小型的、基本上稳定的实体。50 人不是一个团队,而是一个部落。团队成员经常被拉到其他任务上,没有人知道彼此也不是一个团队,他们只是在雨中临时共用一个公交车站的陌生人。 12 | 13 | 一个务实的团队规模很小,成员不到 10-12 人左右。成员来去不多。每个人都很了解彼此,相互信任,相互依赖。 14 | 15 | --- 16 | ## 提示 84 维持小型稳定团队 17 | --- 18 | 19 | 在本节中,我们将简要介绍如何将实用技术应用于整个团队。这些笔记只是一个开始。一旦你有一组务实的开发人员在一个有利的环境中工作,他们将迅速开发和完善自己的团队动态,为他们工作。 20 | 21 | 让我们从团队的角度重铸前面的部分。 22 | 23 | ## 没有破窗 24 | 质量是团队的问题。 放在最不关心团队中的最勤奋的开发人员会发现,难以保持解决小问题所需的热情。 如果团队积极劝阻开发人员将时间花在这些修补程序上,则该问题将进一步加剧。 25 | 26 | 整个团队都不应容忍破碎的窗户-那些没有人修复的微小缺陷。 团队必须对产品的质量负责,原因我们在话题 3 [_软件熵_](../Chapter1/软件熵.md) 中已经描述过,那些已然理解了破窗理论的开发人员,他们会鼓励尚未发现它的开发人员。 27 | 28 | 一些团队方法学有一个“质量官”,即由该团队委派负责交付质量的人员。 这显然是荒谬的:质量只能来自所有团队成员的个人贡献。 质量是内置的,而不是固定的。 29 | 30 | ## 煮青蛙 31 | 还记得话题 4 [_石头汤和煮青蛙_](../Chapter/石头汤和煮青蛙.md) 中的 "水锅里的青蛙 "吗?它没有注意到环境的逐渐变化,最终被煮熟了。同样的情况也会发生在不警惕的个人身上。在项目开发的热潮中你很难注意到自己身边整体环境的变化。 32 | 33 | 对于整个团队来说,更容易被煮熟。人们会认为是别人在处理一个问题,或者团队负责人一定是同意了你的用户要求的更改。即使是用心良苦的团队,也会对项目中的重大变化视而不见。 34 | 35 | 与此抗争。鼓励每个人积极地监控环境中的变化。对范围的增加、时间尺度的减少、额外的功能、新的环境--任何最初理解中没有的东西都要保持清醒的意识。保持对新需求的衡量标准。团队不需要随意拒绝变更--你只需要意识到它们正在发生。否则,你就会身处热水之中。 36 | 37 | ## 安排您的知识组合 38 | 在话题 6 [_你的知识组合_](../Chapter1/你的知识组合.md) 中,我们研究了您应该如何在自己的时间投资于个人知识组合。想要成功的团队也需要考虑他们的知识和技能投资。 “如果您的团队认真对待改进和创新,则需要安排时间。尝试“在有空闲时间的时候”完成工作,这意味着它们永远不会发生。无论您使用哪种待办事项列表,任务列表或流程,都不要仅将其用于功能开发。该团队不仅仅致力于新功能。一些可能的示例包括: 39 | 40 | _旧系统维护_ 41 | 42 | 尽管我们喜欢在崭新的系统上进行工作,但仍有可能需要在旧系统上进行维护工作。我们遇到了尝试在角落里进行这项工作的团队。如果团队负责执行这些任务,那么请真正执行它们。 43 | 44 | _流程反思与完善_ 45 | 46 | 仅当您花时间环顾四周,找出有效的方法并做出更改后才能进行持续改进(请参阅话题 47,敏捷的本质 )。太多的团队忙于救水,以至于他们没有时间修复泄漏。安排它。修理它。 47 | 48 | _新技术实验_ 49 | 50 | 不要仅仅因为“每个人都在做”,或者基于您在会议上或在线阅读的内容而采用新技术,框架或库。故意审查具有原型的候选技术。按计划安排任务以尝试新事物并分析结果。 51 | 52 | _学习和技能改进_ 53 | 54 | 个人学习和改进是一个很好的开始,但是在团队范围内传播时,许多技能会更有效。计划这样做,无论是非正式的午餐,还是更正式的培训课程。 55 | 56 | --- 57 | ## 提示 85 安排好时间,让它成为现实 58 | --- 59 | 60 | ## 沟通团队的存在感 61 | 很明显,团队中的开发人员之间必须互相交流。我们在话题 7 [_沟通_](../Chapter1/沟通.md) 中给出了一些建议来促进这一点。然而,我们很容易忘记团队本身在组织内部也是有存在感的。团队作为一个实体,需要与外界进行清晰的沟通。 62 | 63 | 在外人看来,最糟糕的项目团队是那些看起来闷闷不乐、沉默寡言的团队。他们召开的会议没有任何结构,没有人愿意说话。他们的邮件和项目文件都是一团糟:没有两份看起来一样的,而且每个人使用的术语也不一样。 64 | 65 | 优秀的项目团队有鲜明的个性。人们期待着和他们一起开会,因为他们知道,他们会看到精心准备的表现,让每个人都觉得很好。他们所做的文档是简洁、准确、一致的。这个团队用一个声音说话。"他们甚至可能有幽默感。 66 | 67 | 有一个简单的营销技巧可以帮助团队进行统一沟通:产生一个品牌。当你启动一个项目时,为它想出一个名字,最好是一些非主流的名字。(过去,我们曾以捕食羊的鹦鹉、光学幻象、沙鼠、卡通人物和神话城市等为项目命名。) 花30分钟想出一个古怪的标志,并使用它。在与人交谈的时候,要经常使用你团队的名字。这听起来很傻,但它能让你的团队有一个可以建立的身份,让世界记住你的工作。 68 | 69 | ## 不要重复你自己 70 | 在话题 9 [_重复的恶魔_](../Chapter2/重复的恶魔.md) 中,我们谈到了消除团队成员之间重复工作的困难。这种重复工作导致了工作的浪费,并可能导致维护的噩梦。"炉火纯青" 或 "孤岛式"系统在这些团队中很常见,几乎没有共享,功能重复的情况很多。 71 | 72 | 良好的沟通是避免这些问题的关键。而我们所说的 "良好" 是指即时、无摩擦。 73 | 74 | 你应该能够向团队成员提出一个问题,并得到或多或少的即时回复。如果团队是同地办公,这可能就像把头探过立方体的墙壁或走廊上一样简单。如果是远程团队,你可能不得不依靠消息 APP 或其他电子方式。 75 | 76 | 如果你必须要等一个星期的团队会议,才能提出你的问题或分享你的状态,那是一个可怕的摩擦。无摩擦意味着你可以轻松、低调地提出问题,分享你的进展、你的问题、你的见解和学习,并保持对队友的关注,这就是无摩擦。 77 | 78 | 时刻保持注意才能 DRY。 79 | 80 | ## 团队示踪子弹 81 | 一个项目团队必须在项目的不同领域完成许多不同的任务,涉及许多不同的技术。理解需求,设计架构,为前端和服务器编码,测试,所有这些都必须发生。但人们普遍错误地认为,这些活动和任务可以单独进行。他们不能。 82 | 83 | 有些方法提倡团队中各种不同的角色和头衔,或者完全创建单独的专门团队。但这种方法的问题是它引入了门和切换。现在不是从团队到部署的平滑流程,而是在工作停止的地方设置了人工门。必须等待接受的交接, 批准。文书工作。精干的人称之为浪费,并努力积极消除它。 84 | 85 | 所有这些不同的角色和活动实际上都是对同一个问题的不同看法,人为地将它们分开会造成一大堆麻烦。例如,从代码的实际用户中删除两到三个级别的程序员不太可能知道他们的工作是在什么上下文中使用的。他们将无法做出明智的决定。 86 | 87 | 对于话题 12,[_示踪子弹_](../Chapter2/示踪子弹.md) 我们建议开发在整个系统中端到端的单个功能,不管这些功能最初多么小和有限。这意味着您需要团队中的所有技能来做到这一点:前端、UI/UX、服务器、DBA、QA等,所有这些技能都是舒适的,并且都习惯于相互协作。使用跟踪子弹方法,您可以非常快速地实现非常小的功能,并立即获得关于您的团队沟通和交付情况的反馈。这就创造了一个环境,在这个环境中,您可以快速轻松地进行更改和调整您的团队和流程。 88 | 89 | --- 90 | ## 提示 86 组织全功能的团队 91 | --- 92 | 93 | 构建团队,以便您可以端到端、增量和迭代地构建代码。 94 | 95 | ## 自动化 96 | 确保一致性和准确性的一个好方法是自动化团队所做的一切。当编辑器或 IDE 可以自动为您执行代码格式化标准时,为什么还要与之斗争呢?为什么在连续构建可以自动运行测试时要进行手动测试?当自动化每次都能以同样的方式、可重复且可靠地进行部署时,为什么还要手动部署呢? 97 | 98 | 自动化是每个项目团队的重要组成部分。确保团队具有工具构建技能,能够构建和部署自动化项目开发和生产部署的工具。 99 | 100 | ## 知道何时停止添加颜料 101 | 记住团队是由个人组成的。赋予每个成员以自己的方式发光的能力。给他们足够的结构来支持他们,并确保项目交付价值。然后,像话题 5 [_足够好的软件_](../Chapter1/足够好的软件.md) 中的画家一样,抵制住添加更多颜料的诱惑。 102 | 103 | ## 相关内容包括 104 | - 话题 7 [_沟通_](../Chapter1/沟通.md) 105 | - 话题 2 [_猫吃了我的代码_](../Chapter1/猫吃了我的代码.md) 106 | - 话题 12 [_示踪子弹_](../Chapter2/示踪子弹.md) 107 | - 话题 19 [_版本控制_](../Chapter3/版本控制.md) 108 | - 话题 50 [_实用入门套件_](./实用入门套件.md) 109 | - 话题 49 [_椰子不要切碎_](./椰子不要切碎.md) 110 | 111 | ## 挑战 112 | - 在软件开发领域之外寻找成功的团队。 是什么使他们成功? 他们是否使用本节讨论的任何过程? 113 | 114 | - 下次您启动一个项目时,请尝试说服人们对其进行品牌宣传。 给您的组织足够的时间来适应这个想法,然后进行快速审核,以查看它在团队内部和外部都有什么不同。 115 | 116 | - 代数团队:在学校里,我们遇到一些问题,例如“如果要花 4 个工人 6 个小时挖一个沟,要花 8 个工人多长时间?” 但是,在现实生活中,有什么因素会影响答案:“如果花 4 个程序员 6 个月的时间来开发应用程序,那么 8 个程序员要花多长时间?” 在几种情况下,时间实际上减少了? 117 | 118 | - 阅读 Fredrick Brooks 的《[_The Mythical Man-Month_](https://www.oreilly.com/library/view/mythical-man-month-the/0201835959/)》。 要获得额外的信誉,请购买两份,这样您可以以两倍的速度阅读。 119 | -------------------------------------------------------------------------------- /Chapter9/务实的项目.md: -------------------------------------------------------------------------------- 1 | # 务实的项目 2 | 3 | 4 | 随着您的项目的进行,我们需要摆脱个人哲学和编码问题,而要讨论更大的项目规模的问题。我们将不涉及项目管理的细节,但是我们将讨论一些可能导致或破坏任何项目的关键领域。 5 | 6 | 一旦有一个以上的人员从事一个项目,就需要建立一些基本规则并相应地委派项目的某些部分。在话题 48 [_务实的团队_](./务实的团队.md) 中,我们将展示如何在尊重实用哲学的同时做到这一点。 7 | 8 | 软件开发方法的目的是帮助人们一起工作。您和您的团队是在做对您来说适合的事情,还是仅投资于琐碎的表面工件,而没有获得应有的真正收益?我们将了解为什么话题 49 [_椰子不切实际_](./椰子不切实际.md) 并为成功提供了真正的秘密。 9 | 10 | 当然,如果您不能始终如一地可靠地交付软件,那么这些都不重要。这就是版本控制,测试和自动化的魔幻三重奏的基础:话题 50,[_实用入门套件_](./实用入门套件.md)。 11 | 12 | 最终,成功的眼光在旁观者(项目的发起人)的眼中。成功的关键在于重要性,在话题 51 [_让用户满意_](./让用户满意.md) 中,我们将向您展示如何使每个项目的发起人满意。 13 | 14 | 这本书的最后一个提示是所有其他内容的直接结果。在话题 52 [_傲慢与偏见_](./傲慢与偏见.md) 中,我们鼓励您签署工作并为自己的工作感到自豪。 15 | -------------------------------------------------------------------------------- /Chapter9/实用入门套件.md: -------------------------------------------------------------------------------- 1 | # 实用入门套件 2 | 3 | 4 | > _文明的进步是通过扩大我们可以不假思索地执行的重要操作的数量。_ 5 | > 6 | > _-- 阿尔弗雷德·诺斯·怀特黑德_ 7 | 8 | 在汽车时代到来之际,福特 T 型车的使用说明超过了两页。 对于现代汽车,您只需按一下按钮即可—启动过程是自动且简单的。 遵循一系列说明的人可能会淹没发动机,但自动启动器不会。 9 | 10 | 尽管软件开发仍处于 Model-T 阶段,但我们不能为某些常规操作而一遍又一遍地浏览两页说明。 无论是构建和发布程序,测试,项目文书工作还是项目中的其他任何重复性任务,它都必须在任何有能力的机器上都是自动且可重复的。 11 | 12 | 另外,我们要确保项目的一致性和可重复性。手动处理会保持一致性;无法保证重复性,尤其是在程序的各个方面可以由不同的人解释的情况下。 13 | 14 | 在编写了《实用程序员》的第一版之后,我们希望创建更多书籍来帮助团队开发软件。我们认为应该从头开始:无论方法,语言或技术栈如何,每个团队都需要最基本,最重要的元素。因此,实用入门套件的概念诞生了,涵盖了以下三个关键且相互关联的主题: 15 | 16 | 1. 版本控制 17 | 18 | 2. 回归测试 19 | 20 | 3. 全自动化 21 | 22 | 这是支持每个项目的三个方面。就是这样。 23 | 24 | ## 使用版本控制驱动 25 | 正如我们在话题 19 [_版本控制_](../Chapter3/版本控制.md) 中所说的那样,您希望将构建项目所需的一切都保留在版本控制下。在项目本身的背景下,这个想法变得更加重要。 26 | 27 | 首先,它允许临时使用构建机器。而不是每个人都不敢碰的办公室角落里的一台令人毛骨悚然的机器,在云中作为现场实例按需创建了机器或集群。部署配置也受版本控制,因此可以自动处理发布到生产环境。 28 | 29 | 这就是重要的部分:在项目级别,版本控制驱动着构建和发布过程。 30 | 31 | --- 32 | ## 提示 89 使用版本控制来驱动构建,测试和发布 33 | --- 34 | 35 | ### 建造机器是牛,而不是宠物 36 | 在过去,我们将服务器(尤其是构建机器)当作宠物对待:每一个都有自己的名称和身份,是手动构建和维护的,从而导致严重的单点故障。 37 | 38 | 另一方面来说,牛其实并不特别。 如果误入歧途,它将被替换。 没有戏剧。 39 | 40 | 像对待牛一样对待生产服务器和构建机器。 只是一群匿名的,并且很容易被替换。 41 | 42 | --- 43 | 44 | 也就是说,构建,测试和部署是通过提交或推送到版本控制来触发的,并内置在云的容器中。通过使用版本控制系统中的标记来指定发布到暂存或正式环境。这样一来,发布就成为日常生活中更为低调的部分-真正的持续交付,而不受任何构建机器或开发者机器的束缚。 45 | 46 | ## 冷酷而连续的测试 47 | 许多开发人员在潜意识中进行测试,潜意识地知道代码将在哪里中断并避免出现弱点。实用程序员是不同的。我们现在被迫寻找我们的错误,因此我们不必忍受其他人以后发现我们错误的耻辱。 48 | 49 | 查找错误有点像用网捕鱼。我们使用细小网(单元测试)捕获小鱼,并使用大粗网(集成测试)捕获杀手鲨。有时,鱼会设法逃脱,因此我们修补了发现的任何漏洞,希望捕获越来越多的湿滑缺陷,这些缺陷在我们的项目池中游动。 50 | 51 | --- 52 | ## 提示 90 尽早测试。经常测试。自动测试。 53 | --- 54 | 55 | 我们要在有代码后立即开始测试。这些小鱼有一个讨厌的习惯,那就是很快变成巨型食人鲨,而捉住鲨鱼则要困难得多。因此,我们编写单元测试。很多单元测试。 56 | 57 | 实际上,一个好的项目可能比生产代码具有更多的测试代码。产生此测试代码所花费的时间值得付出努力。从长远来看,它最终会便宜很多,实际上您有机会生产出缺陷率几乎为零的产品。 58 | 59 | 此外,知道您已经通过了测试,可以使您高度自信一段代码已经“完成”。 60 | 61 | --- 62 | ## 提示 91 编码直到所有测试都完成 63 | --- 64 | 65 | 自动构建将运行所有可用的测试。以“真实测试”为目标很重要,换句话说,测试环境应与生产环境紧密匹配。任何差距都是错误滋生的地方。 66 | 67 | 该版本可能涵盖几种主要的软件测试类型: 68 | 69 | - 单元测试 70 | - 整合测试 71 | - 验证与证实 72 | - 性能测试 73 | 74 | 此列表绝不是完整的,并且某些专业项目也将需要其他各种类型的测试。但这为我们提供了一个很好的起点。 75 | 76 | ### 单元测试 77 | 单元测试是执行模块的代码。我们在话题 41 [_代码测试_](../Chapter7/代码测试.md) 中介绍了这一点。单元测试是我们将在本节中讨论的所有其他测试形式的基础。如果各部分无法单独运行,则它们可能无法很好地协同工作。您正在使用的所有模块都必须通过自己的单元测试,然后才能继续。 78 | 79 | 所有相关模块通过各自的测试后,就可以进行下一阶段了。您需要测试整个系统中所有模块的使用方式以及彼此之间的交互方式。 80 | 81 | ### 整合测试 82 | 集成测试表明,构成项目的主要子系统可以正常工作并且相互配合。有了良好的合同并进行了良好的测试,可以轻松检测到任何集成问题。否则,集成将成为错误的沃土。实际上,它通常是系统中最大的错误来源。 83 | 84 | 集成测试实际上只是我们描述的单元测试的扩展-仅现在您正在测试整个子系统如何履行其合同。 85 | 86 | ### 验证与证实 87 | 一旦有了可执行的用户界面或原型,就需要回答一个非常重要的问题:用户告诉您他们想要什么,但这是他们需要的吗? 88 | 89 | 是否满足系统功能要求?这也需要测试。能够回答错误问题的无错误系统不是很有用。注意最终用户访问模式以及它们与开发人员测试数据的区别(例如,请参见此处有关笔触的故事)。 90 | 91 | ### 性能测试 92 | 性能测试,压力测试或负载测试也可能是项目的重要方面。 93 | 94 | 询问您自己,该软件是否满足现实条件下的性能要求,即每秒预期的用户,连接或事务数量。它可扩展吗? 95 | 96 | 对于某些应用程序,您可能需要专门的测试硬件或软件来实际模拟负载。 97 | 98 | ### 测试你的测试 99 | 由于我们无法编写完美的软件,因此我们也无法编写完美的测试软件。我们需要测试。 100 | 101 | 可以将我们的测试套件集视为精心设计的安全系统,旨在在出现错误时发出警报。测试安全系统比尝试闯入更好? 102 | 103 | 编写测试以检测到特定错误的测试后,请故意导致该错误并确保测试能够解决。这样可以确保测试能够在实际发生时捕获错误。 104 | 105 | --- 106 | ## 提示 92 使用破坏者来测试您的测试 107 | --- 108 | 109 | 如果您真的很认真地进行测试,请在源代码树的另一个分支上,故意引入错误,并验证测试是否可以捕获它们。在更高的层次上,您可以使用 Netflix 的 Chaos Monkey 之类的功能来破坏(即“杀死”)服务并测试应用程序的弹性。 110 | 111 | 编写测试时,请确保警报在应有的时候响起。 112 | 113 | ### 彻底测试 114 | 一旦确定测试正确无误并找到了所创建的错误,如何知道您是否已对代码库进行了充分的测试? 115 | 116 | 简短的答案是“您不愿意”,而您永远也不会。但是市场上有些产品可以提供帮助。这些覆盖率分析工具会在测试过程中监视您的代码,并跟踪已执行的代码行和未执行的代码行。这些工具可帮助您大致了解测试的全面程度,但不要期望覆盖率达到100%。 117 | 118 | 即使碰巧碰到了每一行代码,也不是全部。重要的是程序可能具有的状态数。状态不等同于代码行。例如,假设您有一个接受两个整数的函数,每个整数可以是 0 到 999 之间的数字。 119 | 120 | ```java 121 | int test(int a, int b) { 122 | return a / (a + b) 123 | } 124 | ``` 125 | 126 | 从理论上讲,此三行函数具有1,000,000个逻辑状态,其中999,999个逻辑状态将正常工作,而一个不会(当 a + b等于零时)。仅仅知道执行了这一行代码并不能告诉您-您需要确定程序的所有可能状态。不幸的是,总的来说这是一个非常困难的问题。就像“在解决它之前,太阳将是一团冰冷的硬块。” 127 | 128 | --- 129 | ## 提示 93 测试状态范围,而非代码范围 130 | --- 131 | 132 | ### 基于属性的测试 133 | 探索代码如何处理意外状态的一种好方法是让计算机生成这些状态。 134 | 135 | 使用基于属性的测试技术根据被测代码的约定和不变量生成测试数据。我们将在话题 42 [_基于属性的测试_](../Chapter7/基于属性的测试.md) 中详细介绍该主题。 136 | 137 | ## 收紧网 138 | 最后,我们想揭示测试中最重要的一个概念。这是显而易见的,几乎每本教科书都说要这样做。但是由于某些原因,大多数项目仍然没有。 139 | 140 | 如果有漏洞通过现有测试,您需要添加一个新测试以求在下一次捕获它。 141 | 142 | --- 143 | ## 提示 94 一旦发现错误 144 | --- 145 | 146 | 一旦测试人员发现了错误,那应该是测试人员最后一次发现该错误。从那时起,应该修改自动测试以检查该特定错误,无论是多么琐碎的事情,无论开发人员抱怨多少,并说:“哦,那再也不会发生了。” 147 | 148 | 因为它将再次发生。而且,我们只是没有时间去追寻自动测试可能为我们找到的错误。我们必须花时间编写新代码和新错误。 149 | 150 | ## 全自动化 151 | 正如我们在本节开头所说的那样,现代开发依赖于脚本化的自动过程。无论您使用带有 rsync 和 ssh 的 shell 脚本之类的简单脚本,还是使用诸如 Ansible,Puppet,Chef 或 Salt 的功能齐全的解决方案,都无需依赖任何手动干预。 152 | 153 | 曾几何时,我们在所有开发人员都使用相同 IDE 的客户端站点上。他们的系统管理员为每个开发人员提供了有关将附加软件包安装到 IDE 的一组说明。这些说明占据了很多页面,这些页面充满了单击此处,滚动到此处,将其拖动,双击并再次进行操作。 154 | 155 | 毫不奇怪,每台开发人员的计算机加载时都略有不同。当不同的开发人员运行相同的代码时,应用程序的行为会有细微的差异。错误会在一台机器上出现,但不会在其他机器上出现。追踪任何一个组件的版本差异通常会令人惊讶。 156 | 157 | --- 158 | ## 提示 95 请勿使用手动程序 159 | --- 160 | 161 | 人们的可重复性不如计算机。我们也不应该期望他们如此。 Shell 脚本或程序将一次又一次地以相同顺序执行相同的指令。它本身受版本控制,因此您也可以检查随着时间的推移对构建/发布过程所做的更改(“但是它曾经可以工作……”)。 162 | 163 | 一切都取决于自动化。除非构建是全自动的,否则您无法在云中的匿名服务器上构建项目。如果涉及手动步骤,则无法自动部署。一旦您介绍了手动步骤(“仅针对这一部分……”),您就打破了一个很大的窗口。 164 | 165 | 通过这三个方面的版本控制,无情的测试和完全自动化,您的项目将具有所需的牢固基础,因此您可以专注于困难的部分:使用户满意。 166 | 167 | ## 相关内容包括 168 | - 话题 17 [_Shell 游戏_](../Chapter/shell.md) 169 | - 话题 11 [_可逆性_](../Chapter2/可逆性.md) 170 | - 话题 48 [_务实的团队_](./务实的团队.md) 171 | - 话题 41 [_代码测试_](../Chapter7/代码测试.md) 172 | - 话题 12 [_示踪子弹_](../Chapter2/示踪子弹.md) 173 | - 话题 19 [_版本控制_](../Chapter3/版本控制.md) 174 | - 话题 49 [_椰子是不适合的_](./椰子是不适合的.md) 175 | 176 | ## 挑战 177 | - 您夜间的连续构建是否自动进行,但不能部署到生产中? 为什么? 该服务器有什么特别之处? 178 | 179 | - 您可以完全自动测试您的项目吗? 许多团队被迫回答“否”。 为什么? 定义可接受的结果是否太难了? 这是否会使发起人难以证明该项目“完成”了? 180 | 181 | - 独立于 GUI 测试应用程序逻辑是否太难了? 这对 GUI 表示什么? 关于耦合? 182 | -------------------------------------------------------------------------------- /Chapter9/椰子不要切碎.md: -------------------------------------------------------------------------------- 1 | # 椰子不要切碎 2 | 3 | 4 | 土生土长的岛上居民之前从来没有见过一架飞机,也没有遇到过这样的陌生人。作为对土地使用权的回报,这些陌生人提供的机械鸟(指飞机)整天在跑道上飞进飞出,给他们的岛屿家园带来了惊人的物质财富。陌生人提到了一些关于战争和战斗的事情。有一天战争结束他们都带着他们的奇特财富离开了。 5 | 6 | 岛民们不顾一切地想恢复自己的好运气,用当地的材料:藤蔓、椰壳、棕榈树叶之类的东西,重新建造了一个仿制的机场、控制塔和设备。但是不知道为什么,尽管他们把所有的元素都准备好了,但飞机却没有来。他们模仿了形式,却没有模仿内容。人类学家称这为货物崇拜。 7 | 8 | 很多时候,我们就是岛上的人。 9 | 10 | 很容易也很有诱惑力我们落入货物崇拜的陷阱:通过投资和堆砌那些容易看得到的神器,希望能吸引到底层的、能起作用的法宝。但就像美拉尼西亚的原始货运崇拜一样,用椰子壳做成的假机场也不能代替真货。 11 | 12 | 比如说,我们亲眼见过宣称自己在使用 Scrum 的团队。但是仔细观察后发现,原来他们每天都在做每周一次的站立会议,四周的迭代往往会变成六周或八周的迭代。他们觉得这样做还可以,因为他们使用的是一种流行的 "敏捷" 排程工具。他们 13 | 只是投资于表面的人工智能,即使是这样,也往往是名存实亡,仿佛 "站起来" 或 "迭代" 是迷信者的某种咒语。不足为奇的是,他们也没能吸引到真正的魔力。 14 | 15 | ## 背景的重要性 16 | 你或你的团队是否落入了这个陷阱?扪心自问,你甚至为什么要使用那种特定的开发方法?还是那个框架?或者是那个测试技术?它是否真的很适合当前的工作?它对你的工作是否有效?还是只是因为最新的互联网成功案例采用了它? 17 | 18 | 目前有一种趋势是采用 Spotify、Netflix、Stripe、GitLab 等成功公司的策略和流程。在软件开发和管理方面,他们各自都有自己独特的做法。但考虑一下背景:你们是否处于同一个市场,有相同的限制和机会,相似的专业知识和组织规模,相似的管理,相似的文化?类似的用户群和需求? 19 | 20 | --- 21 | ### 要像他们一样! 22 | 我们经常听到软件开发的领导者告诉他们的员工 "我们应该像 Netflix 一样运营"(或者其他这些领先的公司之一)。你当然可以这么做。 23 | 24 | 首先,让自己拥有几十万的服务器和几千万的用户...... 25 | 26 | --- 27 | 28 | 不要上当受骗。仅仅有特定的神器、表面的结构、策略、流程、方法是不够的。 29 | 30 | --- 31 | ## 提示 87 做有用的东西,而不是时髦的东西 32 | --- 33 | 34 | 你怎么知道 "什么是有效的?" 你依靠的是最基本的实用技术: 35 | 36 | 试一试。 37 | 38 | 用一个或一组小团队来试行这个想法。保留那些看起来效果不错的好的部分,把其他作为浪费或开销的东西丢掉。没有人会因为你的组织运作方式与 Spotify 或 Netflix 不同而降低你的组织,因为即使是他们在成长的时候也没有按照他们目前的流程来做。而多年以后,当这些公司成熟、转折并继续茁壮成长的时候,他们又将会再次做一些不同的事情。 39 | 40 | 这就是他们成功的真正秘诀。 41 | 42 | ## 一刀切并不合身 43 | 软件开发方法论的目的是帮助大家一起工作。正如我们在话题 47 [_敏捷的本质_](../Chapter8/敏捷的本质.md) 中讨论的那样,当你在开发软件的时候,没有一个单一的计划可以遵循,尤其是别人在别的公司想出来的计划,你就更不可能遵循。 44 | 45 | 很多认证计划实际上比这更糟糕:它们的前提是学生能够背诵和遵循规则。但这不是你想要的。你需要的是能够看清现有规则之外的东西,并利用各种可能性来获取优势。这和 "但是 Scrum/Lean/Kanban/XP/agile 是这样做的........... "之类的心态完全不同。 46 | 47 | 相反,你要从任何一个特定的方法论中提取最好的部分,并对其进行调整使用。没有一刀切的方法,而且目前的方法还远未完善,所以你需要关注的不仅仅是一种流行的方法。 48 | 49 | 例如,Scrum 是一套有用的项目管理实践。然而,Scrum本身并不能在技术层面为团队提供足够的指导,也不能在组合/治理层面为领导层提供足够的指导。那么你该从哪里开始呢? 50 | 51 | ## 真正的目的 52 | 当然,我们的目标不是 "做Scrum"、"做敏捷"、"做精益" 或是其他。我们的目标是要在一瞬间就能提供给用户一些新能力的工作软件。不是几周、几个月或几年后,而是现在。对于许多团队和组织来说,持续交付感觉是一个高不可攀的目标,尤其是当你的流程将交付时间限制在几个月甚至几周的时候。但与任何目标一样,关键是要保持正确的方向。 53 | 54 | ![deliver time](../assets/topic49_1.png) 55 | 56 | 如果你是以年为单位进行交付,那就尽量把周期缩短到几个月。从几个月缩短到几周。从四周的冲刺,尽量缩短为两周。从两周的冲刺,尝试一个。然后是每天。然后,最后是按需交付。注意,能够按需交付并不意味着你必须每天每时每刻都要交付。你要在用户需要的时候,当这样做是有商业意义的时候,你才去交付。 57 | 58 | --- 59 | ## 提示 88 在用户需要的时候交付 60 | --- 61 | 62 | 为了转到这种持续开发的风格,你需要一个坚如磐石的基础架构,这一点我们在话题 50 [_实用入门套件_](./实用入门套件.md) 中讨论。你在版本控制系统的主干中进行开发,而不是在分支中进行开发,并使用功能切换等技术有选择地向用户推出测试功能。 63 | 64 | 一旦你的基础架构做好了,你需要决定如何组织工作。初学者可能想从 Scrum 开始项目管理,再加上极限编程(XP)的技术实践。更严谨、更有经验的团队可能会把目光投向看板和精益技术,既可以解决团队的问题,或许也可以解决更大的治理问题。 65 | 66 | 但不要相信我们的话,自己去调查和尝试这些方法。不过要小心过头了。过度投资于任何特定的方法,会让你对其他方法视而不见。当你习惯于此。很快你就会变得很难看到任何其他的方法。你已经钙化了,现在你已经无法快速适应了。 67 | 68 | 还不如用椰子呢。 69 | 70 | ## 相关内容包括 71 | • 话题 27 [_别开过头了_](../Chapter4/别开过头了.md) 72 | • 话题 48 [_务实的团队_](./务实的团队.md) 73 | • 话题 12 [_示踪子弹_](../Chapter2/示踪子弹.md) 74 | • 话题 50 [_实用入门套件_](./实用入门套件.md) 75 | • 话题 47 [_敏捷的本质_](../Chapter8/敏捷的本质.md) 76 | -------------------------------------------------------------------------------- /Chapter9/让用户满意.md: -------------------------------------------------------------------------------- 1 | # 让用户满意 2 | 3 | 4 | > _当你迷惑人的时候,你的目标不是为了从他们身上赚钱,也不是为了让他们做你想要的事情,而是为了让他们充满喜悦。_ 5 | > 6 | > _-- 盖伊-川崎_ 7 | 8 | 作为开发者,我们的目标是让用户满意。这也是我们在这里的原因。而不是为了挖掘他们的数据,或数落他们的眼球或掏空他们的钱包。撇开邪恶的目标不谈,即使是及时交付软件也是不够的。光是这一点是不会让他们高兴的。 9 | 10 | 你的用户对代码并不是特别有兴趣。相反,他们有一个需要在他们的目标和预算范围内解决的业务问题。他们相信通过与你的团队合作,他们就能做到这一点。 11 | 12 | 他们的期望与软件无关。他们甚至没有隐含在他们给你的任何规范中(因为在你的团队和他们一起迭代过几次之前,这个规范是不完整的)。这与软件无关。 13 | 14 | 相反,问一个相当简单的问题。 15 | 16 | 在这个项目完成后的一个月(或者说一年,或者是其他什么),你怎么知道我们都成功了呢? 17 | 18 | 你很可能会对答案感到惊讶。一个改进产品推荐的项目,实际上可能会以客户留存率来判断;一个整合两个数据库的项目,可能会以数据质量来判断,也可能是以节约成本来判断。但真正重要的是这些对商业价值的期望,而不仅仅是软件项目本身。软件只是达到这些目的的一种手段。 19 | 20 | 现在,你已经知道了项目背后的一些潜在价值期望,你可以开始思考如何实现这些期望。 21 | 22 | - 确保团队中的每个人都完全清楚这些期望。 23 | 24 | - 在做决定的时候,考虑一下哪条路更接近这些期望。 25 | 26 | - 根据这些期望,对用户需求进行批判性的分析。在许多项目中,我们发现声明的 "需求" 实际上只是对技术可以做的事情的猜测:它实际上是一个外行的实施计划,伪装成需求文档。不要害怕提出改变需求的建议,如果你能证明这些建议会使项目更接近目标,就不要害怕提出改变需求的建议。 27 | 28 | - 随着项目的进展,继续思考这些期望。 29 | 30 | 我们发现,随着我们对该领域知识的增加,我们能够更好地提出其他可以解决基础业务问题的建议。我们坚信开发人员接触到组织中许多不同的方面,他们往往能看到将业务的不同部分编织在一起的方法,而这些方法对单个部门来说并不总是很明显。 31 | 32 | ## 让用户满意,不要只是交付代码 33 | 如果你想让你的客户满意,那就与他们建立一种关系,在这种关系中,你可以主动帮助他们解决问题。即使你的头衔可能是 "软件开发人员" 或 "软件工程师" 的变体,但实际上应该是 "问题解决者"。这就是我们要做的事情,这就是务实的程序员的本质。 34 | 35 | 我们解决的是问题。 36 | 37 | ## 相关内容包括 38 | - 话题 13 [_原型和便签_](../Chapter2/原型和便签.md) 39 | - 话题 45 [_需求坑_](../Chapter8/需求坑.md) 40 | - 话题 12 [_示踪子弹_](../Chapter2/示踪子弹.md) 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pragmatic-programmer-zh 2 | 3 | 《Pragmatic Programmer》中文翻译 欢迎 star 4 | 5 | 由于个人水平有限 精力有限 所以有些翻译或许会显得生硬难懂 有些上下文翻译成中文就会失去原文的味道 在此还请各位大佬包涵 不过说真的 假如你有英语的阅读能力 那我强烈推荐你读原版的 [_戳这里下载_](https://salttiger.com/the-pragmatic-programmer-20th-anniversary-edition-2nd-edition/) 6 | 7 | [Gitbook 传送门](https://caicaishmily.gitbooks.io/pragmatic_programmer/content/) 8 | 9 | ## [第一章 务实主义哲学](./Chapter1/务实主义哲学.md) 10 | - [话题 1 这是你的人生](./Chapter1/这是你的人生.md) 11 | - [话题 2 猫吃了我的源代码](./Chapter1/猫吃了我的源代码.md) 12 | - [话题 3 软件熵](./Chapter1/软件熵.md) 13 | - [话题 4 石汤和煮青蛙](./Chapter1/石汤和煮青蛙.md) 14 | - [话题 5 足够好的软件](./Chapter1/足够好的软件.md) 15 | - [话题 6 你的知识组合](./Chapter1/你的知识组合.md) 16 | - [话题 7 沟通](./Chapter1/沟通.md) 17 | 18 | ## [第二章 务实的方法](./Chapter2/务实的方法.md) 19 | - [话题 8 好设计的本质](./Chapter2/好设计的本质.md) 20 | - [话题 9 重复的恶魔](./Chapter2/重复的恶魔.md) 21 | - [话题 10 正交性](./Chapter2/正交性.md) 22 | - [话题 11 可逆性](./Chapter2/可逆性.md) 23 | - [话题 12 示踪子弹](./Chapter2/示踪子弹.md) 24 | - [话题 13 原型和便签](./Chapter2/原型和便签.md) 25 | - [话题 14 域语言](./Chapter2/域语言.md) 26 | - [话题 15 评估](./Chapter2/评估.md) 27 | 28 | ## [第三章 基本工具](./Chapter3/基本工具.md) 29 | - [话题 16 纯文本的力量](./Chapter3/纯文本的力量.md) 30 | - [话题 17 shell 游戏](./Chapter3/shell.md) 31 | - [话题 18 强大的编辑](./Chapter3/强大的编辑.md) 32 | - [话题 19 版本控制](./Chapter3/版本控制.md) 33 | - [话题 20 调试](./Chapter3/调试.md) 34 | - [话题 21 文本处理](./Chapter3/文本处理.md) 35 | - [话题 22 工程日记](./Chapter3/工程日记.md) 36 | 37 | ## [第四章 程序性妄想症](./Chapter4/程序性妄想症.md) 38 | - [话题 23 契约设计](./Chapter4/契约设计.md) 39 | - [话题 24 死程序不说谎](./Chapter4/死程序不说谎.md) 40 | - [话题 25 断言式编程](./Chapter4/断言式编程.md) 41 | - [话题 26 如何平衡资源](./Chapter4/如何平衡资源.md) 42 | - [话题 27 别开过头了](./Chapter4/别开过头了.md) 43 | 44 | ## [第五章 弯曲或折断](./Chapter5/弯曲或折断.md) 45 | - [话题 28 解耦](./Chapter5/解耦.md) 46 | - [话题 29 杂耍现实世界](./Chapter5/杂耍现实世界.md) 47 | - [话题 30 转换编程](./Chapter5/转换编程.md) 48 | - [话题 31 遗产税](./Chapter5/遗产税.md) 49 | - [话题 32 配置](./Chapter5/配置.md) 50 | 51 | ## [第六章 并发](./Chapter6/并发.md) 52 | - [话题 33 断开时间耦合](./Chapter6/断开时间耦合.md) 53 | - [话题 34 共享状态不正确](./Chapter6/共享状态不正确.md) 54 | - [话题 35 Actors 和进程](./Chapter6/actors和进程.md) 55 | - [话题 36 黑板](./Chapter6/黑板.md) 56 | 57 | ## [第七章 当你编程时](./Chapter7/当你编程时.md) 58 | - [话题 37 聆听你的蜥蜴脑](./Chapter7/聆听你的蜥蜴脑.md) 59 | - [话题 38 巧合编程](./Chapter7/巧合编程.md) 60 | - [话题 39 算法速度](./Chapter7/算法速度.md) 61 | - [话题 40 重构](./Chapter7/重构.md) 62 | - [话题 41 代码测试](./Chapter7/代码测试.md) 63 | - [话题 42 基于属性的测试](./Chapter7/基于属性的测试.md) 64 | - [话题 43 在某处保持安全](./Chapter7/在某处保持安全.md) 65 | - [话题 44 命名](./Chapter7/命名.md) 66 | 67 | ## [第八章 项目之前](./Chapter8/项目之前.md) 68 | - [话题 45 需求坑](./Chapter8/需求坑.md) 69 | - [话题 46 解决不可能的难题](./Chapter8/解决不可能的难题.md) 70 | - [话题 47 敏捷的本质](./Chapter8/敏捷的本质.md) 71 | 72 | ## [第九章 务实的项目](./Chapter9/务实的项目.md) 73 | - [话题 48 务实的团队](./Chapter9/务实的团队.md) 74 | - [话题 49 椰子不要切碎](./Chapter9/椰子不要切碎.md) 75 | - [话题 50 实用入门套件](./Chapter9/实用入门套件.md) 76 | - [话题 51 让用户满意](./Chapter9/让用户满意.md) 77 | - [话题 52 傲慢与偏见](./Chapter9/傲慢与偏见.md) 78 | 79 | ## [第十章 刊后语](./Chapter10/刊后语.md) 80 | -------------------------------------------------------------------------------- /assets/Orthogonality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/Orthogonality.png -------------------------------------------------------------------------------- /assets/bullet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/bullet.png -------------------------------------------------------------------------------- /assets/layering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/layering.png -------------------------------------------------------------------------------- /assets/topic28_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic28_1.png -------------------------------------------------------------------------------- /assets/topic28_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic28_2.png -------------------------------------------------------------------------------- /assets/topic29_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic29_1.png -------------------------------------------------------------------------------- /assets/topic29_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic29_2.png -------------------------------------------------------------------------------- /assets/topic29_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic29_3.png -------------------------------------------------------------------------------- /assets/topic29_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic29_4.png -------------------------------------------------------------------------------- /assets/topic30_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic30_1.png -------------------------------------------------------------------------------- /assets/topic31_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic31_1.png -------------------------------------------------------------------------------- /assets/topic31_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic31_2.png -------------------------------------------------------------------------------- /assets/topic33_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic33_1.png -------------------------------------------------------------------------------- /assets/topic34_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic34_1.png -------------------------------------------------------------------------------- /assets/topic36_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic36_1.png -------------------------------------------------------------------------------- /assets/topic39_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic39_1.png -------------------------------------------------------------------------------- /assets/topic44_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic44_1.png -------------------------------------------------------------------------------- /assets/topic49_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HabenChan/pragmatic-programmer-zh/d002876acb5569484b0d3d961018cbc6fdebd62d/assets/topic49_1.png --------------------------------------------------------------------------------