寫在前面
我要寫得都是一些牢騷滿腹的東西。包含了我十年來覺得很不爽的東西,也有一些我覺得很不錯的觀點。寫代碼是個辛苦活,維護代碼更辛苦。有些時候為了趕時間會寫出低質量的代碼,然后花大量的時間去還債。但是有些時候低質量的代碼并不是由于時間的原因造成的。
客觀來講,低質量的代碼可能是由于程序員對需求的了解不夠深入而引起的。大部分的軟件都是應用于各行各業的,而程序員大部分又都是CS專業畢業的。拿醫療行業舉個例子,寫代碼的人和最終的使用者之間的差距,用“鴻溝”來形容都不為過。而且這種“鴻溝”基本上沒有彌補的可能性。程序員改了無數的BUG,可能都不知道軟件是在什么樣的環境中被什么樣的人所使用的。在沒有充分了解需求的情況下寫出高質量代碼的可能性是有的——這需要一個能力非常強,而且對于行業知識非常了解的人來帶領整個團隊——,但是這種可能性很低。
主觀來講,在這個世界上壓根就沒有太多所謂的高質量代碼。高質量代碼的分布能遵循2/8原則就謝天謝地了。首先“高質量”這三個字的含義都很模糊——對于不同的行業,不同的時期,“高質量”所指代的內涵,以及內涵的優先級都是不一樣的。舉個例子,20世紀80年代的代碼,大約是以運行速度快和占用內存小為“高質量”的標桿的。而到了本世紀,硬件資源不那么稀缺了,在大部分的場景下,可讀性和可維護性要優先于運行速度和空間耗費。即便對于同一個系統,在不同的模塊間,代碼的要求也可能是不同的。對于DSP里運行的代碼,首要因素是速度快和結構清晰,而上位機的代碼可讀性更重要。
啰嗦這么多只想說,寫好代碼是一件千難萬難的事情,是一個需要隨著時間而累積和修行的事情。只有扎實地做好每一步,才能寫出“高質量”的代碼。
基本原則
十年開發,大部分都是在修修補補,根本沒有機會打造屬于自己的開發環境和開發模式。幾個月前從公司跳了出來,算是開始了一段創業歷程。這是我有生以來的第一次。以前我對創業是不屑的,因為在我的觀念里,好的技術全在大公司里,所以我前三年全部的經歷都在琢磨如何提升自己,如何進入大公司。在大公司里待了七年,修修補補,挖坑排雷。這七年的提升不僅僅是技術上的提升,更重要的是心智和領導力的提升。當你的思維進入了正軌,大部分技術都只是時間而已。當你見識得夠多,所有的眼花瞭亂其實都是萬變不離其蹤。我不能說所有的東西都是一樣的,但是解決問題的思路也就是那么有限的幾種。以前沒有實現,并不是前人不夠聰明,而是現實世界的約束太強——巧婦難為無米之炊。因此,作為開發者,最理智的作法是從現有的技術里面,挑出我認為最好的東西(有時候是隨機碰到的),組合使用它們,實現我的系統。
舉個簡單的例子,我選擇在開發中用Conan進行二進制包管理。其實這種類型的包管理工具一直都在我們身邊——Nuget就是一個很成熟的應用。但是我為什么不選擇用Nuget呢?沒有什么特別的理由。我之前對于Nuget的使用體驗并不是特別好,無論是上傳還是下載都很費勁。正好在推上看到了Conan的宣傳,就下載來用用,發現不費勁,我要的功能它都提供了,所以就開始用起了Conan。
這樣的選型方式在我前七年的工作環境里確實有些隨意。大公司對于正式引入開發流程的任何工具和設備,都有嚴格的驗證流程需要遵守。并且要做出若干的表格進行對比分析。這樣做有它的好處,但是對于創業公司未免就有點“殺雞焉用牛刀”的感覺。并且隨著整個業界水平的提升,同一種類型的工具在質量上并不會有太大的差別。這也就是我敢于隨心選型的心理基礎。
總結一下我的基本原則只有兩點:
- 適用于小型團隊
- 看眼緣
開發語言的選擇
十年之間斷斷續續接觸了很多編程語言:C++, C#, Go, JS, Scalar,等。其中C++是我吃飯的家伙,所以格外花得時間多一點。其它都是有需要的時候翻翻文檔,邊查邊寫。幸運的是,除了C++之外其它語言學起來都不是那么難。C++活了這么多年,都快成活化石了,雖然大家都覺得它很難搞,但是總還是有它的強項。
首先就是性能問題。我們所開發的系統是一個封閉的系統,要給客提供軟件、硬件以及機械結構的一整套東西。并且,封閉式的系統一般情況下不會升級硬件,網絡資源也很有限。因此,相較于開放式的開發環境(類似于網頁應用或者一些桌面應用),封閉系統的計算資源受到很大的制約,性能就成了一個非常重要的考量方面。
其次與已有的成熟系統有關。感謝開源社區的發展,以及全世界無數英才的無私貢獻,現在已經很少需要從無到有地開發一個項目了。大部分基礎性的工作在社區里都能找到。作為開發者所要做的事情,就是找到你需要的項目,再把這些項目做裁剪,改進并膠合起來。除了開源社區,你也總能找到一些商用軟件可以全部或者部分得解決你的問題。這些開源或者商業的軟件所使用的語言,一定程度上會決定項目的開發語言選擇。
這些原因綜合起來,讓我決定以C++作為主力的開發語言。
單元測試
十年C++的開發經驗中,寫單元測試的經歷少之又少,也只在我自己的業務項目里面會寫寫測試用例。在公司正式的開發過程中,從來沒有寫過單元測試。因為大家都很明白,給C++寫單元測試就是一個坑,尤其是給一個已經跑了20年,并且從來沒有重構過的系統寫單元測試,這個坑不是一般得大。對于這種老系統,寫單元測試的可能性已經幾乎為零(需要重構幾乎所有的業務代碼才有可能寫出較大覆蓋率的測試用例),更不要說市場部門還在不停地壓縮開發時間。另外,這種老系統還有一個特點就是所有的開發人員被迫使用“open-close”原則。對于這種老系統已經沒有人能夠完全理解(比深度學習訓練出來的網絡還要神秘),所以最保險的方式就是加代碼。有些年輕人就是不信邪,但是碰過兩次壁之后就都學乖了,十年來從沒有過例外。
所以在系統開發的起初就要求必須給每個組件寫單元測試和系統測試。單元測試是對源代碼的測試,而系統測試是對組件的測試(系統測試可以是對單個組件的測試,也可能是對多個組件組合功能的測試)。在C++里組件一般是以shared library的形式出現,也有可能是header only library。無論是哪種形式,原則上每一個體現具體功能的文件都應該被測試到,即單元測試應該直接將相關文件包含進來,針對里面的代碼寫測試用例,而不是針對組件暴露的接口寫測試(這屬于系統測試)。比如有一個名字叫Foo的庫,庫里面有4個文件分別是FooA.h/cpp和FooB.h/cpp。在做單元測試的時候首先需要建立測試工程TestFoo,然后將Foo里的4個文件全部加到TestFoo工程里,然后分別針對4個文件里的代碼寫測試用例。然而實際上這樣的測試用例很難寫(有時候基本不可能,有時候則是代價太大)。前段時候用Boost.Asio寫了串口通信的庫,跟串口相關的功能就很難寫測試。另外googletest(我在項目里使用google test)也沒有很好地支持異步調用。
對于沒有反射支持的語言,單元測試都會是個頭疼的問題。很多測試代碼寫起來非常繁瑣,我甚至一度懷疑寫單元測試是不是一個正確的選擇。尤其是在寫Fake Object的時候,想要模擬一些與硬件異步通信相關的行為,經常會被搞得焦頭爛額。但是一但單元測試寫出來了,這些測試用例就會跟著你的項目一起成長。在寫單元測試的過程當中你會更加了解你自己的代碼,也更了解項目的需求,扎扎實實地寫出高質量的代碼。