来源:京东技术
导读
本文主要从如何快速学习掌握编码技能展开,强调了认知对学习的重要性,提出了选择方向、树立榜样、学以致用等学习路径。同时针对成长过程中遇到的困惑和职业发展方向做了阐述,借事成长,择时出发,避免进入一些认知误区。以代码阅读案例,直观地展现了如何在代码阅读中学习和思考。最后,介绍了重构的意义和部分原则。
来源:京东技术
在日常工作中,经常听到部分同学抱怨代码质量问题,潜台词是:“除了自己的代码,其他人写的都是垃圾,得送到绞刑架上,重构!”。今天就来聊一聊,如何写得一手好代码。要回答这个问题之前,得先弄清楚一个问题,好代码的标准是什么?易阅读,可扩展,高内聚,低耦合,编程范式,设计原则......,要求不少,却很难度量。实则代码和文章一样,正所谓文无第一,武无第二。
这里不打算从规则宝典,最佳实践等方面入手,因为那将陷入到无数的规则细节中去,容易不得要领。这也是很多同学,学了很多当下最新技术,掌握了N门编程语言,却始终没有明显提升的原因。对于技术而言,底层的原理和运行规律是根本,它和编程语言,语法等应用层的重要程度是不一样的,切记不要进入这个误区。
技能的掌握一般需要经历学习、模仿、思考、创新四个过程,下面就分几个阶段来探讨一下,到底该如何快速学习成长。
学习出来的代码
2.1 学习意识
2.2 选择榜样
学习的过程中,带着批判的思维去消化,只有这样才能改进创新,所有的金科玉律都有其限定范围,当限定边界打破了,之前的正确性,就值得你去怀疑。举个例子,很多编码规范里都有那么一条:“一行代码长度,不超过80个字符”。
它的来历是这样的:在很久很久以前,有一个很流行的人机交互接口(终端) 叫 VT100,用来处理字符/文本,后来其它的很多终端都是以它为标准。这个终端屏幕24行、80列,编辑器菜单还占了 4 行。所以,代码编写建议是一个逻辑的处理代码,20行最佳、每行字符长度不超过80列。目的就是为了可视性(目之所能及)、可维护性。而如今显示终端的分辨率普遍提高了,所以升级调整规范并无不可,比如:“每行 120 个字符,每个函数体代码 80 行以内”。
所以,很多历史经验,了解其背后的运行逻辑,才能发挥出它原本的作用。
2.3 学以致用
大家常说万事开头难,究竟难在哪里?难在决心上,难在门槛上。决心可以通过痛点和目标来牵引,门槛可以通过目标拆解来降低。高质量地完成某件事情,有很多科学的工具和方法,比如:PDCA循环,SMART原则等,有兴趣的,可以拓展学习。
这里想要表达的是,拥有痛点和目标是能够持之以恒的前提,因为在学习实践的过程中,能够练习中进行应用,并能获得真实反馈和回报,这是能够坚持精进的原动力。否则就容易陷入,类似背诵四级应用单词“abandon”的魔咒,也不知道从它开始背诵了多少遍了。
再举个例子,当需要做系统模块解耦,调研下来,使用消息队列MQ中间件,能够很好地解决目前面临的问题。
1. 第一步,开始学习有关MQ的知识,了解各种MQ中间件的适用场景,结合使用场景,给出具体中间件的选型;
2. 第二步,将MQ中间件引入到系统中。使用的过程,就是一个不断发现问题,研究原因,修复问题,总结经验的过程。
如果没有真实的应用场景,往往会始终停留在第一步,反复开始,直到失去耐心,最终放弃。
2.4 小结
想要写好代码,需要有学习的意识,至少能够知道什么样的代码是好的,什么样的代码是有改进空间的。这种判断能力,需要通过不断的阅读各种类型的代码,从中找出榜样。资料的来源可以是经典书籍,好的开源项目,甚至是你身边的优秀项目。同时也需要规避一些误区:
1. 工作上遇到的大部分问题,只要去寻找,都是有解决方案的。需要亲自下场试错,创造答案的场景,很少;
2. 经验都是有适用边界的,照搬的经验不一定就适合我们,这需要了解经验背后的支撑逻辑,灵活的做出调整;
模仿出来的代码
3.1 认知陷阱
所谓代码模仿,不是简单的照猫画虎。而是遇到问题了,可以基于之前的学习积累,能够快速地找到优秀的解决方案,并加以利用。可以是一个技术点,可以是一种模式策略,可以是一种解决方案,甚至是一种编程思想。
图1.认知模型
3.2 以终为始
在实践过程中,会面临着要学的知识,要补的课,太多了:“计算机网络,应用服务,数据存储......”。计算机网络又包含运营商,光纤,电缆,域名,cdn,交换机,代理服务,http协议,客户端,浏览器,会话...,每一个节点又可以往下拆下去,感觉没有尽头。
是需要全部弄懂再去动手吗?到底是要做一个专才,还是成为一个全才?这个问题还要以发展的眼光来看。下面就以市场人才发展观为例,展开探讨一下。
由于行业的快速迭代,叠加各种不确定性,有些岗位的合理性受到不同程度的挑战,失去竞争力,遭遇下岗的境遇。于是业内又喊出了“π型人才”的概念。
π型人才是源于新加坡的一种人才观,在T型人才的基础上,进一步进化。π比T多出来的一竖,一般是源于兴趣爱好或工作所需,孵化出来的第二事业线。“两条腿走路”,势必有更强的抗风险能力,和更强的市场变化应对能力。
•梳子型人才
3.3 小结
4.1 编程思维
所谓编程思维,就是“理解问题,找出路径”的思维过程,它由分解、模式识别、抽象、算法四个步骤组成。
1. 一个复杂问题会先被拆解成一系列好解决的小问题;
2. 每一个小问题被单独检视、思考,搜索解决方案;
3. 聚焦几个重要节点,忽视小细节,形成解决思路;
4. 最后,设计步骤,执行,直至问题解决。
编程思维并不是编写程序的技巧,而是一种高效解决问题的思维方式。
4.2 编码原则
计算机是人造的学科,编码原则就三个字:好维护。如果考虑目标诉求,可能还可以追加一条:“运行快”,但目前大部分应用场景,计算机性能已经足够快了,很多时候,第二条往往被忽略掉了。
开闭原则,KISS 原则,组合原则,依赖反转,单一职责原则等大部分设计原则都是围绕着这个基本原则展开的。如果你觉得你的编码设计比较别扭,老得腾出精力来调整维护,那么大概率这个设计是不合理的,你得想办法让自己从中解放出来。
实际编码过程中的各种规范约束,比如:代码规范,设计模式,日志打印规范,异常处理策略,接口设计规范,圈复杂度等,参照基本编码原则去理解,就能想的通了。
4.3 抽丝剥茧
所有的技巧都是建立在熟悉的基础上,对于程序员来说,就是代码。阅读海量的代码,编写海量的代码,在这个过程中,不断地改进调优,是练就硬功夫的前提。下面就以案例的形式,为大家展现一下该如何阅读源码,借此提升自己的设计能力。

代码出处:org.apache.rocketmq.store.logfile.DefaultMappedFile#warmMappedFile
·背景介绍
·提出问题
·刨根问底
图3.缺页中断大概流程
简单解释一下这个流程:
进程通过CPU访问虚拟地址VA,通过MMU找到对应的物理地址(主存),当内存页在物理内存中没有对应的页帧或者存在但无对应的访问权限,在这种情况下,CPU就会报告一个缺页的错误;
物理内存中没有对应的页帧,需要CPU打开磁盘设备读取到物理内存中,再让MMU建立VA和PA的映射;
缺页对性能的影响,也得看具体情况,参照下图:

◦从磁盘缓冲区中调入缺页:数百ns
如果不加载任何MappedFile数据至内存中的话,按照最坏的影响,1GB的CommitLog需要发生26w多次缺页中断。所以通过代码设计,减少缺页的情况出现,会大大提升应用响应效率。
2)我们再来看第二个问题,为什么循环次数是4K,为什么往ByteBuffer中写0?
传统HDD扇区单位一直习惯于512Byte,有些文件系统默认保留前63个扇区,也就是前512 * 63 / 1024 = 31.5KB,假设闪存Page和簇(OS读写基本单位)都大小为4KB,那么一个Page对应着8个扇区,用户数据将于第8个Page的第3.5KB位置开始写入,导致之后的每一个簇都会跨两个Page,读写处于超界处,这对于闪存会造成更多的读损及读写开销。
图5.在文件写入过程中需要关注4K对齐的问题
3)最后,我们再来看第三个问题,为什么Thread.sleep代码块,注释上写着:prevent gc?
这段程序表达的意思很容易理解,每执行1000次循环,执行一次Thread.sleep(0)语句。但背后的目的却没那么明显。即Thread.sleep(0)可以让线程进入 Safepoint,从而触发GC。
这就得了解一下安全点(Safepoint),用户程序执行时,并非在代码指令流的任意位置都能够停顿下来,开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。意思就是在可数循环(Counted Loop)的情况下,HotSpot 虚拟机有一个优化,就是等循环结束之后,线程才会进入安全点。代码中int类型就属于可数循环,当然Long类型属于不可数循环。
总结一下,这段代码的目的就是,在预热数据的时候,每写入1000个字节,让该线程立即从运行阶段进入就绪队列,释放CPU时间,可以让操作系统切换其他线程来执行,比如GC线程的执行。这也侧面的反映出系统设计者对数据响应效率的追求,通过人工介入GC频率,防止出现超长时间GC情况的出现,影响瞬时的吞吐效率。
4.4 小结
程序编写,按照不同的维度视角,有很多各种各样的原则和建议。其本质还是以下两个方面:
1. 着眼于人:容易维护;
2. 着眼于机器:运算速度快。
清楚地认识到程序本质,是能够进行创新的基础。
最后,通过一个实际案例,简单地展现了一下如何阅读代码,以及如何从别人的代码中学习程序设计,其核心还是要有刨根问底的好奇心,拥有举一反三的思考与沉淀。
重构出来的代码
5.1 认识重构
重构(Refactoring)就是通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。更广义的理解,就是打破原有的组织形式,按照新的标准进行重新组合。
从理论和实际经验来讲,系统或代码的重构往往是个人能力实现快速提升的良好契机。相同条件下,有重构经验的同学和没有重构经验的同学,对很多概念和规则的理解深度会有很大区别。这就是典型的习得性经验,通过教学很难掌握。生活场景中的游泳,骑自行车就是习得性经验的代表。
图6.关于重构相关的要点
5.2 独当一面
现实情况中,整个团队梯队建设一般是金字塔型的,即高中低职级的同学一起对项目负责。不同职级的同学,对技术的要求和标准也不一样,要都按照高标准来执行,对低职级的同学显然是不公平的,反之也是一样。所以如何解决这个矛盾,是我们不得不面对的问题。
抓大放小,就是要按照业务区分核心和非核心,流程区分上游和下游,系统区分0级和n级......,重要的尽量按照高标准来执行,一般的按照普通标准来执行,并投入与之相匹配的资源。举个例子,比如,B端的某些运营工具,我们对它的QPS,可用率要求和C端用户的是不一样的,影响决策的依据是边际成本和收益。
•常见误区一:对性能优化的执念。“优秀的程序员应该榨干每一字节内存”,听起来很有道理,不是吗?但经济学上来讲,边际效应决定了一次项目中,越优化性价比越低。有一个很容易被忽略的事实:硬件其实比程序员要便宜。
•常见误区二:对设计模式的崇拜。设计模式当然是好东西,但如果像强迫症一样使用它们,就会导致按图索骥,强行让问题去适应设计模式,而不是让解决方案针对问题,这就本末倒置了。
所谓“甲之蜜糖,乙之毒药”。对于某款产品,稳定性,高吞吐就是其立身之本,直接影响着市场份额和收益,那么投入精力去做性能优化就是划算的。如果你的产品是一个提效工具,对代码的性能和扩展性,往往就没有那么高的要求,过于苛求反而没有必要。
5.3 小结
重构虽然对个人和业务都是有莫大好处,但考虑到实际成本问题,很多重构都是没有必要的。总结一下重构的要点:
◦重构的前提:重构是为了满足业务诉求而不得不做的最佳方案;
◦重构的最大风险就是:低估了实施难度。考虑兼容现有业务,同时支撑好未来规划。负重前行,要比重启新项目要复杂的多,如果新项目的难度是0到1,那么重构就是从-1到1;
◦重构最大难度是:目标制定和过程管理。
“知道自己知道”,就会对技术会有一些自己的思考和创新,不容易人云亦云,能够基于现状和目标,做出决策,即所谓的独当一面。
总结
本文主要从如何快速学习掌握编码技能展开,强调了认知对学习的重要性,提出了选择方向,树立榜样,学以致用等学习路径。同时针对成长过程中遇到的困惑和职业发展方向,做了阐述,借事成长,择时出发,避免进入一些认知误区。以代码阅读案例,直观地展现了如何在代码阅读中学习和思考。最后,介绍了重构的意义和部分原则。总体上,是按照学习成长路线来进行阐述的,希望能够减少前路上那些成长的烦恼!
注:本文部分图片和案例来自网上