作者 | 馬大偉
多年以后,面對這篇文章,我會想起那兩天失敗的令人崩潰的開發過程。當時,只是一個簡單的編碼需求,我信心滿滿的計劃一下午搞定,但是最終的過程卻是令人如此沮喪,讓我不得不懷疑我還適不適合繼續當程序員。
思緒飄到那天的場景,我在開發過程中遇到一個很簡單的需求:將 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 數據結構然后遞歸遍歷輸出最終目標數據結構。
在群里與這兩個人溝通的過程中,我發覺我在不知不覺中犯了幾個錯誤:
- 不熟悉 Clojure 代碼,導致沒法使用最佳的函數和思路去解決問題;
- 通過 Json 庫去輸出最終數據結構,而我卻是采用打印的方式將問題復雜化;
- 沒必要通過抽象語法樹去解決,通過 Json 庫遞歸遍歷 Json 是更簡單的方案;
- 沒使用更好的工具。我一開始用命令行自帶的 Repl,后來覺得編輯長函數不方便,所以在網上找了一個在線 Repl。不過后來看到群友提供的在線 livebook, 這種能更方便的開發并記錄這類代碼。
回顧這個問題的解決過程,我總結此次開發失敗的原因有以下:
- 理解需求錯誤。我在遇到這個問題后并未做深入的分析思考,導致一開始就沖著問題的表象去解決。想著用打印的方式去解決問題,實際上可以用庫來輸出目標格式。
- 不熟悉相關技術。我對 Clojure 的熟悉程度還不足以解決這類并不簡單的問題。
- 解決問題不全面。問題總有很多解,拿著錘子很容易看啥都是釘子。我從一開始就想通過 AST 去解決這個問題,導致思維局限到一條線上了。
- 害怕失敗。因為一開始覺得問題很簡單,害怕自己沒法在很短的時間解決,心態處于失衡的狀態。后期耗著的時間越長,思考能力越不在狀態,反而越來越迷糊。
失敗驅動開發
不了解程序員的人眼中的程序員可能是這樣的:
但開發程序或維護程序,失敗是很常見的:
- 編譯失??;
- 運行失?。?/li>
- 網絡失敗;
- 內存失??;
- 并發失敗;
- I/O 失?。?/li>
- 認證失敗;
- 權限失??;
- 依賴失敗;
- 資源失?。?/li>
- 上線失??;
- 升級失?。?/li>
- 環境設置失敗;
- 理解需求失??;
- 項目管理失??;
- 架構設計失敗;
程序員的日常就是要在無數失敗中找尋讓程序正常運行的那一種組合,成功運行更像是運氣與實力的雙重作用,這也就有了失敗驅動開發(Failure Driven Development)。
失敗既然是不可避免的,要做好一個程序員,與失敗平和相處是必須要解決的問題,不然情緒會長期處于失衡狀態。
如何以失敗驅動開發?我會從以下清單出發找尋處理失敗的方法:
是否全面理解問題?
很多時候不是問題復雜,而是我錯誤的理解了問題,在錯誤的路上越走越遠。每當失敗時我會重新全面的思考問題,看是否能發現新的解決問題的思路。
是否涉及知識盲區?
盲區是你不知道自己不知道。用有限的知識去解決未知的問題很容易陷入盲區而不自知。我的方法是如果一個失敗的原因我沒法在幾天內解決,那很可能就是遇到知識盲區了。要跳脫盲區必須全面的搜索關聯的知識,通過知識的交叉理解或尋找更了解這個領域的人幫忙是有效的解決方法。
對技術的掌握是否滿足要求?
用不熟悉的技術去解決不懂的問題很容易失敗。如果對技術不熟悉并且難以解決問題的話,我會從短期和長期兩個方面出發制定不同的方案。短期可能會尋求外部幫助讓更了解的人來幫我解決,長期我會投入更多時間提升這方面的技術。
所用技術或工具是否合適?
用不合適的技術和工具去解決問題也很容易導致失敗,并且這種失敗是難以察覺的。有時候不合適的技術或工具并不會讓問題無法得到解決,而是會浪費你大量的時間去解決技術或工具本身的問題。要解決這類失敗需要擴大知識廣度,在搜索資料時不局限某一種技術,如果你對多種技術有一定的理解,就很容易發覺技術之間的差異。用合適的技術或工具能達到事半功倍的效果。
是否存在解決方案?
很多問題早已經被前人解決。所以當遇到感覺復雜的問題,我會先搜索一番已經存在的解決方法,對問題現存的解決方法有個大概的認知,然后修改這些解決方法讓其能更好的解決我的問題。
是否需要記錄問題?
各類很難搞的問題是提高能力的好機會,學習現存的解決方法能消滅知識盲區。所以不斷的記錄總結這種問題是提高我能力的好方法。如果一個人一輩子遇不到難題,他也只能停留在現有的能力圈無法破圈。
是否需要尋求幫助?
花了很多時間問題卻解決不了是很令人沮喪,有些問題還很緊迫。在嘗試一定時間還毫無頭緒時我就會想辦法找人幫忙。讓人愿意幫忙也需要一些技巧,如果你提出一個很大的問題,沒人會愿意免費幫忙。所以我會把問題相關的上下文都寫下來或畫下來,然后將我錯誤的解決方法放上去,標記清楚失敗的點在哪里,然后把問題發給我覺得有這方面技術的朋友、同事及相關的社區。
如果問題比較復雜,我會提出付費咨詢的請求。在別人幫忙解決后,及時表達感謝之情,如有必要也可以發個紅包。當你通過這種方法認識不同領域的人,逐漸地你解決問題的效率也會得到提高。一些人會擔心,將自己的愚蠢公開暴露出來,尤其是一些低級錯誤出現的時候,是一件很掉面子的事情。其實一開始我也擔心,但是在網上你可以有很多虛擬身份,能緩解這種不適。
更重要的是,暴露自己的愚蠢能有效的解決自己的知識盲區,你覺得很復雜的問題在有經驗的人看來是很簡單的事情。這其實是一種極其有效的學習成長方式,在這個過程中我不僅可以解決我的難題,還能學習有經驗的人在這領域里的方法論和效率工具。
身體狀態是否合適?
長時間耗在一個問題上,身體和大腦都會疲憊。當心態失衡時,解決問題的能力也會直線下降。我經常會陷入一種急迫解決問題的困境,直到身體完全扛不住才放棄。這其實是一種低效的方式,情緒會在這個過程中逐漸壓制理智,讓人很難全面的思考問題。
與自己平和相處,接納自己的不足,休息好重新出發才能走的更長遠。所以當遇到自己很難解決的問題時,試著先確保身體狀態是正常的,如果身體很疲憊,先休息而不是直接攻克難題。
每一次失敗都是一次提升自己的機會。正是對失敗過程的不斷迭代解決,多年以后,讓我成為一個更好的開發者。