多年以后,面对这篇文章,我会想起那两天失败的令人崩溃的开发过程。当时,只是一个简单的编码需求,我信心满满的计划一下午搞定,但是最终的过程却是令人如此沮丧,让我不得不怀疑我还适不适合继续当程序员。
思绪飘到那天的场景,我在开发过程中遇到一个很简单的需求:将 JSON 格式的文件转换成 JavaScript 的常量文件(json到js的转换不只是格式的转换,还要在js文件生成json的全路径)。如下图:
我的想法是先把 JSON 转成一棵抽象语法树(AST),然后遍历这棵树,在特定的节点打印出所需的字符就可以了。JSON 转 AST 直接用 Clojure 的神器 instaparse 库。我对 Clojure 不熟悉,刚好可以通过这个过程提升下,也能试试这个神器到底神不神。通过这种特殊需求能一举多得,让无聊的开发过程变得有期待。
第一步是将 JSON 转 AST。对于 instaparse 库来说这是个很简单的任务,网上随便搜索下就找到了解析 JSON 的代码。耗时不过几分钟。
第二步是需要遍历这棵树。遍历树是我在大学算法课程上就学过的,虽然年代久远算法的细节都已经忘记,但是我还记得有深度遍历和广度遍历两种方式。我的这个需求特殊之处在于需要在遍历的时候打印相关的字符,比如需要在遍历某个节点开始和结束的时候都得打印 [] 或 {} 。Clojure 应该有具体的库做这个事,简单搜索下很快就找到了 walk 和 tree-seq 这两个函数。这两个函数看起来比较复杂,找了一些例子大概了解到: walk 函数可以在遍历是提供入和出两个钩子来执行对集合元素的转换,而 tree-seq 会以深度遍历树的方式输出一个节点序列。理解后就开始尝试,花了半天后发现事情比我想象中的复杂,这两个函数看起来强大,但是无法在遍历节点时保存状态,而我却需要这个状态来记录我遍历的路径。看起来需要自己写个遍历算法来实现了,这时候半天已经过去了,但我目前的进度只解决了一半的问题。
自己写遍历树的算法是一件不难的事情,我用 Java 也实现过,现在用 Clojure 实现看起来也不难。但是 Clojure 和 Java 的差异很大:它是函数式的,数据类型都不可变,很多操作都是通过递归来完成。用递归来实现深度遍历也不是难事,但是当你用不熟悉的语言去实现问题可能就会变得不可控。
在尝试了一天多并写了三个失败的版本后我陷入了绝望的状态,因为一个非常简单的问题我却搞不定。在第二个版本的时候我以为我解决了这个问题,最终把实际的数据输入却发现结果不符合预期。因为我用了简单的测试数据,实际的数据比测试数据全面,我写的版本只是解决了测试数据的问题。在第三个版本的时候因为考虑的情况更多写的也更复杂了,导致程序始终跑不起来。因为我不熟悉 Clojure 的语法,始终难以写出满足条件的递归代码。
由于长时间在这个问题上耗着又没有任何思路,我在周末连续搞了十几小时后眼睛和腰终于受不了了。第二天整个人身心俱疲,在床上躺了半天后琢磨如何寻求帮助。脑海中第一个念头就是在 Clojure 的社区里直接提问。为了能让大家有意愿回答我的问题,我首先把自己的问题梳理了下,画了一个简单的草图:
然后在 StackOverflow 提了这个问题,并在 Clojure 的 Discord 群组、Telegram 国内社群和微信群里发了这个问题。大概不到半小时,微信群里有两个人发了自己的代码。这两种代码体现了不同的解决思路,并且附带优雅的实现,具体的实现方案我整理到了这个 livebook 中。
第一种方案直接通过递归将 AST 语法树转换成了目标 Map 的数据结构,然后使用 Json 库打印成 Json 格式。第二种方案没有使用 AST 语法树,直接通过 Json 库拿到 Json 数据结构然后递归遍历输出最终目标数据结构。
在群里与这两个人沟通的过程中,我发觉我在不知不觉中犯了几个错误:
回顾这个问题的解决过程,我总结此次开发失败的原因有以下:
不了解程序员的人眼中的程序员可能是这样的:
但开发程序或维护程序,失败是很常见的:
程序员的日常就是要在无数失败中找寻让程序正常运行的那一种组合,成功运行更像是运气与实力的双重作用,这也就有了失败驱动开发(Failure Driven Development)。
失败既然是不可避免的,要做好一个程序员,与失败平和相处是必须要解决的问题,不然情绪会长期处于失衡状态。
如何以失败驱动开发?我会从以下清单出发找寻处理失败的方法:
每一次失败都是一次提升自己的机会。正是对失败过程的不断迭代解决,多年以后,让我成为一个更好的开发者。
文/Thoughtworks 马大伟
原文链接:失败驱动开发-Thoughtworks洞见