摘要:在一次迭代开始时,各项需求看上去安排的张弛有度,但仍无法避免在某个时间承受巨大的进度压力,当你发现必须在干得快和干得好之间选择时候,一般都会选择干得快,并提醒自己将来再回头来返工优化。但实际上下一轮迭代总会有下一个问题,将来永不会来。这类久拖不决的任务就是技术
01
对待技术债务要谨慎
在一次迭代开始时,各项需求看上去安排的张弛有度,但仍无法避免在某个时间承受巨大的进度压力,当你发现必须在 干得快 和 干得好 之间选择时候,一般都会选择 干得快,并提醒自己将来再回头来返工优化。但实际上下一轮迭代总会有下一个问题,将来永不会来。这类久拖不决的任务就是 技术债 ,技术债 就像贷款,短期能获得好处,但你总要付出利息。时间久了,利息也将是一笔难以偿还的债务。因为有债务的存在,新功能的加入将变得困难,而且这些债务也将是bug 的滋生地。
我们很容易说服自己,当前变更引入的一点点技术债并不算什么大问题。就跟破窗效应一样,只要放开了口子,系统会腐化的很快。变坏容易,变好很难。
尽量避免拖欠技术债务,应该要采取零容忍策略。如果迫不得已,也要在任务卡片上追踪这笔债务,及时偿还,不要遗忘。
02
分析需求背后的意义
假如用户提出一个需求:设计一架飞行速度在2.5马赫(2.5倍音速)的低成本战斗机。
因为风阻和速度的平方成正比,从1马赫到2马赫,需要的动力和机身设计有更严苛的要求。但通过追问为什么需要这种类型的战机时,得到的答案是:为了快速撤离战场,因为用户想利用速度优势撤离战场。
了解需求之后,可以有更好的方案:通过提升推力重量比,改善战机的加速性能和机动变向性能,用灵巧性取代了对速度的需求。
我们首次遇到的用户需求可能只是用户/产品从自己角度想到的一个方案,可能并不是最佳的。可以通过分析用户需求的真正意义,定位真正的问题,尝试提出比用户的建议更好、成本更低的方案。而了解真正需求的办法也很简单:面对用户,不断问为什么。如果用户(也可能是产品)并不能回答你的问题,那么你可以想一下是否需要 Say NO。
03
美在于简单
有些东西是我们在代码中尽力争取的,比如:
可读性可维护性、扩展性开发速度“难以捉摸”的美的追求对 美 我们都有自己的判断,但 简单 一定是它的内在品质。一个应用或者系统无论多么复杂,其中每个单独的组成部分都保持着它的简洁性:简单的模块承担了单一的职责,它包含着同样简单的方法,而且方法的功能如同其名称描述的一样,简单且直接。
不仅每一个组成部分本身的职责要简单,而且与系统其它部分也应该保持着简单的关系。美来自于简单,亦存在于简单。有时因为问题本身很复杂,实现自然也会是复杂的,那就把这些复杂性封装到模块内部,使得模块之间的交互是简单的。
比如文件系统是复杂的,但是作为封装的模块,对外暴露的只有 open read write lseek close 几个方法。所以这是一个好的设计。如果非要选择,模块拥有一个简单的接口比拥有一个简单的实现更重要。
对问题最好的解决方案,就是能够满足该问题的所有需求且最为简单的解决方案。如何衡量简单呢?
是否能快速读懂。是否易于创建,不只是代码敲出来,而是 debug 并写对的时间,复杂的代码通常需要好一阵才能写对。代码的数量。新概念的数量。简单不是简化,不要把问题想简单了。比如“爬楼梯”算法,递归确实简单,但是当台阶级数比较多时,递归耗时太久,并不是一个好的方案。
更进一步,有时需要简化的是问题而不是方案,当问题难以解决的时候,回头看看这个问题是否合理。比如上面提到的2.5马赫的战机。还比如你还没学会使用动态规划或者其他算法解决“爬楼梯”,再回到这个问题来看,现实中台阶数会不会很大呢?并不会,十几级台阶而已,如果计算过程不是在关键路径上,递归看起来又不是不可接受。问题本身是不是个有效问题,远比采用什么方案更重要。
控制复杂度,是所有编程原则的总则。编程工作的核心就是与复杂度较量。添加功能会让代码复杂,代码复杂,使用起来就困难,项目进展就会变慢。要写好代码,就要尽量拖延代码变复杂的时机,如果非这么做不可的时候再写。即使要写也要尽量消除重复,设法调整架构,使得新添加的功能不会令系统的整体复杂度大幅上升,这才是编程中最难的部分。
思从深而行从简
04
少即是多
Less is More,虽然是一句老掉牙的格言,但也确实是至理名言。
不要过度实现,不要预计将来会用到,不要觉得这里性能可能会有问题而提早优化,不要做现在不需要但你觉得将来需要的工作。你编写的代码,只应该解决目前确实存在的这些问题,而不是去解决你正在设想的问题,因为你永远不知道接下来会遇到什么问题。
系统设计应该保留可扩展性,扩展性更多的是在接口扩展性设计/外部依赖的可扩展性等设计层面,这些一般都不需要太早的代码实现。比如接口层面保留引入本地缓存的能力,但先不要写本地缓存的实现代码,因为会增加数据一致性的复杂度和代码复杂度,等以后需要了再添加即可。
最好的代码是不能再删减一行的代码,哪怕是注释。
05
时刻想着删减代码
删除一行代码带给我的成就感要多于增加一行代码。代码是负债,而不是资产,对于增加代码我们要吝啬,用新增代码量来衡量工作是愚蠢的。新增的代码都需要维护,而且代码都可能会出错,通过删减代码来提升代码质量也是一种尝试,能用8行代码实现的就不要用10行。
我们在局部或服务整体重构之后,老代码没有下掉,意味着运维责任多了一倍,这都是负债,我们并没有比重构前变得更轻松。当然并不是只有重构才能下线代码,冗余的逻辑、无效的代码、不再使用的接口功能等等都是可以删减的代码;实在不济,去掉一些无效或错误的注释也是一份功劳。
运行不到的代码是不管用的,称为死代码。如果发现某段代码已经没人用了,而且可以安全的删掉,这应该是个值得高兴的事情,应该比你新写代码更值得高兴,因为你在不影响功能下缩减了代码量,减少了维护成本。
当然,删减代码并不会被纳入 OKR 目标中,但正如运动能够减脂增肌一样,这就是对程序员而言难而正确的事。
06
故障终究会发生
机械硬盘的平均无故障工作时间是3年,也就是说有1000多块硬盘的机房每天都会有故障发生。硬件会出错,于是增加冗余资源提升系统可靠性,虽然可以避免单点故障导致的错误,但更多的硬件会有更大的概率故障。
软件也会出错,于是增加额外的监控程序,但监控程序也是软件,一样会出错。
人无完人,我们也会犯错,因此我们把操作和处理都变成自动化。自动化虽然降低了主动犯错的概率,却增加了错误被忽略的概率。何况任何自动化应对变化的能力远不及人类,于是又为自动化增加监控,结果是更多的软件,更多的故障率。
所以我们不得不承认:系统中必然存在不同形式的故障隐患,无论如何都无法彻底消灭。只有承认这一点,我们才能对特定的故障设计对策,就比如汽车工程师知道交通事故无法避免,所以设计了 安全带、气囊、撞击缓冲区等来保护乘客。因此你的系统也应该设计预防措施来限制故障,可能是限流、熔断、降级、异常上报,甚至是数据恢复脚本。
不应该惧怕故障,也不应该像鸵鸟把头插进沙子里一样对故障视而不见。
07
不要忽略那个错误
1. 一条错误日志
2. 用户反馈的一个不符合预期的Case
3. 代码中对错误的处理
4. 数据库中的数据不符合预期
如果你选择忽略类似上面的一个错误,对其视而不见,假装一切都没发生,那你就是在背负巨大的风险。不顾红灯亮起,继续前行,结果只会招致更大的损失,要在时机初现的时候就动手,把损失减少到最小。
编译器的警告不要忽略,codecc 的警告不要忽略,对每个调用后的错误检查不要省略。错误才是常态,不要忽略任何错误,因为一切都正常实在是小概率事件。
08
问题要追踪到根本原因
不是稳定复现的,重试没问题就忽略了。看板数据有一些异常波动,查起来麻烦,反馈一下得了。不是我服务的问题,客户端或者外部系统的问题,先不管了。QA 反馈了个 bug,在代码里加了个 if 判断处理了一下就给放过了。等等很多只解决了问题表面,而没有根本解决问题。多问“为什么”?直到查明真正的原因。计算机不会撒谎,也不会骗你,所有不符合你预期的行为都值得追查一下,要不就是程序或数据哪里错了,要不就是你的预期错了。不管修正哪一个错误,都是有意义的。
比如最近在业务上,用户反馈购买会员后视频试看结束仍然出蒙层无法播放,分析发现是客户端从后端没有获取到购买成功的信息而拦截,但客户端并没有请求发出和响应的日志。一切都很诡异,如果选择让用户重装一下也是能恢复的。但如果继续追查原因,会发现是客户端对登录态判断逻辑有问题,导致没有发请求。所以用户不能看的根本原因是登录态判断有问题。但凡中间断了没有追查到底,都不能揪出根因。
如果过往经历中没有抓耳挠腮、茶饭不思的追查过几个问题,是有点不完整的。只有在追查根本原因的过程中,才能学到东西并且印象深刻。
09
营地法则:要让离开时的营地比进入时干净
努力留下一个比你发现时更好的世界 - Robert
当你发现地上有脏东西,不管是谁造成的,都要把它清理干净。提交代码时同样如此,只需要 commit 提交时好于 checkout 时即可,你可能仅仅改进了一些变量命名的可读性,或者把长函数拆分成两个短函数;你可以是打破了一个循环依赖,或者增加了一个接口解耦策略和实现细节。
做的事情可大可小,但是要变好。
具体可实施的小步改进可参照《重构》
10
多问自己:用户会怎么做?
外网用户没有程序员的意识和能力来解决问题,也不会认可程序员使用的界面模式和提示信息;因此你在界面底部的一次错误提示,用户可能根本就看不见;同时,用户也不会按下 F12 看一下接口调用是否正常。
要想知道用户的真实想法,最好就是观察一个用户的完整操作。给他一个 APP,看他完整的操作轨迹。你要一直问自己:为什么他要那样做?为什么她不那样做?
不要用用户访谈代替观察用户,因为他们访谈的表述和操作之间会有巨大差异。用户在使用你的应用时,在你看来就是个“傻子”。直接观察用户是获取需求的最佳途径,用户到底想要什么?与其闷头猜测,不如花一小时去仔细观察。
敬畏外网用户的反馈,一条外网反馈意味着1000个用户遇到了问题。不要无视他们,他们使用并乐于反馈问题,我们要珍惜并积极响应。每一次用户反馈问题的解决,都是提升用户满意度的最有效举措。
如果你在滴滴就去当一名司机体验几天,看一下接送流程是否有优化空间。如果在美团就当一名骑手,加入线下骑手团,问一下骑手们接送单过程是否有问题。如果你不是一名游戏玩家,你就不知道这个游戏做的到底怎么样。
当然,也不可能所有人都去当司机或者抓人过来搞测试,但是总有其他办法:
比如滴滴建设了“听音工具”,让一线研发可以去听客服解决用户进线的录音,知道用户会有哪些问题,客服是如何解决的。
有千万用户的应用都会建设一个好的埋点系统,好好分析用户点击轨迹行为。这样的边际成本为0,而且还很客观。
11
设计两次
软件设计不是一件简单的事情,所以你对如何构建模块的第一个想法不太会是一个好的设计。多做一个备选方案,对比它们,一定会对你有所启发。尝试选择那些彼此截然不同的方法,这样你会学到更多。当然,最重要还是要考虑上层软件的易用性。
对比多个方案,挑选最佳方案,或者整合推动产生最佳方案。多否定自己,多从使用者角度考虑是否合理。设计两次并不会浪费太多时间,设计上浪费的两个小时,相比实现需要的两周而言,不值一提。今天设计好之后,先放在那,切换一下头脑,等待灵光一现看是否有更合理的方案。
但我们大家都是聪明人,我们会觉得没有必要设计两次,因为第一次的设计就是天才的杰作。但软件设计是一件很难的事情,没有人第一次就做对,所以:尝试设计两次。
12
对代码审查好一点
你可能不喜欢代码审查Code Review,就跟不喜欢让别人看到你内裤的颜色一样。但我们应该知道,代码审查不仅仅是更正代码错误,其目的还是共享知识、建立统一的代码指导标准。代码审查的时候态度要温和,确保评语是有建设性的,不是刻薄的。同样,对评审意见的回复也不应该是“设计如此”/“这次先这样”等无意义的回复。
应该月度组织一场“代码审查会议”,在会议上畅谈“代码为什么这样写”和“怎么写可以更好”,可以针对一个命名、一个范式、一个循环判断、单测是否有效、数据库设计是否合理、领域模型是否正确、代码分层和模块划分是否合适等等问题充分讨论。如果这个过程中有点心和饮料,那代码审查的过程会充满乐趣,并且值得期待。
13
良好的命名就是最佳的文档
编程无非就是在对26个字母排列组合,如何能让别人读懂你的代码,除去更大范围的模式设计之外,在一个屏幕可见范围内,良好的变量和函数命名是最有助于让读者看懂你代码的实现。
不要写注释:如果必须添加注释才能说明一段代码的含义,那就拆分成多个子函数,用函数名来表述注释要表达的含义。不要为了少打几个字母而缩减名称。 x y xr rn 等,除非它的作用域只有两三行。不要将多种命名习惯混用,要保持一致,比如昵称不要混用 nick 和 name,在全场景使用一个就好; 另外,符合惯用法,比如 empty 方法,一般场景用于判断 是否为空,那么就不要给这个方法再赋予清空该容器的动词含义。词要达意,存储名称的集合,不要叫 nameList,直接 names 就好。尽量不要使用 itemData, userInfo 等含义很广,必需要深入看实现才能懂的命名。名称的长短和名称的作用域成正比。短循环内局部变量就用短命名。整个函数声明周期的命名就要长一些。命名最重要的就是精确性和一致性。不要过于笼统或者模糊。对重复使用的概念,保持一致的命名。不要一会儿 title 一会儿 name
对变量的命名要像给自己孩子起名一样慎重,好的名字十分重要,但往往并非唾手可得。
14
要写注释
注释的目的是补充代码所不能表达的意图。如果能通过调整代码结构、重命名函数和变量就能解释清楚的就不要写注释。代码应该能向下一个接替我的程序员解释自己,注释并非总是邪恶的,它们和代码结构一样重要。在代码顶部写上注释,用来说明代码要完成什么事情;在代码内部加上注释,可以让其他开发人员知晓如何修复或扩展这些代码。对复杂项目来说,通过阅读代码来了解其行为是不现实的, 注释是快速了解项目的入口。
设身处地的为以后的读者或者15分钟前的自己想一想:你希望知道什么?
注释主要将代码中不明显的东西揭示给读者:
不要写行内注释,使用有意义的命名来代替行内注释。不要为显而易见的代码添加注释不要注释旧代码,直接删掉它不要写在做什么,而要写为什么。比如这个注释就能解决读者疑惑:由于需要尽可能减少网络请求的次数,代码处理变得复杂了。审阅代码时,如果你发现一段逻辑理解起来很吃力,而代码本身也没有太多优化空间,请不要迟疑,勇敢表达出你对于注释的需求吧!
15
早部署,常部署
软件开发中,部署和调试我们往往会被放在项目的最后去做,前面的步骤我们都在假定一切都是正常的。协议会按照约定实现,正在做的功能就是用户想要的,只需要等待最后时刻全部拿出来,一定能惊艳众人。
但是真到了交付的时候会发现问题很多:协议和预期的不一致、产品说这个功能不是Ta想要的。这个时候不是陷入需求撕扯中,要不就是加班加点的修改。何其痛苦。这时候不如把开发过程切开一下,完成一些功能之后我们就部署联调一次,这会确保我们已经完成的工作是有效而且可控的。
早部署、早交付、常部署、常交付,越早部署就越能发现问题,不要堆到最后一把梭哈。
16
不要怕搞砸
你可能有过这样的经历:
基础代码相当不稳定;系统结构差,更改一样东西总会破坏另一个不相关的功能;无论增加什么功能,编码人员的目标都是尽可能少的改动,每次到了发布时间,都提心吊胆;耦合严重,很难找到修改切入点。每次的修改都会影响到老功能;任何作出系统更改的理由都如此痛苦。因为系统病了,它需要一个医生。你知道系统出了问题,但是你会担心你的优化修改是否会引入新的问题,你会秉持 能用就行 的原则得过且过。这个时候请大胆一些,不要怕搞砸,就像手术一样,为了治疗疾病总是需要几道切口。切口会愈合,系统也是这样。花在重构/优化上的时间能够在项目生命周期里得到数倍的回报。
重构过程中,你需要重新定义内部接口、重新设计模块,以及采用减少依赖的方式简化代码。通过这些工作,可以显著降低代码的复杂度。
同时保持 测试完备,重构的前提是你有测试用例证明你的重构是安全的,不管是回归测试还是 diff 测试,这些重构的改动都应该在可控范围内。
不要犹豫,如果你觉得系统需要优化,不管大小,请勇敢前行。
17
决定重构之前,先想想这能不能比原来好一倍
上一条我们鼓励优化重构,是的,这一条却希望你慎重,这很矛盾
简单来看,程序员分为两类:
喜欢渐进的思考,总是基于现有方案来思考每一个问题,通过调整现有方案来解决新的问题总喜欢用一个系统解决所有问题,而不只是当前这个问题,因此,只要有机会,他们就想从头设计方案这两种倾向走向极端,都会造成灾难。如果所有修复都是小修小补,那你会将自己困住,无法从更大维度思考问题,他们的说辞总是“不可能”、“时间不够”等。反之,如果所有方案都需要从头开始,仅凭一个单独的用例,就决定重做整个系统那你会将前一套架构带给你的知识丢掉,陷入原地转圈/不断重构的过程,这类同学只从解决方案出发,来论证需要重构,而没有能够从当前需要解决的问题出发。凡事不从问题出发的提议,都是值得怀疑的。
如何决定选择小的优化修复还是重构呢?那就是:重构/优化之后能不能比原来好一倍
研发效能是不是高一倍?
系统性能是否高一倍?
问题反馈环比下降是否能好一倍?
之前因架构不能支持的功能,是否可以支持到?(从这个角度看,不只是好一倍,从零到一是好了无数倍)
好一倍是对必须有大幅改变才值得大幅变更的粗略量化描述,不要因为好一点就换掉现有东西,只有在强很多的情况下才值得去做。
当我们不知道要还是不要的时候,应该果断的选择不要,至少是暂时不要,直到我们清楚的知道为什么要。
18
不要重复你自己
很经典的 "DRY"原则 ,它可能是所有编程原则里最根本的原则之一。
重复就是浪费,每一行代码都会被维护到,而且可能是未来 buy 的潜在来源,重复的代码会带来代码库的膨胀,增加了复杂度,维护成本也会增加。DRY 的要求是 在一个系统内,每一块知识必须有一个单一的、明确的、权威的表示。
将重复的过程调用自动化, 人工测试很慢,所以需要自动化测试。代码集成部署手工完成耗时且容易出错,所以有了流水线。但凡需要重复多次的工作,都需要自动化。如果经常需要处理相同格式的文本,就写一个包含 cut grep sed awk 的脚本帮你完成工作。
重复不仅仅指代码的重复,即使代码不同但实现了同样的功能,或者不同代码在描述同一个领域模型概念,都是重复。
19
没有高手神话,请把问题描述清楚
如果有人问你这样一个问题:
1) 我碰到一个XYZ异常,你知道问题出在哪里吗?
2) 创作者不能发文了,帮忙看一下吧?
3) 群里发你一个没有上下文的截图,或者群里@你,帮忙看一下?
上面的问题反馈还是一个完整的句子,有的同学在提问问题的时候都没有断句和标点,甚至有错别字和倒装句。遇到这种情况真的是苦不堪言。
你一定会满头问号,心里怒骂:没有堆栈信息、没有错误日志、没有信息上下文、没有 trace 链路信息,没有对比截图,仅凭一句话就寻求你的帮助,希望你能看懂并且帮助解决问题。对方真是把你当成神仙高手了。
提出问题的人可能更适合回答问题,因为他有更多的信息上下文。所以在对别人提出问题的时候,请把问题使用对方能理解的词汇和方式描述清楚,并且把上下文也要描述清楚。
总之一句话:会问问题。
20
学习新语言
我们假设一种场景:
业务同学讲了十分钟,提出一个需求,期望能对产品做一个改进,进而吸引更多的用户使用业务人员在讲的过程中,技术同学在草纸上画起了神秘的符号业务同学讲完之后,一脸期待的看着技术同学技术同学低声讨论完之后,走到白板旁,一边画着表示多个系统视图的复杂图形,一边以复杂的技术术语解释为何没有对系统做出重大升级之前无法支持这个需求业务人员很震惊,如此简单的需求竟然需要如此巨大的改变技术同学也很诧异,业务同学竟然没理解他刚绘制的架构改动才能适配需求问题就在这里,没有一方能够理解另一方的意思,也不理解对方言论中的一些词语。相比和自己不同的人,和与自己类似的人相处会更舒服,这是基本的心理学规律。
新语言并不特指明编程语言,更多的是不同的领域概念。
想象一下,如果技术同学能够以业务同学可理解的术语向其解释其中的问题,业务同学以技术能够理解的术语解释业务上的问题,上述情况将会有改观。
同样的道理,如果我们和交易同学有合作就应该去了解交易系统的概念和规则。如果和推荐系统合作就该去了解一下召回/倒排等概念,买一本推荐系统的书从头看一下,可以不求甚解。如果和会计系统合作就应该了解“有借必有贷,借贷必相等”,甚至可以去考一些会计基础证书。
问出好问题,很重要。只有先让对方听懂你的问题,你才能有好答案。不要沉浸在自己的世界中和别人沟通,要使用别人能理解的词汇去描述或回答问题,以及描述问题的时候携带必要的上下文,尽量给对方提供舒服的交流环境。
学了新的语言,才能够使用别人能听懂的语言沟通,这样提出的问题才能得到更好的解答
21
加班加点,事倍功半
专业编程不像几公里的全力奔跑,目标就如宽阔马路的尽头一样清晰可见。而更像黑暗中进行的马拉松,你只有一张粗略的地图。如果蒙头奔着一个方向跑下去,则不太可能成功。你需要保持一个可接受的步速,当你对自己所处的位置有了更多了解之后,就调整你的行程。
同时,你还需要时间来持续学习,让你的专业领域知识保持最新。因此,你不能把你的晚上、周末、假期都用在加班上。对工作的准备和持续学习才是职业生涯的核心部分。
工作要像一个专业人员一样:充分准备、付诸实施、观察、反省和改变。专注于你的项目,竭尽所能找到灵活的解决方案,提高你的技能,反省思考你正在做的事情,然后调整自己的行为,不要把所有精力都放在全力奔跑上。
放下鼠标,离开键盘,去走一走,让大脑做一些与创造性相关的事情,听听音乐,休息一下。只有当你离开电脑之后,你才会发现你解决问题的第一次尝试并不是解决问题的最佳方案。这也是设计两次的最佳时机。
在开发项目时,你会对问题领域了解的越来越多,并有望找到更高效的途径。为了避免无效工作,你必须有足够的时间去观察你手头上的工作,思考你所看到的情况,并随时调整你的行为
22
数据是核心
从站在更远的角度来看,系统一般都是操作数据的工具而已。无论代码量级有多少,处于核心地位的永远是数据。而且相对于繁杂的代码,把注意力集中到底层小的多的基本数据集上,会更容易一些。数据要比代码更加精炼,也更好理解。要想读懂程序,最为困难的就是厘清哪些数据之间是相互关联的。
就如同代码的优化升级都还好做一些,但涉及到数据结构的变化,解决起来就会困难得多,要考虑双写、同步、迁移、一致性。数据构成了系统的核心,数据是系统中最重要的部分,代码可以是一坨便便,但数据一定要清晰且合理。所以对数据的设计要给予足够的重视。
1. 系统设计时优先考虑数据模型的设计
2. 上手新服务时优先查看数据存储的实现。这时候如果是非结构化存储,会有理解成本。
23
根据投资回报率(ROI)进行决策
测试是一件 ROI 很高的事情,投入4个小时时间编写测试用例。却可以省下8小时的问题排查时间,投资回报率高达 200%。
如果发布上架对投资方是至关重要的,那么花费半年才能得到的完美系统相比于花费2个月的 MVP 版本”,ROI 就要低很多。发布了能用的版本,根据用户反馈适当调整,更能活得下去。
系统的升级改造,主要是时间投入和预计收益是否能适当。即使一个系统架构不佳,但是修改频率很低,那么就可以先放置在那里,不去管他。先把时间投入其他重要事情上,如果没有其他事情可做,那另当别论。
24
拉伸关键维度,发现设计中的不足
我们的设计一般都是基于当前需求、当前熟悉的技术、预期的数据量和当前性能要求等实现的。
在设计的时候,我们可以来拉伸解决方案中的关键维度,看看哪些方面会遭到破坏。比如,如果用户量越来越多,当前系统是否能承载;如果每天处理的交易数据量越来越多,是当前的十倍百倍会如何;如果数据必须要保存6个月而非一周时,当前存储是否满足;通过这些手段,这样我们就能发现设计的不足。
我们暂且简称其为无限法,即尝试系统中的关键维度扩展到无限大的时候,会发生什么。
设计归设计,实现归实现。当前数据量少,我们可以用 mysql 实现检索,但是要保留以后检索维度或数据增多之后替换成 ES 或者搜索中台的能力。对 ES 操作的代码在这次实现中可能一行都不会写,但是要保证以后对于扩展来说是易于添加/切换的。
这就涉及到对好代码(好设计)的标准,就是:是否能轻而易举的修改它。主要有两点:
可读性:首先要读懂,才能谈修改。可扩展性:能够轻易找到修改点,快速做出修改。并且基于开闭原则,只增加而不修改原有逻辑才能避免引入错误。25
编码标准的自动化
研发流程中引入 CI/CD 流水线,代码不符合规范就要中止流程,测试覆盖度不够或者测试用例失败都要中止流水线。代码检查和发布上线都使用流水线完成,避免人为犯错的机会。
越早发现 bug,修复成本越低。因此我们希望在编码阶段就能发现 bug,发现 bug 的方式就是自动化测试,可能是单测,也可能是接口测试。
要想实现这个目标,需要:
非侵入式的测试框架。一套能够与测试相协调的部署系统。团队成员乐于执行自动化测试。编写易于测试的代码 - 无状态的代码更易于测试。让所有的线上变更都走流程,即使你认为加行日志不会有问题,但是也可能会因为空指针而 core dump
26
靠谱和信用是我们的资产
工作就是树立个人影响力的过程,不管是你写的代码多牛逼,解决了多复杂的问题,这是一方面。但总还是有一些软素质直接影响评价和定位。比如:
问题的解决者和终结者:只会抱怨还是低头去解决问题事事有回应,不管办不办得成:不丢消息不只是对系统的要求,也是对个人的要求不要有太强自尊心,别人跟你你说:你这个实现有问题。第一反应不要是反驳如果不能如期履行承诺,请尽快告知受影响的人,以便对方调整计划。完不成不是一件丢人的事情。把事做完:系统重构新功能上线,老服务没下掉是否是完成?27
应该了解设计模式的原则
这些原则耳熟能详,但是是否做到了呢。过上几个月问一下自己。
SOLID 原则:单一职责、开闭原则、里氏替换、接口隔离、依赖倒置。KISS 原则:Keep it Simple and Stupid、Keep it Short and Simple、 Keep it Simple and Straightforward。DRY: Dont Repeat Yourself。LoD 原则: 最少依赖。28
应该了解设计模式的原则
模块原则:使用简洁的接口拼合简单的部件,每个模块做好一件事儿。清晰原则:清晰胜于机巧,代码首先是给人看的,其次才是给机器执行。组合原则:设计时考虑拼接组合,类似于 把每个程序都写成过滤器:和 shell 管道一样,一个函数的输出可以作为另一个函数的输入。能组合就不要耦合。分离原则:策略同机制分离,接口同引擎分离。简洁原则:设计要简洁,复杂度能低则低。吝啬原则:除非确无它法,不要编写庞大的程序。避免不必要的代码和逻辑,保持精简。透明性原则:设计要可见,以便审查和调试。健壮原则:健壮源于透明与简洁。表示原则:把知识叠入数据以求逻辑质朴而健壮。代码的复杂度应该在数据中,而不是代码中。通俗原则:接口设计避免标新立异。“+”应该永远表示加法,而不是除法。缄默原则:如果程序没什么好说的,就保持沉默。补救原则:出现异常时,马上退出并给出足量错误信息。经济原则:宁花机器一分,不花程序员一秒。生成原则:避免手撕, 尽量编写程序去生成程序。优化原则:雕琢前先要有原型,跑之前先学会走。扩展原则:设计着眼未来,未来总比预想来得快。29
在责备别人之前先检查自己的代码
开发人员通常难以相信自己的代码会出错,绝不可能出错,即使出错了,也必定是编译器出问题了。
如果我们使用的工具/库/框架是被广泛使用的,那就几乎没有理由怀疑它会出错。如果发生了不符合预期的表现,那就首先怀疑自己的代码:用桩代码进行调用调试,围绕可疑代码编写测试用例,检查版本库版本号,检查配置是否生效等等。
如果其他人报告说有问题,而你无法复现的时候,那就走过去看看他们到底是怎么使用的。他们可能进行了你未曾预料的操作,或者采用了不同的操作次序。排除掉一切不可能之后,剩下的即使多么不可能,即使你的代码看起来没问题,也只能是真相。
30
在责备别人之前先检查自己的代码
先看如下代码:
Map> portfolioIdsByTraderIdIf portfolioIdsByTraderId.get(trader.getId).containsKey(portfolio.getId)){ …}看起来像是从 trader中取id,然后用它从一个….,最后看起来是判断protfolio的id是否存在于这个map中,那portfolioIdsByTraderId 是什么结构呢?奥,它是个嵌套map...
从另一份代码中,你看到的是这样:
If trader.canView(portfolio) {…}这种情况下就不用挠头,也不需要知道它内部到底嵌套了几层 map,因为那是 trader 的事情,与你无关。
我们努力的方向就是将现实世界映射到有限的数据结构中,这就需要用户自定义类型,如果你的代码中包含类似 trader portfolio 这样的概念,你就可以使用他们的名称来建模,而不是使用基本数据类型复杂嵌套表述你的目的。不要有基本类型偏执,不要固执的非要使用基本类型(如 map array int等).
否则你怎么知道这个 int 代表了 trader,那个 int 代表了 portfolio,这就是在用一种加密的方式在编码,除了你自己,谁都看不懂;可能明天你自己也就看不懂了。
31
最好的注释是软件设计架构文档
前面有两条我们提到,又要好好写注释,又不要写注释。因为注释和注释是不一样的,有意义的注释永不嫌多。从另一个角度看,代码里的注释都是补丁,是碎片化的,局部的注释很难推导模块整体的顺序及耦合关系。整个项目思路的重要性远甚于穿插在代码中的几行注释。
文档建设和技术沉淀比我们想象的还重要,刨除交接困难的影响以外,没有文档建设的项目一般在质量和架构上也会是一团乱麻。就跟没有地图的路径规划一样,没有文档建设的系统发展也会极度追求短期效益,最终走向混乱。
好好写代码注释,说明你是一个不错的程序员。能够好好写架构设计文档,把各个模块交代清楚,同时代码内也能好好写注释,那应该可以“配享太庙”。
32
越早写单元测试越好
测试既可以是狭义的单元测试,也可以是广义上的功能回归测试。除了单测对质量的保障之外,单元测试还能够帮助改善设计,因为不好测试的代码一般意味着代码结构不好; 可能是引用了全局变量、也没有使用依赖注入导致难以 mock、也可能是没有针对接口编程;函数内部随意调用 time.Now 导致测试用例不稳定等等。
问题越早被发现,解决的成本越低。单元测试是最早能发现 bug 的手段,同时测试带来的边际成本是很低的,只要写一次,就可以永远为你保驾护航。
33
没有什么能够阻止项目成为屎山
你的代码会随着新功能的增加,以及新技术组件的引入,技术债务会逐步推迟并积压,代码也一定会腐化,因此没有什么办法能够阻止项目腐化,最终也一定会变为我们曾经厌恶的屎山。不如我们妥协一些,接受它最终会变烂,只不过要让他腐化的慢一些、再慢一些。以及做好故障隔离,即使部分功能出了问题能够限制影响范围,不要雪崩。
让服务稳定的跑起来,支撑业务迭代的时间久一些,即使出了问题也能及时发现并解决,把日志打清楚清楚一些,定位能够快一些。做到这些,就善莫大焉了。永远没有正确架构,你永远无法偿还完所有的技术债务,如同你永远不会设计出完美的界面,请避开完美主义的陷阱,永远没有完美的代码,能够满足当前需求,又为未来不管是自己还是他人留有扩展空间,就很好了。
我们能够做到的就是减缓复杂度增长的速率,通过 《整洁的代码》、《整洁的架构》和持续的《重构》,达成写更好的代码的目的。如果项目经过几年不得不重构了,那就大胆推倒重构好了,因为架构都是对过去经验的总结,无法预计业务的发展,站在几年后的时间节点回看,重构也是自然的,要不然怎么创造新的岗位需求呢。
34
要不断学习,有针对性的勤加练习
互联网唯一不变的就是变化,技术的出现也是层出不穷,知识的边界也是难以触达。唯有不断学习,才能保持竞争力。
阅读书籍、博客和技术网站,或者为自己找一个导师,可能 Ta 并不承认有你这个学生。导师可能是技术领域的大拿,也可能是网上的技术博主,或者是线下技术分享的讲师,也可能是 B 站上的一位 Up 主。他是谁并不重要,重要的是你要从他身上学到些什么。
了解你使用的框架和库。读懂他们的运作方式,你会使用的更好。这样你也有机会看到那些聪明人写下的并经过审查的代码。
当你犯错时,或者修复一个 bug,或者遇到问题时,试着真正去了解到底发生了什么。多问几次 Why,多往下追问几层,尝试找出根本原因。
多分享,只有分享才会让你真正了解底层原理。准备分享的过程就是不断追问自己为什么的过程,正因为你不知道为什么所以才需要查阅更多的资料来搞明白。能讲出来三分,说明你懂了七分。
学习那些能改变你的东西,学习那些能改变你行为的东西。不要在自己已经是专家的领域重复练习,去完成那些超出你当前能力的任务,尽力去做,在任务中和任务后分析你的表现情况,改正错误。
保持好奇,尝试找到自己的一套方法论,这套方法论能指导你快速了解一个复杂软件具体是如何工作的;
定期复盘,复盘不只再揭开伤疤撒把盐,更多看是否能有新的输入。比如上个季度做的一项工作,如果以现在的视角再去看一下,是否可以做的更好。
学习的东西不一定跟技术相关,如果学习所从事领域的知识可能会让你更好的理解需求、更快的解决业务问题、甚至于能够让你更充足更开心,那就去做。
不要把所有时间都用在工作, 毕竟,工作之余就应该是生活。
35
了解你的局限性
你的资源是有限的:
你每天只有8个小时时间可以用在工作上。你跑步的上限是10KM,最多打球2小时,否则容易拉伤。你的系统只能承载一万的 QPS。你的机器带宽只是千兆网卡, 只能传输100MB。你的系统核心数据结构的时间复杂度是O(n)的。RAM一次读取需要耗时20ns,而磁盘的一次随机读需要耗时10ms,从北京到广州的耗时是50ms。这些都是你的局限,除了知道你能做什么以外,还应该了解你的局限。这样你才能在排期协调、系统容量预估、大事件扩容、数据量增多导致的查询性能下降等方面做得更好。
36
使接口易于正确使用,难于错误使用
接口的存在是为了方便使用者,而不是接口实现者。如果一个接口需要在 好实现但是不好使用 和 好使用但是不好实现 上选择的话,请毫不犹豫的选择后者。
比如输入框需要用户输入日期,用户怎么知道输入格式呢,用户输入错误该怎么提醒呢?更合理的应该提供一个日历组件,用户直接选择就好了,不需要输入。
一个功能需要调用方必须先调用 A 初始化,再调用 B,最后再调用 C 清理。那何不如把这些步骤封装起来直接暴露一个接口呢。
37
简化根本复杂性,消除偶发复杂性
根本复杂性指的是问题与生俱来的、无法避免的困难。比如,协调全国的空中交通,必须实时追踪每架飞机的位置、高度、航速、航向、目的地、降落次序等,才能避免空中和地面冲突。以及还要兼顾因天气原因等航班延误导致的变化。
与之相反,偶发复杂性是解决根本复杂性过程中衍生的,即解决方案本身带来了新问题。比如笨拙的语言语法会导致大家轻易犯错、忘记释放内存导致 OOM 等;比如因为系统设计不佳导致需要更多的对账脚本或监控系统等。
软件开发是为了解决一套高度错综复杂、环环相扣的概念的全部细节问题。之所以存在根本复杂性,就是因为要和复杂、无序的现实世界对接;要准确、完整的识别依赖关系和例外情况;要设计出完全正确而不是大致正确的解决方案。这是完全无法避免的根本复杂度。
管理复杂性是软件开发最重要的课题,可以通过划分子模块、拆解子系统甚至到类的定义、子函数的拆分都是为了将复杂度拆解为简单的部分,隐藏细节,暴露简单的接口。各模块/类之间不要跟一团毛线一样混乱。这样才能易于理解,方便扩展。不要逼迫大脑去理解高复杂度的东西,而应该逐步拆解、简化复杂度,方便大脑理解。
我们应该合理划分职责,进而能够编写高扇入、低扇出的类。(高扇入表示被很多类依赖,低扇出表示不依赖太多其他类;和高内聚/低耦合是一回事儿)。
关注根本复杂性,消除偶发复杂性,抽丝剥茧制定解决方案,才是真正的挑战。
38
想象一个场景:你已经跳槽来腾讯3年了,一个工作日的下午你正在会议室开会,突然接到了上家公司的一个电话,需要你解释一下你所设计的系统架构决策点是什么,以及这个函数为什么要这样写。
是不是会很痛苦也会很无奈
我们进行的所有的工作最终都会落实在代码上,如果你写过的代码需要你对他负责一辈子,那么你会逼着三年前的自己好好命名,将大函数拆分成易于理解的小函数。将类和包结构好好组织,写好注释,并测试自己的代码。设计文档也会好好写,技术债务也会清理而不会任其堆积。因为余生你都需要对这份代码负责,所以你只能将它变得更优美、更灵活、更高效。
更进一步,如果你提交的代码会自动发布到你的朋友圈,让你的狐朋狗友可以评论观赏,你会不会把代码写得更好一点呢?猜测应该会好一点,就跟我们不在视频号上随便给小姐姐点赞一样,因为它真的会推给你的女朋友。
来源:散文随风想一点号