《代码之外的功夫:程序员精进之路》
《代码之外的功夫:程序员精进之路》
第1章 善用设计原型,探索项目创意
- 无论做什么产品或项目,首要任务都是尽快发掘和实现客户头脑中的需求。
- 项目刚刚起步时,对话和绘制线框图是很有用的方法;
- 但紧接着就应该进入探索式编程阶段。
- 尽早生成可工作的软件,可以令产品设计变成交互式协作过程。高效的反馈环有利于快速识别潜在的不良设计,并对此提出解决方案,以免日后在更关键的阶段浪费大量时间和精力。
- 再简单的软件系统也是由很多活动的组件构成的,所以在设计的早期阶段就让它运转起来,看看组件之间的相互作用,是很有好处的。
- 探索式编程技术,在开发过程开始后的几小时内为产品创意构思出有意义的概念验证方案。
1.1 从理解项目背后的需求入手
- 探讨概念验证方案
- 新系统是要作为独立项目,还是要和现有的某些系统整合在一起。
- 客户不是很明确自己的需求,提出的细节问题都可以暂时搁置,集中精力探讨这个想法究竟是否可行。
1.2 利用线框图表达功能需求
- 可以通过线框图向大家解释待开发应用的基本结构,同时让大家都了解需要完成什么工作,以免过度关注技术细节而迷失了方向。
- 先集中精力寻找“最简单可行的方法”。(这一观点,目的在于提醒人们注意工作的最根本目标,不要因为过多地考虑收益损失而迷失方向。)
- 新想法非常不错,但是实现起来可能不太容易。在稍微权衡利弊之后,统一意见先实现前一个简单的想法,毕竟这样能更快将真实的网页交到真人手上测试使用。
1.3 编程之初立即搭建测试系统
- 速成原型法的意义是拉近项目中每个参与者之间的距离;不仅是开发人员和客户之间,也包括客户和用户之间。
- 在这个阶段,你对项目完成时生产环境最终的样子毫无概念,其实你也不太在乎。为了方便从客户的目标观众那里收集反馈,你会构建一些考察性的特性,这部分代码在产品完成上线之前就会被移除。
- YAGNI即You Aren't Gonna Need It(你不需要它)。这是一种设计原则,旨在告诉设计人员不要添加任何不必要的功能。
- 细节根本无关紧要。
- 第一次发布的真正目的是创建一个可用的系统,以便提高后续的变更速度,并由此开始探索项目创意的过程。
1.4 全面探讨不足,改善追求实效
- 完全不犯错误是不可能的,但最关键的是你对这些问题作何反应。
- 每当发现自己的软件有瑕疵时,你可能想要停下手中的工作,立即去修复。但是在项目的探索阶段,你得去平衡软件缺陷带来的损失和修复这一缺陷的时间成本。
1.5 早问多问,验证设想
- 验证对这种事情的设想总是有好处的,而且越早验证越好。最初的线框图主要关注用户界面的外观,而现在是时候讨论一下系统如何工作了。
- 如果下次客户头脑中有什么复杂的想法,早问总比晚问好。所谓“学之乃知,不问不识”,多问问总不会有坏处!
1.6 力求缩小自己的工作范围
- 对细节关注得过多了。所以你们又回到那个经典问题:“最简单可行的方法是什么?”
- “行走的骷髅”指的是以端对端的方式小规模实现某个特性。它可以作为项目的一个很好的切入点,有助于设计和演化系统的剩余部分。
- 第一天所做的大部分工作是搭建一个基本框架,因此接下来的开发工作就会变得相对容易。你可以在这个已有的框架中发觉真正的问题,并试着去解决。如果一开始就直接考虑整个问题怎么解决,你就会很难找到一个切入点,这样停滞的时间就会更长。
1.7 谨记原型并非生产系统
- 为了节约时间,决定先用不受官方支持的方法来生成缩略图URL。但是你做了标注,好让这一模块的后续开发人员知道有这么一回事。
1.8 巧妙设计特性,轻松收集反馈
- 想要做到张弛有毒,知道什么时候该块,什么时候该细,是需要经验的。
- 俗话说“细节决定成败”,随后的几次迭代会设计更多细节。在原型阶段结束之前,应该至少会有一个始料未及的重大问题浮出水面。
- 但出现这样的情况并不意味着开发过程有问题,你恰恰应该做好准备面对反馈环的加速所产生的这一副作用。
- 原型可以帮助你更快地做出有用的产品,但也可能让你失败得更快。如果不用花太多时间就能辨认出死胡同,那你就有更多的时间和精力去寻找正确的路。
忠告与提醒
- 多向项目参与者提一些能够发掘其目标的问题。这样一来,你既可以验证自己的想法,又可以更好地了解他人对问题的看法。
- 绘制线框图(草图)可以清晰地和他人探讨应用的结构,不会因为被样式细节绊住而停滞不前。
- 一定要在一开始写代码的时候就搭建一个测试系统,让大家都能与其交互。测试系统不需要完善到满足上线要求,只要适合收集有用的反馈即可。
- 在项目早期,集中精力解决有风险或未知的问题。建立原型是为了探索问题空间,而不是为了做出完整的产品。
《代码之外的功夫:程序员精进之路》
第2章 观察增量变更,发掘隐藏依赖
- 假设你所在的公司以其基于高质量文档的大型知识库而闻名。
2.1 不存在所谓的“独立特性”
- 在添加新特性的时候,有意避开了对原有内容管理系统的代码做任何的修改。即使维基系统本身土崩瓦解,对网站的其他部分又能有什么影响呢?
- 不加限制地允许任何人新建和编辑页面,从存储的角度来说有极大的风险。
- 需要考虑许多可能发生的攻击行为,从创建超大文档以占满所有的存储空间,到创建大量小文档,再到飞速创建文档使存储机制过载。
- 因为知识库和维基系统使用了同一个存储机制,所以只攻击维基系统就能把原有知识库一起干掉。这就是基础设施层面存在依赖关系的例子。这种依赖在你刚刚为原有代码库引入新变更时,不会表现得很明显。
- 降低风险的步骤:
- 你把最大页数限定为不超过1000个文档。
- 你把每个维基页面的大小限定为不超过500KB。
- 你把维基系统的Markdown进程放到一个队列里,并把队列大小设置为不超过20个挂起任务。当队列过载时,会发出“请重试”的出错信息。
- 你加入了监听器,以便监管维基页面的创建、删除和编辑。这些操作发生得太过频繁时系统会发出警告。
- 你为知识库网站加入了有效性监管机制,每分钟进行两次ping操作,以保证网站一直能够有效访问,并在可接受的时间范围内响应请求。这项工作本应该很久之前就做好。但现在由于对改良监管机制有明确的需求,因此这时候做再适合不过了。
- 这些方法本身不足以保证系统绝对安全。然而,花上大约一小时,预防一下由共享基础设施引发的最基本的风险,是非常值得的。
2.2 两特性同屏必相互依赖
- 这些赶出来的工作会在以后出问题时反过头来咬你一口吗?
- 在告诉客户你的这些想法之前,你决定先快速分析一下,看看自己在当前的情况下能做到什么程度。
- 根本没有零风险的事情。
- “特性内测”(Feature Flipping)是一种向特性用户发布新特性的即使。特定用户有可能是某个开发人员、一组测试人员或网站的一部分真实访问者。已经有很多开源库支持这种工作方式,所以应该很容易找到一个兼容你喜欢的程序语言的库。
- 对数据库模式所作的任何修改都应该考虑到数据的一致性。不管新特性在代码层面上多么独立,在数据层面上仍有可能存在隐藏的依赖关系。这就意味着,为了支持某一特性而在代码库的某一部分进行的模式升级,可能会使一些看起来毫无关系的其他特性崩溃。
- 如果两个特性在统一个页面显式,就得做些测试,检查以下它们之间会不会互相影响。
2.3 避免不必要的实时数据同步
- 经验告诉你,集成外部服务一般是非常让人头疼的工作,因为会出现各种奇奇怪怪、令人十分不悦的问题。你得提前考虑到,所有的服务集成都可能出现以下问题:响应慢,因评分限制问题而拒绝请求,频繁出现停机故障,返回空响应或格式不正确的响应,触发超时错误等等。即使上述问题都每出现,迟早也会冒出其他问题,反正就是不让你好过。
- 如果实在需要操作实时数据,那就每别的办法了,乖乖地花大量时间和精力去写健壮的、容错能力强的代码吧。但在这个案例中,如果每天只把页面访问量更新几次,受欢迎度评分依旧是比较准确、可以接受的。因此,正确的解决方案就是写一个小脚本,在其中设定计划任务。
- 偶尔发生的错误并不会产生不良影响,因为这段代码运行在主程序之外。最糟糕的后果不过是受欢迎度评分更新得不及时。
- 用这种方法,你缩小了问题范围,将它变成了一个简单的数据库查询操作。同时,你也避免了向主站应用添加新的设置信息或库,因为脚本是独立运行的,并且只在数据库层与主站应用共享信息。
2.4 复用旧代码,寻找新问题
- 把维基转到维护页面之后,你开始写一个脚本,用于侦测Markdown文档中的HTML标签。这可以帮助你确定有多少页面收到影响以及如何修复。
- 到现在为止,维基系统已经拒绝所有访问达半小时了,但你对问题有了更好的理解。
- 你开始恢复维基的部分功能,以求把对客户的负面影响降到最低。你首先把那12个受影响的页面中的script标签移除,然后部署了一段代码,允许对维基页面进行只读访问,打电话给客户团队,向他们报告了你们的进展。到这一刻,紧张的气氛似乎已经有所缓解了。
- 从根源上讲,这其实也是一个依赖问题。你复用了一个为某种目的而鳄梨设置的工具,但并没有考虑到在稍有不同的情况下,这种设置可能会产生不良影响。你这样做时,只考虑到两种使用环境表面上的相似性,而忽略了二者本质的不同。这使你被表象迷惑,从而做出了错误的判断。
- 需要把因修复而带来的负面影响降到最低。
忠告与提醒
- 不要因为某个变更没有明显改变现有特性,就认为它会向后兼容或绝对安全。相反,应该对隐藏的依赖关系随时保持警惕,即使进行的是最简单的更新操作。
- 注意除代码库之外的大量共享资源:存储机制、处理能力、数据库、外部服务、库、用户界面,等等。这些工具形成了一张“隐藏依赖网”,会给看起来毫无关联的应用特性带来副作用或引起故障。
- 利用限制和验证的方式,在最大程度上防止局部故障对整个系统造成影响。但还要确保系统拥有良好的监控机制,以保证快速知晓和处理突如其来的系统故障。在复用现有的工具和资源时,要尤其注意使用环境的变化。任何对使用范围、性能标准或隐私安全级别的改变,如果不经过仔细考虑,都可能引起非常危险的问题。
《代码之外的功夫:程序员精进之路》
第3章 准确识别痛点,高效集成服务
- 假设你管理者一份面向程序员的教育刊物。该刊物拥有少量的付费读者,其收入不足以支持你雇用全职员工。
- 使用自己控制不了的代码其实成本很高,也有很大风险。由于在设计软件时欠考虑,你已经吃了好几次苦头。这让你在进行外部服务集成时变得更加谨慎。
3.1 面对小众需求,切记未雨绸缪
- 草率决策本来是可以避免的。
- 让你陷入窘境的根本原因时自己的思维过程有缺陷。
- 冒烟测试是肯定是有帮助的,但该想到的时候,我总想不到。这才是真正的问题。
- 理论上讲,在使用任何第三方系统时,我们都不能完全信任它,除非已经证明它确实可靠。但现实是,时间紧迫、预算紧张;我们经常得急着往前赶。
- 解决方案:
- 应该多做一些调查,看看有没有其他人也在以你的这种方式使用该服务。也许很难找到这样的人,这本身就应该给你敲响警钟,让你停下来思考一下为什么没有人这样用。
- 对于这种特殊用法,不应该想当然地认为一定会顺利,这是很危险的。反之,最好是采取和对待其他位置问题一样的态度,保持警惕。注意到风险后,应该进行更为全面的测试,或者应该考虑至少用几周时间进行内测,而不是急着开放注册功能。
- 另一个被忽略的问题是:”如果第三方服务不符合我们的预期,该怎么办?“这个问题在任何涉及软件系统中的重要依赖时都应该被问到。无论你是想进行纯粹的思维实验,还是想着手制订详实的备选计划,注意这个问题都会对你大有裨益。如果服务真的出现问题,你就不会大惊失色,也不会手足无措。
3.2 谨记外部服务并不可靠
- 在收到第一封警告邮件之后的一小时内,你就找出了解决方案,但不出所料,你的方案并不完善。然后你找到了一个更为完善的方案,但补丁引入了一个小小的错误,而几天后你才发现这个错误。
- 产生这个故障的技术原因没有什么特别之处,但你认为,探究一下问题究竟为什么会发生,可以帮助你获得一些有用的见解。为了揭示问题的答案,你建议玩“五个为什么”的游戏。
- “五个为什么”一般用于发就问题的根本原因。做法是通过反复询问“为什么”来揭示问题的大背景。由于大部分问题的根本原因不止一个,因此为了从不同角度发掘原因,可以根据需要,不断重复询问的过程。
- 审查一下正在进行的所有项目,看看它们依赖哪些服务,然后确定如何得知服务发生变更。你们俩达成一致意见,决定先抽出一些时间做这件事,然后再继续讨论。
3.3 服务一旦有变,查找过期的模拟对象
- 测试策略的缺陷是导致问题出现的一个原因。
- 不做全面的测试就升级一个很重要的库是非常危险的。于是,你抽查了应用的验收测试情况,发现对身份验证的测试还是比较全面的——验证成功和验证失败都测试过。
- 如果你在抽查测试覆盖情况的时候再深入一点,肯定会有好处。模拟对象是在底层搭建的,因此有没有模拟,对验收测试结果的影响都微乎其微。这一点很容易让人忘掉模拟对象的存在。
- 在升级了库而没有发现任何故障后,你又手动进行了测试,确认事情如你所想,预是你就想当然地认为一切正常了。自动化测试能够捕获到库接口中的所有以外变更,手动测试则智能验证服务是正常的。
- 现场测试非常重要。
3.4 遭遇烂代码,维护必头疼
- 在开放的互联网环境中,你需要担心的不只是自己集成的服务。有时,会有人不请自来地把你和他们自己集成到一起。
- 这个问题其实应该归结于你们的经验不足,你们俩都见过爬虫做一些奇怪的事,但从来没见过它们对服务产生负面影响。
- 在处理紧急事件时,交流顺畅那个能够决定成败。
3.5 不存在纯粹的内部问题
- 为发现的所有问题都添加回归测试,无论问题有多小。这一点很重要。
- 务必检查测试配置中的模拟对象,以免测试结果让你错误地认为模拟的API客户都安仍能正常工作。
- 让异常报告系统和客户通知系统共享一套邮件投递机制,是有风险的。
- 异常报告系统最好能把相似的问题合成一个报告,而不是针对每个问题都法一条警告信息。
- 在维护过程中,本意是好的,但往往被误导了。
- 只要是客户遇到的问题,无论多小,都应该作为首要问题去解决。但是我们在这上面投入的精力过多,导致对一些内部的质量问题和稳定性问题考虑不足。
- 也许根本不存在存粹的内部问题。只要我们的代码面向外界、与外界交互,当它出现问题时,就有可能对客户产生影响。如果我们多关注一下系统与外界的交互问题,重视每一个小问题,可能就会有更好的结果。
忠告与提醒
- 当你的应用依赖于你不甚了解的外部服务时,一定要加倍小心。如果找不到和你的用法相似的陈工例子,就要提高警惕;往好的方面说,可能没有人用过该服务解决与你的需求类似的问题;往坏的方面说,该服务可能根本就不适合你。
- 谨记库和服务之间最关键的不同点:只有在代码库或基础设施发生改变时,库才会引起显著变更;而外部服务服饰都可能让系统行为发生改变,甚至导致系统崩溃。
- 只要服务依赖发生变更,就要在测试中密切关注是否有模拟对象过期。为了防止被测试结果舞蹈,一定要确保针对你锁依赖的真实服务运行一些测试。
- 认真对待每次代码复查,把它当作对所依赖服务的一次小规模审查,比如评估测试策略、考虑如何处理故障,或者思考如何防止错用资源。
《代码之外的功夫:程序员精进之路》
第4章 设计严密方案,逐步解决问题
- 假设在最近几个月里,你正在知道一位刚刚进入软件开发行业的朋友。
- 在遇到定义明确的任务时做得不错,而在遇到包含很多细节,需要自己一一理清再解决的问题时,就有点不知所措了。
- 编程题一般画蛇添足地将实现细节复杂化,故意把数据表示得很麻烦,而且再完全理清其规则之前,是很难解出来的。这就意味着,靠做题很难训练编程实战能力,但编程题很适合用于探索通用的问题解决技巧。
4.1 收集事实,清晰描述
- 排除早升信息,找到问题的核心,是在遇到任何复杂问题时都应该首先做的工作。
- 在此之前,题干只是一大堆毫无意义的细节,输入文件格式混乱,让人一知半解。而现在,头脑中已经有一个清晰的解题目标,也对达成这一目标的方法有了初步的思路。她已经能从全新的角度看待此题了。
4.2 写代码之前手动解决部分问题
- 手动绘制了一张表,表中列出了每个玩家手中牌的状态。
- 仔细查看这张表之后,此题背后的逻辑变得更清晰了。
- 手动推敲只是为了对此问题的大框架有一个大致的了解。在具体实现代码细节时遇到一些很难解决的问题实属正常。
- 逐步找出了另外几个手动推敲时不容易发现的极端情况。终于,代码在独立测试时正常了,于是把第一个样本数据集中的游戏记录手动转换为自己的模型中的函数调用。
4.3 核实输入数据,随后进行处理
- 这个文件和刚才那个在格式和布局上略有不同。于是,她重新看了看问题描述,并阅读自己当时的笔记,以确定这个样本数据集的用途。
- 为了验证自己的假设,把每个分支中的出牌动作说明完全展开。理论上讲,需要做的无非就是将动作与每个分支中的动作对应起来。但实际上,并不是所有的分hi都使用了正确的格式。
- 养成好习惯,否则很容易写出“垃圾进、垃圾出”的程序。
- 只要你找到一个格式不正确的分支,就能知道需要检查所有分支的格式。发现这一问题只需要几分钟,但磨刀不误砍柴功,这时间花得很值。
- 当然在默写情况下假设数据格式正确时没问题的,但如果你不能确定,最好还是谨慎一点。
4.4 善用演绎推理,检验工作质量
- 从逻辑上判断,解题进行到这里,画一个草图来表示再合适不过。
- 无论如何都要验证一下。
4.5 欲解复杂问题,先知简单情况
- 可以着手解决主问题了。但最困难的部分是,如何将这些零散的想法串起来,处理一个每轮包含很多分支的完整游戏。
- 搞清楚此题的这一细节后,还需要考虑很多问题。
- 制作的该题的简化版,以便帮助理解应该怎样进行后续工作。
忠告与提醒
- 描述问题的原始资料一般都是零散的语句、示例和参考资料。为了理解这些资料,你需要记笔记,排除噪声信息,只留下最关键的细节。
- 每个问题的背后都有一推简单的子问题,你早已知道如何解决它们。要将问题不断拆分,直到你能辨认出构成它的子问题为止。
- 难题由很多活动部分组成。先观察各个部分如何联系在一起,而不要管具体的实现细节。在写代码之前,先用纸币解决部分问题。
- 在无效数据集上运用有效规则可能会得到难以调试的混乱结果。不要假设输入数据集的格式正确。在处理任何数据集之前,都要预先检查,以避免出现“垃圾进、垃圾出”的情况。
《代码之外的功夫:程序员精进之路》
第5章 谨记自底向上,优化软件设计
- 假设你是一门软件设计课程的客座讲师,并且你希望缩小理论与实践的差距。
- 大部分学生并没有构建软件系统的实战经验。这使得学生把软件设计看成是抽象的练习,而不是具象且必要的技能。
- 课本上的例子强化了自顶向下的软件设计方式,其中的设计理念出现得很突兀。真正的设计不同于此,但是学生经常照葫芦画瓢,进而产生气馁情绪。
- 学习自底向上的软件设计方法,并权衡利弊。