作者|马大伟多年后,面对这篇文章,我会想起那两天失败的崩溃开发过程。当时只是一个简单的coding需求,本来满怀信心的打算一个下午就能搞定的,但是最后的过程实在是太折腾了,我不得不怀疑自己是否适合继续做程序员。我的思绪飘向了那天的情景。在开发过程中遇到了一个很简单的需求:将json格式文件转换为javascript常量文件(json转js不仅仅是格式转换,还要在js文件中生成json全路径)。如下图所示:我的想法是将JSON转化为一棵抽象语法树(AST),然后遍历这棵树,在特定的节点打印出需要的字符。使用Clojure的工件instaparse库将JSON直接转换为AST。我对Clojure不熟悉,正好可以通过这个过程来提高,也可以试试这个神器,看看有没有神奇之处。通过这种特殊的需求,可以一举多得,让枯燥的开发过程变得充满期待。第一步是将JSON转换为AST。对于instaparse库,这是一个非常简单的任务。在网上随便搜了一下,找到了解析JSON的代码。只需几分钟。第二步是遍历树。树遍历是我在大学算法课上学到的。虽然老算法的细节已经忘记了,但是我还记得有深度遍历和广度遍历两种方式。我的需求比较特殊的是遍历的时候需要打印相关的字符,比如在遍历某个节点的开始和结束的时候需要打印[]或者{}。Clojure应该有一个特定的库来执行此操作。简单的搜索很快就找到了walk和tree-seq这两个函数。这两个函数看起来比较复杂。找了一些例子,大致可以理解为:walk函数可以提供遍历时进入和退出的两个hook来进行集合元素的转换,而tree-seq会在深树遍历中输出一个节点序列。明白之后,我就开始尝试了。折腾了半天,发现事情比我想象的要复杂。这两个功能看起来很强大,但是在遍历节点的时候不能保存状态,但是我需要这个状态来记录我遍历的路径。看来需要自己写一个遍历算法来实现了。这时候半天过去了,但是我现在的进度只解决了一半的问题。自己写遍历树的算法并不难。我也用Java实现过,现在看来用Clojure实现也不难了。但Clojure与Java有很大的不同:它是函数式的,数据类型是不可变的,很多操作都是通过递归完成的。用递归实现深度遍历并不难,但是当你用一种不熟悉的语言来实现时,问题可能会变得不可控。在尝试了一天多并编写了三个失败的版本之后,由于一个我无法弄清楚的非常简单的问题,我处于绝望状态。在第二个版本中,我以为我解决了问题,最后输入实际数据,发现结果并不如预期。因为我用的是简单的测试数据,实际数据比测试数据更全面,我写的版本只是解决了测试数据的问题。在第三个版本中,由于考虑较多,编写较为复杂,程序仍然无法运行。由于不熟悉Clojure的语法,写出满足条件的递归代码一直很困难。因为在这道题上花了半天时间没有思路,周末搞了十几个小时,眼睛和腰终于受不了了。第二天,我身心俱疲,躺在床上久久不知如何寻求帮助。第一个想到的是直接在Clojure社区里问。为了让大家愿意回答我的问题,我先把我的问题整理了一下,画了一个简单的草图:然后我在StackOverflow上问了这个问题,并在Clojure的Discord群,Telegram国内社区和微信中发布了这个问题团体。不到半个小时,微信群里就有两个人晒出了自己的代码。这两个代码体现了不同的解决方案,并带有优雅的实现。我把具体的实现整理成了这本livebook。第一种方案直接将AST语法树通过递归转换成目标Map的数据结构,然后使用Json库打印成Json格式。方案二不使用AST语法树,直接通过Json库获取Json数据结构,然后递归遍历输出最终目标数据结构。在和群里这两个人交流的过程中,我发现自己在不知不觉中犯了几个错误:对Clojure代码不熟悉,导致无法用最好的功能和思路解决问题;通过Json库输出最终的数据结构,但是我用了打印把问题复杂化了;不需要通过抽象语法树来解决,通过Json库递归遍历Json是更简单的解决方案;没有使用更好的工具。一开始用的是命令行自带的Repl,后来发现编辑长函数不方便,于是在网上找了一个在线的Repl。不过后来看到群友提供的在线livebook,可以更方便的开发记录这类代码。回顾解决这个问题的过程,我总结这次开发失败的原因有以下几点:需求理解错误。我遇到这个问题后,没有做深入的分析和思考,导致一开始就解决了问题。想着通过打印解决问题,其实可以用库来输出目标格式。不熟悉相关技术。我对Clojure不够熟悉,无法解决此类重要问题。问题解决不彻底。一个问题总是有很多解决方案,很容易看出一切都是钉子和锤子。一开始我想通过AST来解决这个问题,导致我的思路仅限于一行。害怕失败。因为一开始觉得问题很简单,怕短时间内解决不了,心态不平衡。后期时间越长,思维能力越是处于状态,越是迷茫。失败驱动开发不懂程序员的人眼中的程序员可能是这样的:但是开发程序或者维护程序,失败是很常见的:编译失败;运行失败;网络故障;内存故障;并发失败;I/O失败;认证失败;权限失败;依赖失败;资源故障;在线失败;升级失败;环境设置失败;了解需求失败;项目管理失败;对于那种组合来说,成功的运作更像是运气和实力的双重作用,这也导致了失败驱动开发(FailureDrivenDevelopment)。既然失败是不可避免的,那么要成为一名优秀的程序员,与失败相处是必须要解决的问题,否则情绪会长期处于失衡状态。如何进行失败驱动开发?我会从以下清单开始寻找处理故障的方法:问题是否得到充分理解?很多时候,并不是问题复杂了,而是自己理解错了问题,在错误的道路上越走越远。每当我失败时,我都会全面地重新思考问题,看看能不能找到新的思路来解决问题。是否涉及知识盲点?盲点是你不知道你不知道什么。用有限的知识去解决未知的问题,很容易在不知不觉中陷入盲点。我的做法是,如果我不能在几天内解决一个失败的原因,那很可能是遇到了知识盲点。要想跳出盲点,就要对相关知识进行全面的搜索。知识的交叉理解或向更了解该领域的人寻求帮助是有效的解决方案。掌握的技术是否满足要求?使用不熟悉的技术来解决您不了解的问题很容易失败。如果技术是新的,难以解决的问题,我会在短期和长期制定不同的解决方案。短期内,我可能会寻求外部帮助,让更了解的人帮我解决。从长远来看,我会投入更多的时间来改进这方面的技术。使用的技术或工具是否合适?使用不合适的技术和工具来解决问题很容易导致失败,而且这种失败很难被发现。有时候不合适的技术或工具不会让问题无法解决,反而会浪费你大量的时间去解决技术或工具本身的问题。解决这类故障,需要扩大知识面。在搜索信息时,您不局限于某种技术。如果对多种技术有一定的了解,很容易发现技术之间的差异。使用正确的技术或工具,可以事半功倍。是否存在解决方案?很多问题已经被前人解决了。所以当我遇到一个感觉比较复杂的问题时,我会先搜索现有的解决方案,对问题的现有解决方案有一个大概的了解,然后修改这些解决方案以更好地解决我的问题。您需要记录问题吗?各种疑难问题都是提升能力的好机会,学习已有的解决方案可以消除知识盲点。所以不断地记录和总结这类问题是提高自己能力的好方法。一个人如果一生不遇到困难,就只能停留在现有的能力圈内,无法突破这个圈子。需要帮忙?在未解决的问题上花费大量时间可能会令人沮丧,而且有些问题很紧迫。当我尝试了一定时间还是毫无头绪时,我会想办法找人帮忙。让人们帮忙需要一些技巧,如果你问一个非常大的问题,没有人愿意免费提供帮助。所以我会写下或画出所有与问题相关的上下文,然后把我错误的解决方案放在上面,标记出故障点,然后将问题发送给我认为有这项技术的朋友、同事和相关人员.社区。如果问题比较复杂,我会提出付费咨询的要求。别人帮忙解决问题后,及时表达感谢,必要时发红包。当你通过这种方式认识不同领域的人时,你解决问题的效率就会逐渐提高。有些人担心公开揭露自己的愚蠢会很尴尬,尤其是在犯了一些低级错误的情况下。其实一开始我也很担心,不过网上可以有很多虚拟身份,可以缓解这种不适。更重要的是,暴露自己的愚蠢可以有效解决自己的知识盲点。你认为复杂的问题对于有经验的人来说是非常简单的事情。这其实是一种极其有效的学习和成长方式。在这个过程中,我不仅可以解决自己的问题,还可以学习到这个领域有经验的人的方法论和效率工具。身体状况是否合适?长时间研究一个问题会使身心疲惫。当心态失衡时,解决问题的能力就会直线下降。我经常陷入这样一种情况,即急于解决问题,直到我的身体完全不堪重负才放弃。这实际上是一种低效的方式。情绪在这个过程中会逐渐压制理性,让人难以全面地思考问题。平静的与自己相处,接受自己的不足,休养生息,重新出发,才能走得更远。所以当你遇到自己难以解决的问题时,尽量确保自己的身体状况是正常的。如果你很累,先休息,而不是直接解决问题。每一次失败都是提升自己的机会。多年后,正是这种失败过程的不断迭代解决方案使我成为了更好的开发人员。
