极客时间已完结课程限时免费阅读

11 | 三阶段进化:调试,编写与运行代码

11 | 三阶段进化:调试,编写与运行代码-极客时间

11 | 三阶段进化:调试,编写与运行代码

讲述:刘飞

时长13:55大小6.37M

刚开始学编程写代码,总会碰到一些困惑。比如,曾经就有刚入行的同学问我:“写程序是想到哪写到哪,边写边改边验证好,还是先整体梳理出思路,有步骤、有计划地分析后,再写更好?”
老实说,我刚入行时走的是前一条路,因为没有什么人或方法论来指导我,都是自己瞎摸索。一路走来十多年后,再回溯编程之路的经历,总结编程的进化过程,大概会经历下面三个阶段。

阶段一:调试代码 Debugging

编程,是把用自然语言描述的现实问题,转变为用程序语言来描述并解决问题的过程;翻译,也是把一种语言的文字转变为另一种语言的文字,所以我想编程和翻译应该是有相通之处的。
好些年前,我曾偶然读到一篇关于性能的英文文章,读完不禁拍案叫绝,就忍不住想翻译过来。那是我第一次尝试翻译长篇英文,老实说翻得很痛苦,断断续续花了好几周的业余时间。那时的我,之于翻译,就是一个刚入门的初学者。
初次翻译,免不了遇到不少不熟悉的单词或词组,一路磕磕碰碰地查词典或 Google。一些似乎能理解含义的句子,却感觉无法很好地用中文来表达,如果直白地译出来感觉又不像正常的中文句子表达方式。
如是种种的磕碰之处,难道不像你刚学编程时候的情形吗?刚开始写代码,对语法掌握得不熟,对各种库和 API 不知道,不了解,也不熟悉。一路写代码,翻翻书,查查 Google,搜搜 API 文档,好不容易写完一段代码,却又不知道能否执行,执行能否正确等等。
小心翼翼地点击 Debug 按钮开始了单步调试之旅,一步步验证所有的变量或执行结果是否符合预期。如果出错了,是在哪一步开始或哪个变量出错的?一段不到一屏的代码,足足单步走了半小时,反复改了好几次,终于顺利执行完毕,按预期输出了执行结果。
如果不是自己写全新的代码,而是一来就接手了别人的代码,没有文档,前辈稍微给你介绍两句,你就很快又开始了 Debug 的单步调试之旅,一步步搞清代码运行的所有步骤和内部逻辑。根据你接手代码的规模,这个阶段可能持续数天到数周不等。
这就是我感觉可以划为编程第一阶段的 “调试代码 Debugging” 时期。这个时期或长或短,也许你曾经为各种编程工具或 IDE 提供的高级 Debug 功能激动不已,但如果你不逐渐降低使用 Debug 功能的频率,那么你可能很难走入第二阶段。

阶段二:编写代码 Coding

翻译讲究 “信、达、雅”,编码亦如此。
那么何谓 “信、达、雅” ?它是由我国清末新兴启蒙思想家严复提出的,他在《天演论》中的 “译例言” 讲到:
译事三难:信、达、雅。求其信已大难矣,顾信矣,不达,虽译犹不译也,则达尚焉。
信,指不违背原文,不偏离原文,不篡改,不增不减,要求准确可信地表达原文描述的事实
这条应用在编程上就是:程序员需要深刻地理解用户的原始需求。虽然需求很多时候来自于需求(产品)文档,但需求(产品)文档上写的并不一定真正体现了用户的原始需求。关于用户需求的“提炼”,早已有流传甚广的“福特之问”。
福特:您需要一个什么样的更好的交通工具?  
用户:我要一匹更快的马。
用户说需要一匹更快的马,你就跑去 “养” 只更壮、更快的马;后来用户需求又变了,说要让马能在天上飞,你可能就傻眼了,只能拒绝用户说:“这需求不合理,技术上实现不了。”可见,用户所说的也不可 “信” 矣。只有真正挖掘并理解了用户的原始需求,最后通过编程实现的程序系统才是符合 “信” 的标准的。
但在这一条的修行上几乎没有止境,因为要做到 “信” 的标准,编写行业软件程序的程序员需要在一个行业长期沉淀,才能慢慢搞明白用户的真实需求。
达,指不拘泥于原文的形式,表达通顺明白,让读者对所述内容明达
这条应用在编程上就是在说程序的可读性、可理解性和可维护性。
按严复的标准,只满足 “信” 一条的翻译,还不如不译,至少还需要满足 “达” 这条才算尚可。
同样,只满足 “信” 这一条的程序虽然能准确地满足用户的需要,但没有 “达” 则很难维护下去。因为程序固然是写给机器去执行的,但其实也是给人看的。
所有关于代码规范和风格的编程约束都是在约定 “达” 的标准。个人可以通过编程实践用时间来积累经验,逐渐达到 “达” 的标准。但一个团队中程序员们的代码风格差异如何解决?这就像如果一本书由一群人来翻译,你会发现每章的文字风格都有差异,所以我是不太喜欢读由一群人一起翻译的书。
一些流行建议的解决方案是:多沟通,深入理解别人的代码思路和风格,不要轻易盲目地修改。但这些年实践下来,这个方法在现实中走得并不顺畅。
随着微服务架构的流行,倒是提供了另一种解决方案:每个服务对应一个唯一的负责人(Owner)。长期由一个人来维护的代码,就不会那么容易腐烂,因为一个人不存在沟通问题。而一个人所能 “达” 到的层次,完全由个人的经验水平和追求来决定。
雅,指选用的词语要得体,追求文章本身的古雅,简明优雅
雅的标准,应用在编程上已经从技艺上升到了艺术的追求,这当然是很高的要求与自我追求了,难以强求。而只有先满足于 “信” 和 “达” 的要求,你才有余力来追求 “雅” 。
举个例子来说明下从 “达” 到 “雅” 的追求与差异。
下面是一段程序片段,同一个方法,实现完全一样的功能,都符合 “信” 的要求;而方法很短小,命名也完全符合规范,可理解性和维护性都没问题,符合 “达” 的要求;差别就在对 “雅” 的追求上。
private String generateKey(String service, String method) {
String head = "DBO$";
String key = "";
int len = head.length() + service.length() + method.length();
if (len <= 50) {
key = head + service + method;
} else {
service = service.substring(service.lastIndexOf(".") + 1);
len = head.length() + service.length() + method.length();
key = head + service + method;
if (len > 50) {
key = head + method;
if (key.length() > 50) {
key = key.substring(0, 48) + ".~";
}
}
}
return key;
}
该方法的目标是生成一个字符串 key 值,传入两个参数:服务名和方法名,然后返回 key 值,key 的长度受外部条件约束不能超过 50 个字符。方法实现不复杂,很短,看起来也还不错,分析下其中的逻辑:
先 key 由固定的头(head)+ service(全类名)+ method(方法)组成,若小于 50 字符,直接返回。
若超过 50 字符限制,则去掉包名,保留类名,再判断一次,若此时小于 50 字符则返回。
若还是超过 50 字符限制,则连类名一起去掉,保留头和方法再判断一次,若小于 50 字符则返回。
最后如果有个变态长的方法名(46+ 个字符),没办法,只好暴力截断到 50 字符返回。
这个实现最大限度地在生成的 key 中保留全部有用的信息,对超过限制的情况依次按信息重要程度的不同进行丢弃。这里只有一个问题,这个业务规则只有 4 个判断,实现进行了三次 if 语句嵌套,还好这个方法比较短,可读性还不成问题。
而现实中很多业务规则比这复杂得多,以前看过一些实现的 if 嵌套多达 10 层的,方法也长得要命。当然一开始没有嵌套那么多层,只是后来随着时间的演变,业务规则发生了变化,慢慢增加了。之后接手的程序员就按照这种方式继续嵌套下去,慢慢演变至此,到我看到的时候就有 10 层了。
程序员有一种编程的惯性,特别是进行维护性编程时。一开始接手一个别人做的系统,不可能一下能了解和掌控全局。当要增加新功能时,在原有代码上添加逻辑,很容易保持原来程序的写法惯性,因为这样写也更安全。
所以一个 10 层嵌套 if 的业务逻辑方法实现,第一个程序员也许只写了 3 次嵌套,感觉还不错,也不失简洁。后来写 4、5、6 层的程序员则是懒惰不愿再改,到了写第 8、9、10 层的程序员时,基本很可能就是不敢再乱动了。
那么如何让这个小程序在未来的生命周期内,更优雅地演变下去?下面是另一个版本的实现:
private String generateKey(String service, String method) {
String head = "DBO$";
String key = head + service + method;
// head + service(with package) + method
if (key.length() <= 50) {
return key;
}
// head + service(without package) + method
service = service.substring(service.lastIndexOf(".") + 1);
key = head + service + method;
if (key.length() <= 50) {
return key;
}
// head + method
key = head + method;
if (key.length() <= 50) {
return key;
}
// last, we cut the string to 50 characters limit.
key = key.substring(0, 48) + ".~";
return key;
}
从嵌套变成了顺序逻辑,这样可以为未来的程序员留下更优雅地编程惯性方向。

阶段三:运行代码 Running

编程相对翻译,其超越 “信、达、雅” 的部分在于:翻译出来的文字能让人读懂,读爽就够了;但代码写出来还需要运行,才能产生最终的价值。
写程序我们追求 “又快又好”,并且写出来的代码要符合 “信、达、雅” 的标准,但清晰定义 “多快多好” 则是指运行时的效率和效果。为准确评估代码的运行效率和效果,每个程序员可能都需要深刻记住并理解下面这张关于程序延迟数字的图:
每个程序员都应该知道的延迟数字
只有深刻记住并理解了程序运行各环节的效率数据,你才有可能接近准确地评估程序运行的最终效果。当然,上面这张图只是最基础的程序运行效率数据,实际的生产运行环节会需要更多的基准效率数据才可能做出更准确的预估。
说一个例子,曾经我所在团队的一个高级程序员和我讨论要在所有的微服务中引入一个限流开源工具。这对于他和我们团队都是一个新东西,如何进行引入后线上运行效果的评估呢?
第一步,他去阅读资料和代码搞懂该工具的实现原理与机制并能清晰地描述出来。第二步,去对该工具进行效果测试,又称功能可用性验证。第三步,进行基准性能测试,或者又叫基准效率测试(Benchmark),以确定符合预期的标准。
做完上述三步,他拿出一个该工具的原理性描述说明文档,一份样例使用代码和一份基准效率测试结果,如下:
上图中有个红色字体部分,当阀值设置为 100 万而请求数超过 100 万时,发生了很大偏差。这是一个很奇怪的测试结果,但如果心里对各种基准效率数据有谱的话,会知道这实际绝不会影响线上服务的运行。
因为我们的服务主要由两部分组成:RPC 和业务逻辑。而 RPC 又由网络通信加上编解码序列化组成。服务都是 Java 实现的,而目前 Java 中最高效且吞吐最大的网络通信方式是基于 NIO 的方式,而我们服务使用的 RPC 框架正是基于 Netty(一个基于 Java NIO 的开源网络通信框架)的。
我曾经单独在一组 4 核的物理主机上测试过 Java 原生 NIO 与 Netty v3 和 v4 两个版本的基准性能对比,经过 Netty 封装后,大约有 10% 的性能损耗。在 1K 大小报文时,原生的 Java NIO 在当时的测试环境所能达到 TPS(每秒事务数) 的极限大约 5 万出头(极限,就是继续加压,但 TPS 不再上升,CPU 也消耗不上去,延时却在增加),而 Netty 在 4.5 万附近。增加了 RPC 的编解码后,TPS 极限下降至 1.3 万左右。
所以,实际一个服务在类似基准测试的环境下单实例所能承载的 TPS 极限不可能超过 RPC 的上限,因为 RPC 是没有包含业务逻辑的部分。加上不算简单的业务逻辑,我能预期的单实例真实 TPS 也许只有 1 千 ~2 千。
因此,上面 100 万的阀值偏差是绝对影响不到单实例的服务的。当然最后我们也搞明白了,100 万的阀值偏差来自于时间精度的大小,那个限流工具采用了微秒作为最小时间精度,所以只能在百万级的范围内保证准确。
讲完上述例子,就是想说明一个程序员要想精确评估程序的运行效率和效果,就得自己动手做大量的基准测试。
基准测试和测试人员做的性能测试不同。测试人员做的性能测试都是针对真实业务综合场景的模拟,测试的是整体系统的运行;而基准测试是开发人员自己做来帮助准确理解程序运行效率和效果的方式,当测试人员在性能测试发现了系统的性能问题时,开发人员才可能一步步拆解根据基准测试的标尺效果找到真正的瓶颈点,否则大部分的性能优化都是在靠猜测。
到了这个阶段,一段代码写出来,基本就该在你头脑中跑过一遍了。等上线进入真实生产环境跑起来,你就可以拿真实的运行数据和头脑中的预期做出对比,如果差距较大,那可能就掩藏着问题,值得你去分析和思考。
最后,文章开头那个问题有答案了吗?在第一阶段,你是想到哪就写到哪;而到了第三阶段,写到哪,一段鲜活的代码就成为了你想的那样。
你现在处在编程的哪个阶段?有怎样的感悟?欢迎你留言分享。
分享给需要的人,Ta购买本课程,你将得20
生成海报并分享

赞 4

提建议

上一篇
10 | 炫技与克制:代码的两种味道与态度
下一篇
12 | Bug的空间属性:环境依赖与过敏反应
unpreview
 写留言

精选留言(21)

  • third
    2018-08-27
    心得如下: 1.我对三个阶段的理解是,调试代码-独立,编写代码-合作,运行代码-事业 2.独立,你需要掌握编程的知识,并熟练掌握(一个人) 2.合作,你要需求与他人的合作,并降低合作成本。(一群人) 3.主要方法是,信达雅 4.信:理解真实的用户需求 5.达:方便维护和使用 6.雅:有一种美的感受和体验 7.事业,所有的道理和方法,都需要在现实中得到验证,同时保持进步(一群人和事物) 8.老实说,我觉得我在第一段的前边,属于刚入门的新手,要努力学习。 顺便请问一下老师,一个相对比较模糊问题。 以一个普通人的资质,编程2500小时左右,成为一个基本合格的新手程序员,是否是一件大概率事件呢?
    展开

    作者回复: 2500小时应该很熟练了,应该跨过新手阶段了

    14
  • 2018-08-28
    恩,现在处在一个什么阶段还真定位不好,我们现在的开发模式,基本是先弄清楚整体流程再动手,每天必开早会,代码开发完了自测、测试、review、完善这么个流程。 就个人而言,也比较喜欢将所有代码都搞定再验证,有问题再调整和优化。 编程之于翻译,确有异曲同工之妙。 业务-将现实问题翻译成需求 产品-将业务的需求翻译成PRD 研发-将产品的PRD翻译成代码 测试-验证研发的翻译工作信否 压测-验证研发的翻译工作性能好不 架构-检查研发的翻译工作达否 编译器-将Java源码翻译成Java字节码 JVM-将Java字节码翻译成机器码 cpu-将机器码指令执行起来,真正去解决现实的问题
    展开

    作者回复: 你们有个不错的流程^_^

    8
  • 维维
    2018-08-29
    理清思路,行云流水的敲完代码,一气呵成。然后第一个闪念就是我的代码根本不会出问题。😄😄😄

    作者回复: 哈哈😄

    共 2 条评论
    3
  • Franklin.du
    2018-08-27
    在第二阶段,到第三阶段还有好远的距离!!

    作者回复: 2,3阶段不是完全串行的,只是为了方便写作表达,可以是穿插迭代进化的

    2
  • Sch0ng
    2021-02-25
    一开始什么都不懂,不管什么需求都想用代码解决,最常说的话就是:这个功能我能实现,这个需求简单。 后来渐渐掉到一个四壁光滑的井里,天天都在解决bug,怎么都爬不出来。 再后来,换了个部门,被老大一句话点醒了--"天天在解决遗留问题,这里面有很大的问题,你的产出是什么?" 由此,我开始主动去想做每件事的意义和价值,坚决不做按下葫芦起了瓢的事。 我从井里看到了一束光,从此再也没有掉过坑里,并且产出也变得很有价值。
    展开
    2
  • 刘士涛
    2019-05-03
    老师文中的latency图有链接吗?

    作者回复: 有,Google一下图上的英文你就能找到

    1
  • helloworld
    2018-10-25
    关于基准测试部分的经验不错,不过工作中真有真多时间为了使用一个来源工具,先研究其源码的时间吗?直接压测看效果不就行了?

    作者回复: 一个新东西引入到核心服务中,不理解实现原理,是用不好的,还可能埋坑,这是必要的成本

    1
  • 朱月俊
    2018-10-18
    "实际一个服务在类似基准测试的环境下单实例所能承载的 TPS 极限不可能超过 RPC 的上限,因为 RPC 是没有包含业务逻辑的部分。加上不算简单的业务逻辑,我能预期的单实例真实 TPS 也许只有 1 千 ~2 千。" 意思是JAVA rpc 单实例上限tps只能达到1000-2000? 这个能详细说下嘛?

    作者回复: 不是,例子中说的是Java业务服务的TPS,包含了业务逻辑的

    1
  • 天师
    2018-10-11
    想讲个有点炫技的关于雅的经历,做OpenGL渲染的时候,有个同事提出个练习题,使用OpenGL实时渲染一个转动着的魔方,在各个轴各个层上都能够自由旋转,当时存在一个问题,需要确定旋转以后的小方块移动到了哪个位置,同事他们的处理方式是人工穷举,比如2x2x2的魔方里面,顶层旋转了90度,那么顶层1号的小方块就变成了旁边的2号小方块。 我觉得这种穷举不太舒服,而且不通用与3x3x3,4x4x4甚至更高阶的魔方,后来想出了个招数,用三维向量替代小方块的数字编号,这样每次旋转,实际上也是对小方块三维向量编号进行旋转,旋转后的向量就是小方块新的编号,这样的算法只需要用数学库构造一个旋转矩阵就可以完成,并且无论对于几阶的魔方,算法上都是通用的,并且也不需要人工穷举。这个招数,无论什么时候想起来,都觉得是挺舒服的一个思路。 相应的,见过的让我觉得舒服,欣赏的架构与代码,还有Akka的Reactive Stream,Scala的trait特性等等
    展开

    作者回复: 感觉这不是炫技,而是用了更通用的方法解决问题😊

    1
  • funky的两斤K仔
    2018-08-27
    现在在工作了,还是戒不掉写两行代码就想debug的习惯。怎么把代码模型整个放进脑子里好像也是个挑战。

    作者回复: 那还需要多想,多写代码

    1
  • Aliliin
    2018-08-27
    目前处于第一阶段,正在努力学习中。。

    作者回复: 加油💪😄

    1
  • 时熵
    2021-09-17
    tps 1 千 ~2 千 为什么会导致100 万的阀值偏差呢?这里没明白,能否再细说一下

    作者回复: 偏差是时间精度引起的

  • 第一装甲集群司令克莱...
    2021-01-02
    代码不会骗人,出问题debug总会复现,还原场景,也提示我们多做TDD,敬畏生产环境!
  • 😳
    2020-04-10
    目前正在阶段二,加油吧。
  • kali.allen
    2020-02-29
    可读性的代码-规范性的代码-易理解可扩展的代码,这是一个不断修炼不断精进提升的过程~
  • 今之人兮
    2019-01-03
    第一阶段,新手的时候熟悉代码的过程通常是使用debug。一步一步去看 第二阶段写代码的时候开始从需求上深入理解,把代码融会贯通。甚至更进一步。到第三个阶段的时候开始代码的功能性测试基准测试
  • 苦行僧
    2018-12-25
    现阶段走在达这条路上 希望能把核心业务做到测试驱动开发 雅对我来说是最高要求
  • 北风一叶
    2018-11-13
    我能说我还在第一阶段吗

    作者回复: 那继续加油^_^

  • 小白
    2018-09-09
    峰哥,那个是阀值还是阈值啊😂

    作者回复: 我理解意思差不多一样

  • June Yuan
    2018-09-03
    正在阶段二努力,有时会滑落到一。