近来我一直都需要带些毕业生(怎么我的队伍就那么多新人呢,呵呵),发现很多人在解决问题的方法上都存在一些问题。解决问题,其实是每个程序员每天都在干的事情。但是方法好还是不好,我觉得正是有经验和没有经验的程序员之间很重要的区别。刚好最近又看到了余晟公众号(yurii-says)的《砍伐大树 v.s. 收割庄稼》里面阐述的解决问题的四个步骤。所以,我想根据身边的案例详细一点解释一下这四个步骤。
一般来说,新手进入一个公司和团队之后,team lead 会分配一两个简单的任务让新人练练手。所以,首先要做的事情就是搭环境,和认识已有的系统和代码。
在搭环境和做任务的过程中,新人经常遇到问题或异常后就蒙了,不知道怎么继续下去。然后就求救说:“出错了,怎么办?”。我都会反问一句:“哪里出错了?你定位到问题在哪里了吗?”。可是一般收到的答案都是“不知道”。像我们公司的系统架构,把后端分了两层,一层专门服务于表现层,另一层专门访问数据库和提供 web service。系统的架构就简单如下图所示。如果在浏览器发起的一个 Ajax 请求有异常返回,那异常到底是在哪一层,或者是哪一个传输环节?如果在传输环节可能是因为对象类型,属性不一致导致。如果在 Domain 层可能是业务逻辑,数据库操作出现问题等。
认识和定位问题的能力应该是程序员技能宝典里面的 101,并且是要持续修炼的能力。 看日志 和 看代码 是获取这种能力的两种最基础的手段。
在没有源码的情况下,看日志可以说是唯一定位问题的方法了。再说,即便有源码,也不可能全都看一通,或者顺着源头一直 debug 下去,直到遇到问题为止。所以,定位问题的起点一般都是先看日志。
日志来源基本可以分为几大类:
一般来说,我们都先从第一种着手。因为一个好的项目,第一大类的日志应该是要把内部框架和所有外部系统组件的异常都封装好。异常处理设计的好,从这里的日志出发就可以较容易地追溯至真正问题所在。可是如果看完第一大类的日志都没能确定问题的原因,很可能是自身项目没有做好异常的捕捉处理,把外部系统或组件的异常给吞吃和隐藏了;又或者是因为一些运行时异常没有被捕捉到,然后就被写到外层的框架或者容器的日志里,比如说 Weblogic/Tomcat。
所以,经验首先就在于懂得在什么地方找日志;先看什么,后看什么;以及在项目自身日志不健全情况下,如何寻求第二或者第三大类的日志,看出错时间点的前后到底有什么信息可以帮助推断真正问题所在。
当定位到要改动的源码的位置时,很多新人面对着一个从未接触过的庞大的项目代码库,在 IDE 里面跳转多几次就乱了。他们遇到每一个方法调用都想进去看个明白。但是,一开始就想看懂整个系统的代码是不现实的。除了代码量庞大,或者找不到源码作者咨询以外,我们可能还有时间的压力,必须尽快找到问题和解决方案。那么,我们就必须有 猜代码 的能力。如何忽略细节,不迷失在茫茫代码海洋里,迅速定位和看懂自己的任务需要涉及和修改的代码是哪一块,是非常重要的。就像浏览文章一样,找段落大意,猜不同的代码大概是完成什么功能,然后再决定是否应该深入去研究某一块,而不是一次搞懂所有的代码。
其实就是 知其然,知其所以然 。
准确定位到问题或者异常发生在什么地方了,并不代表要修改的代码就是且只是那一块。我们不能头痛医头,脚痛医脚。
比如说,我们对外提供的一个 Web Service,因为浏览器或者调用方传的参数不全,报了 NullPointerException,我们要怎么改?改调用方还是提供方?首先,根据健壮性原则,也就是 Postel’s law ,看起来应该是改提供方这边:
Be conservative in what you do, be liberal in what you accept from others
但是,除了改提供方,我们还要考虑为什么调用方会提供少了一个参数,是所有的情况下都少了吗?还是某一些场景才会?尤其是当调用方是从一个具有复杂的业务操作逻辑的页面过来的时候,我们更需要全面分析。这才不至于说见一个洞,补一个洞,其实还漏了几个洞。
明白用户的功能使用场景和背后的动机,是产品功能的设计,如何取舍技术难点等的重要决策依据。
举个栗子,船运公司的单据录入部门其实有一份标准的操作指南。录入人员要根据托运人是谁,运什么货,从哪运到哪,要不要报关等一大堆的信息,决定应该怎么录入单据。那份操作指南其实就像一个问答系统,或者更像是一颗决策树一样。从成百上千种分门别类,如货物,航线,费用,报关等的问题找到那对应的决策路径来获知最后该怎么做。
那么,如果我们要做一个指南维护页面,怎么让用户容易找到他想要修改的条目,和清晰展现出来就要有一些考量了。对查找这个功能,开发人员一开始把那些门类和问题都做为下拉框展示出来以作为查询条件,并且还算聪明地实现了级联过滤,不至于用户因为门类和问题数目过多而不好找到他想要的东西。但是用户反映说那个问题的下拉框不能有级联。为什么?原因其实是有些问题是通用的。它们有可能出现在很多门类当中。所以,一种场景是他们已经知道要改的是某一个具体的问题,他想找出相关的所有门类。那么,我们把问题那个下拉框做成级联,他们就悲剧了。因为他们根本就不知道什么门类下有那个问题,怎么可能要求他们先选好门类,再选问题来查找呢?就问题这个下拉框,换另一种思路,做自动补全,让用户输入关键字就能过滤就好了。
在定位好问题和界定范围后,下一步就要看能采取什么方法来解决问题。
拿最近让新人做的一个开发任务来说吧。有一个页面,需要把数据库的数据,按照树的结构渲染出来和进行数据维护。这里,我们要考虑的东西有什么呢?
页面生成树要用什么前端技术,它的特点是什么?它生成树形结构的表现形式时,需要的数据结构是什么样的,是不是就是对应的树状数据结构还是没有要求?对树的结点进行页面上的操作时,数据就会自动更新吗,还是说我们要另外再独立更新数据?
页面的数据结构和数据库的结构一样吗?中间要经过多少次的转换?转换放在前台还是后台做?页面到底有一些什么业务操作,以及这些业务操作其实最后反映到数据库层面的形态是什么样的?界面操作后如何把信息带回后台,要带回什么数据(比如 ID,操作类型标记等)?
后台的数据更新逻辑是怎么样的?每次更新都需要整颗树的全部信息进行全量更新吗?
动手编码之前,对这些问题有初步的认识是相当重要的。有初步的想法之后,怎么做才能用最少的时间和精力来验证是否可行?如果还没有搞清楚,一下子就上手编码是很危险的。这个开发任务,也正是因为上面说的某几个方面没有做好,中间一些实现推倒重来了几次,进度比预期长了很多。
另一种非常重要的情况就是,我们想到或者碰到的技术难点,可能在实际应用场景根本碰到的情况就不高。如果避开那种场景来考虑的话,系统设计和实现方式可能就大大不同了。对于怎么舍弃一些技术细节问题,曹政的公众号(caozsay)的《如何应对并发(3) - 需求裁剪》就针对一个案例说得很清楚,大家可以看看。
还有,可行路径的选择,当然也受计划的影响。一个月,还是三个月时间的实现方案当然会有所不同。
估算和可行性分析其实密切相关。只有经过充分的可行性分析,估算才相对有意义。我觉得不是说有经验的人,估算的时间就比较准。而是有经验的人大概知道什么地方有坑,把上面的可行性分析和任务拆分做得更细,考虑得更全面一些。
其实人对时间的估算是非常不准确的,也不应当仅仅依赖于时间估算来做计划或者确定项目期限。我个人对估算的想法在之前写的 Kanban 系列里面有一篇文章也讲了一下。有兴趣可以看看。