一個 30 年老程序員的修煉之道

本文作者 Julio Biason 從 1990 年開始從事軟件開發工作,以下是他從過去 30 年軟件開發生涯總結出來的一系列冷笑話式的經驗之談。

關於軟件開發
規範先行,然後纔是代碼
在知道要解決什麼問題之前,請不要寫代碼。

Louis Srygley 說過:“如果沒有需求或設計,編程就成了一門往空文本里添加 bug 的藝術”。

有時候,僅僅一兩段簡單的描述就足以說明一個程序是用來幹什麼的。

每當你停下來,看着代碼,並開始思考下一步該做什麼的時候,通常是因爲你不知道下一步該做什麼。這個時候,你需要做的是與同事討論,或者可能需要重新思考之前的解決方案。

用批註的方式把實現步驟寫下來
如果你不知道從哪裏下手,先使用英語(或者你的母語)把程序的流程寫下來,然後在批註中添加代碼。

你也可以把每個批註當成是一個函數,然後用代碼實現它們。

用好 Gherkin
Gherkin 是一種測試 DSL,用來描述“系統處於某種狀態,如果發生某個事件,這就是所期望的狀態”。即使你不使用測試工具,Gherkin 仍然可以幫你更好地瞭解你能夠從程序中獲得哪些東西。

單元測試還不夠,最好還要有集成測試
在我目前的工作中,我們會進行模塊和類級別的測試。這些測試可以讓我們知道模塊或類的行爲,但無法讓我們知道系統整體的行爲——而集成測試可以告訴我們這些。

測試讓 API 變得更健壯
代碼是分層的:存儲層負責數據持久化,處理層負責轉換存儲的數據,視圖層負責呈現數據,等等……

分層測試可以讓你更好地瞭解各個層的 API,知道如何更好地調用各個層:API 會不會太複雜了?要進行一次簡單的調用,是否需要保留很多數據?

通過命令行運行測試用例
命令行對於任何一個項目來說都很重要。在你知道了如何使用命令來執行測試用例之後,就可以進行自動化測試,然後將它們集成到持續集成工具中。

做好丟棄代碼的準備
有很多人在開始使用 TDD 時會感到很惱火,因爲他們可能需要重寫很多代碼,包括已經寫好的那些。

而這正是 TDD 的“設計哲學”:隨時準備好丟棄你的代碼。隨着對問題研究的深入,你對要解決的問題越來越瞭解,不管之前寫了怎樣的代碼,它們終究不是解決問題的最終方案。

不過你不用擔心,代碼並不是一堵牆,如果將它們丟棄掉,也算不上是浪費磚塊。但花在寫代碼上的時間確實會一去不復返,不過換來的是你對問題更好的瞭解。

好的編程語言自帶測試框架
可以肯定地說,如果一門編程語言的標準庫自帶了測試框架,哪怕這個框架很小,它的生態系統也會得到比那些不提供測試框架的編程語言更好的測試,即使外部爲這些語言提供了很好的測試框架。

想得太長遠是一種浪費
有時候,程序員在解決一個問題時會想方設法尋找可以解決所有問題的方法,包括那些可能會在未來出現的問題。

但事實是,未來的問題可能永遠都不會出現,而你不得不去維護一大堆在未來可能永遠都用不上的代碼,甚至重寫所有代碼。

問題要一個一個解決,解決完眼前的,再解決下一個。到了某個時刻,你可能會從解決方案中找到某種模式,而這些模式纔是用來解決“所有問題”的良方。

寫文檔其實是在善待未來的你
誰都知道,給函數、類或者模塊編寫文檔是一件苦差事,但這也是在給未來的你省下很多麻煩。

文檔就是契約
代碼的文檔實際上是一種契約:文檔裏怎麼寫的,這個函數就是用來做什麼的。

如果後面你發現代碼和文檔不匹配,那麼就是代碼有問題,而不是文檔有問題。

如果一個函數文檔裏出現了“和”邏輯,那就一定有問題
一個函數應該只做一件事情。在給函數編寫文檔時,如果你發現需要用到“和”邏輯,那說明這個函數所做的事情不止一件。這個時候需要把函數拆成多個,不要在文檔裏出現“和”邏輯。

不要將布爾類型作爲參數
程序員在設計函數時通常喜歡在參數列表裏添加布爾類型,但請不要這麼做。

舉個例子:假設你有一個消息系統和一個函數,這個函數將所有消息返回給用戶,叫作“getUserMessage”。不過,有時候用戶需要獲取整條消息,有時候只需要獲取消息概要(比如消息的第一段)。於是,你加了一個布爾類型的參數,叫作“retrieveFullMessage”。

再次提醒,最好不要這麼做。

因爲當別人看到“getUserMessage(userId, true)”這樣的代碼時,他們可能不知道“true”是什麼意思。

你可以新增兩個函數“getUserMessageSummaries”和“getUserMessageFull”,然後讓這兩個函數分別調用“getUserMessage”,並將 true 或 false 傳給它,這樣可以保證對外的接口是清晰的。

在修改接口時要小心
上面提到了重命名函數,如果調用函數的代碼完全處在你的控制之下,那麼這麼做就沒什麼問題,你只需要把需要修改的地方找出來,然後改掉它們就可以了。

但如果被重命名的函數是作爲庫的一部分暴露給外部,那就不能隨意修改了。因爲這樣做會影響到所有調用函數的代碼,而這些代碼不在你的掌控之下,修改函數名只會給這些代碼的主人帶來大麻煩。

你可以新增一個函數,然後把舊函數標記爲已棄用。經過一些版本發佈之後,就可以慢慢將舊函數去掉。

好的編程語言自帶集成文檔
如果一門編程語言爲函數、類、模塊提供了文檔或者生成文檔的方式,那麼你就可以肯定,這門語言的函數、類、模塊、庫和框架也會有很好的文檔(即使不是最好的,但肯定不會差)。

相反,不提供集成文檔的編程語言通常只有糟糕的文檔。

在選擇編程語言時,不要只看語言本身
編程語言是你用來解決問題的得力工具,但不僅限於此:它還有構建系統,有依賴管理系統,有工具、庫和框架,有社區……

在選擇編程語言時,不要僅僅因爲它用起來很簡單。記住,你可以認爲一門語言的語法很簡單,但也需要考慮到社區因素。

有時候,讓程序奔潰比什麼都不做更好
這句話聽起來有點奇怪:與其捕獲了錯誤卻什麼都不做,還不如不捕獲錯誤。

在 Java 裏,經常會有人這麼幹:

複製代碼
try {
something_that_can_raise_exception()
} catch (Exception ex) {
System.out.println(ex);
}
這幾行代碼除了把異常打印出來之外,什麼都沒做。

如果你不知道該怎麼處理它,還不如把它拋出來,這樣起碼可以知道什麼時候會出現這樣的異常。

如果你知道怎麼處理異常,那就處理好它
這與上一條剛好相反:如果你知道什麼時候會拋出異常、錯誤或得到返回結果,並且知道怎麼處理它們,那就處理好它們。可以把錯誤消息顯示出來,試着把數據保存到某個地方,把用戶的輸入寫入日誌文件,等後面再回頭來處理。

類型系統會告訴你數據長什麼樣子
內存裏存的只不過是一系列字節,而字節只不過是從 0 到 255 的數字,這些數字的意義需要通過編程語言的類型系統來說明。

例如,在 C 語言中,“char”類型的 65 其實就是字母“A”,而“int”類型的 65 就是數字 65。

在處理數據時要牢記這個。

下面是我最近看到的一些 JavaScript 代碼,有人用這種方式判斷 True 的值,但顯然是錯的。

複製代碼

console.log(true+true === 2);
> true
console.log(true === 1);
> false
如果數據是有模式的,那就用合適的結構來保持數據的模式
你可能會用 list(或者 tuple)來保存數據簡單的數據,比如只有兩個字段的數據。

但如果數據是有模式的,也就是有固定格式的,那麼就應該使用合適的結構來保持數據的模式,比如使用結構體或類。

停止盲目跟風
“盲目跟風”的意思是:如果有人這麼做了,那我們也可以這麼做。大多數時候,盲目跟風是解決問題最簡單的方式。

“如果某個大公司是這樣保存數據的,那麼我們也可以這樣”。

“如果有大公司撐腰,那它就是好東西”。

“正確的工具”可能只是個人喜好
“正確的工具”本來應該是指使用合適的工具來完成某個任務,例如,使用合適的編程語言或框架來代替目前使用的語言或框架。

但每當我聽到有人提到這個說法時,他們只不過是想用他們喜歡的語言或框架來替代真正合適的語言或框架。

“正確的工具”不一定是正確的
假設你所在的項目需要處理一些文本,你可能會說:“讓我們使用 Perl 吧”,因爲你知道 Perl 很適合用來處理文本。

但你忽略了一點:你周邊的人只懂 C 語言,不懂 Perl。

當然,如果這個項目只是個無關緊要的小項目,那麼可以嘗試使用 Perl,但如果這個項目對公司來說很重要,那麼最好是使用 C 語言。

不要去修改項目以外的東西
有時候,人們會去直接修改外部的工具、庫或框架,比如直接修改 WordPress 或 Django 的代碼。

這樣很快會讓項目變得難以維護。當外部工具發佈新版本時,你不得不去同步變更,但很快你會發現,之前修改的東西對新版本不再有效了,所以只能保留舊版本,而舊版本可能有很多 bug。

數據流勝過設計模式
如果你知道數據是怎麼流經系統的,就可以寫出更好的代碼,這比應用各種設計模式要好得多(這只是個人觀點)。

設計模式是用來描述解決方案的,而不是用來尋找解決方案的
同樣,這也只是我的個人觀點。

大多數時候,人們會應用設計模式,試圖通過設計模式來找到解決方案,但結果是不得不對解決方案(甚至是問題本身)作出調整來匹配設計模式。

這樣的事情我見得多了:先是遇到問題,然後找到一個接近解決方案的設計模式,接着開始應用設計模式,然後在解決方案裏添加很多東西,讓它與設計模式相匹配。

學會基本的函數式編程
你不一定要成爲函數式編程專家,但請記住,有時候需要保持數據的不變性。使用新值來創建新元素,如果有可能,不要讓函數或類擁有內部狀態。

認知成本是代碼可讀性殺手
“認知衝突”是“需要同時記住兩件(甚至更多)東西才能幫你理解事物”的另一種說法。同時記住不同的東西會給大腦增加負擔,並且會削弱事物的相關性(因爲你需要在腦子裏保留更多的東西)。

例如,通過相加布爾類型來判斷 True 值的個數就是一種輕微的認知衝突。假設有一個“sum()”函數,你一看就會知道這個函數是用來計算一個列表中所有數值的和。而我卻見過有人用這個函數來計算一個布爾值列表中所有 True 值的個數,這樣很讓人感到困惑。

魔術數 7
魔術數 7解釋了人類在同一時間可以記住多少件東西。

如果你有一個函數,這個函數調用第二個函數,第二個函數又調用第三個函數,第三個函數又調用第四個函數,第四個函數又調用第五個函數,第五個函數又調用第六個函數,到最後你會發現這樣的代碼可讀性很差。

或者更進一步,你拿到一個函數返回的結果,把它傳給第二個函數,然後拿到第二個函數返回的結果,把它傳給第三個函數,一直這樣重複下去……

但問題是:

現如今,人們談論更多的是魔術數 4,而不是 7;

考慮使用函數組合(先調用第一個函數,再調用第二個……)代替函數調用(第一個調用第二個,第二個調用第三個……)。

捷徑雖好,但好處是短暫的
有很多編程語言、庫或框架會幫你簡化代碼,減少輸入。

但走捷徑會在以後給你帶來更多麻煩,甚至會讓你不得不重新使用複雜的代碼代替簡單的代碼。

所以,在採取捷徑之前,要先了解它們。

你不一定要先寫出複雜的代碼,然後使用捷徑來簡化:你可以採取捷徑,但一定要知道走捷徑可能會導致什麼後果,或者知道在不走捷徑的情況下該如何實現代碼。

抵制“簡單”的誘惑
IDE 爲我們提供了很多自動完成功能,讓我們可以更容易地構建項目,但你知道背後都發生了什麼嗎?

你知道構建系統的工作原理嗎?如果沒有 IDE,你知道怎麼構建項目嗎?

如果不借助自動完成功能,你記得那些函數的名字嗎?

所以,我們要對這些背後的東西保持一顆好奇心。

給日期帶上時區
在處理日期時,記得帶上時區。因爲你的電腦或服務器的時區有可能不對,在排查問題時可能會因爲接口返回錯誤的時區而浪費你很多時間。

總是使用 UTF-8
在進行字符編碼時也會遇到與日期類似的問題。所以,總是把字符串轉成 UTF8 格式,使用 UTF8 格式保存在數據庫中,API 返回的字符串也使用 UTF8 格式。

極簡主義
擺脫 IDE 可以從“極簡主義”開始:只使用編譯器和帶有代碼高亮功能的編輯器,並用它們構建和運行代碼……

但其實這樣做並不容易。不過當你再次使用 IDE 時,你就會知道按下那些按鈕之後 IDE 會做些什麼。

日誌用於記錄事件,不需要展現給用戶
在很長一段時間內,我一直通過日誌告訴用戶系統發生了什麼。

但其實我們可以使用標準輸出告訴用戶系統發生了什麼事件,使用標準錯誤輸出告訴用戶系統發生了什麼錯誤,然後使用日誌記錄事件,便於後續分析這些事件。

你可以把日誌看成是以後需要從中抽取信息的數據,它們不是面向用戶的,所以不一定非要人類可讀的。

調試器被過度高估了
有很多人認爲不帶有調試功能的代碼編輯器都不是好編輯器。

但是,代碼一旦進入生產環境,你就用不了調試器了,即使是你最喜歡的 IDE 也用不上了,但日誌卻無處不在。在發生故障時你可能不知道是怎麼回事,但你可以從日誌中查找原因。

我並不是說調試器毫無用處,只是它不像大多數人認爲得那樣有用。

一定要使用版本控制系統
你可以說“我只是想使用這個項目來學點東西”,但它不能成爲你不使用版本控制系統的理由。

如果你從一開始就使用版本控制系統,在出現問題之後可以很容易回滾。

一個變更對應一個提交
我經常看到代碼提交裏有這樣的消息:“修復問題 #1、#2 和 #3”。除非這三個問題是重複的(其中兩個應該是已關閉的),否則應該分成三次提交,每個提交對應一個問題。

如果代碼改過頭了,可以使用“git add -p”
Git 允許用戶通過“-p”參數進行部分提交,也就是選擇只提交部分代碼變更,把剩下的留到後面再提交。

按照數據或類型來組織代碼,而不是功能
很多項目的代碼結構類似下面這樣:

+-- IncomingModels
|   +-- DataTypeInterface
|   +-- DataType1
|   +-- DataType2
|   +-- DataType3
+-- Filters
|   +-- FilterInterface
|   +-- FilterValidDataType2
+-- Processors
|   +-- ProcessorInterface
|   +-- ConvertDataType1ToDto1
|   +-- ConvertDataType2ToDto2
+-- OutgoingModels
    +-- DtoInterface
    +-- Dto1
    +-- Dto2

也就是說,他們是基於功能來組織代碼的(所有模型放在同一個目錄中,所有過濾器放在另一個目錄中)。

這樣做其實也沒有什麼問題,只是如果按照數據來組織代碼的,那麼在將項目拆分成小項目時就會更容易些。

+-- Base
|   +-- IncomingModels
|   |   +-- DataTypeInterface
|   +-- Filters
|   |   +-- FilterInterface
|   +-- Processors
|   |   +-- ProcessorInterface
|   +-- OutgoingModels
|       +-- DtoInterface
+-- Data1
{1}|   +-- IncomingModels
|   |   +-- DataType1
|   +-- Processors
|   |   +-- ConvertDataType1ToDto1
|   +-- OutgoingModels
|       +-- Dto1
...
{1}

這樣你就可以獨立出各個模塊,比如只處理 Data1 的模塊,或者只處理 Data2 的模塊。

如果有另外一個項目需要處理 Data1,你就可以重用 Data1 模塊了。

創建公共庫
我見過很多項目使用同一個單獨的大代碼庫,與其這樣,爲什麼不把公共部分提取出來做成公共庫,然後在各個項目裏引用這些庫呢?

學會使用監控
以前,爲了瞭解系統的行爲,我會往系統裏添加很多度量指標。在習慣了這些之後,如果一個系統沒有監控,我就會覺得很奇怪。只是通過發送簡單的請求根本不足以判斷一個系統是否健康。

儘早給系統加入監控可以讓你更好地瞭解系統的行爲。

使用配置文件
假設你寫了一個函數,它只接受一個值作爲參數。如果你有兩個值需要分兩次傳給它,就需要調用這個函數兩次。

你也可以使用配置文件,把這兩個值分別寫在兩個配置文件裏,然後運行這個程序兩次。

命令行選項很有用
在將參數寫到配置文件裏之後,你還可以增加命令行參數,用來指定使用哪個配置文件。

有很多編程語言都提供了命令行參數解析器,可以用它們構建一個好用的命令行程序。

不只有函數組合,我們還有程序組合
Unix 的哲學是“一個程序只做一件事,而且做到極致”。

你可以使用一個程序和多個配置文件,但如你需要使用所有程序的運行結果,那該怎麼辦?

你可以再寫一個程序,把多次運行結果合併起來變成一個。

程序組合也可以從簡單的開始
程序組合模式到最後會變成微服務架構,而微服務架構要求服務之間具有良好的通信機制。

不過你也不用擔心個問題,你可以讓程序通過文件來通信,一個往文件裏寫,一個從文件裏讀。

在你瞭解了網絡通信機制之後再去考慮其他更復雜的通信問題吧。

把代碼優化工作留給編譯器
你想要獲得更好的代碼性能,於是總想着在這裏優化一點,在那裏優化一點。

但你要知道,優化代碼正是編譯器的拿手好戲。聰明的編譯器甚至會幫你把返回相同結果的代碼移除掉。

你需要做的是如何更好地設計你的代碼,而不是想方設法改進已有的代碼。

延遲求值
在很早以前,一些編程語言會在表達式被用到時求值,而不是在它們出現時求值。

Lips 在很早以前就這麼做了,現在有很多編程語言也這麼做了。

關於團隊協作
代碼評審不是爲了檢查代碼風格
在進行代碼評審時,請把時間花在架構和設計問題上,不要對代碼風格問題吹毛求疵。沒有人會喜歡這樣的代碼評審:“這一行開頭多了一些空格”、“括號裏少了空格”……

代碼格式化工具不能解決所有問題
爲了避免在代碼評審時討論代碼風格問題,有些團隊在提交代碼之前會使用格式化工具格式化代碼。

但這樣還有一個問題:我們是人,不是計算機,計算機能夠讀懂的東西不一定適合人類。格式化工具雖然爲我們帶來了某種程度的可讀性,但不一定都是對的。

遵循代碼風格
如果你的項目定義了一種代碼風格,就必須遵循它。

C/C++ 只有一種代碼風格:K&R
其他代碼風格都是不對的。

Python 只有一種代碼風格:PEP8
既然社區都在遵循 PEP8,你也應該這樣做,這樣你的代碼就可以更容易與社區的代碼集成。

顯式勝於隱式
你知道到目前爲止最糟糕的函數名是哪個嗎?

是“sleep()”。

究竟要 sleep 多久?是以秒爲單位還是以毫秒爲單位?

雖然諸如“sleepForSecs”或者“sleepForMs”之類的函數名也算不上最好,但肯定比“sleep”好。

通才的職業生命線比專才長
如果你精通某一門編程語言,可以很容易地找到一份工作,但從長遠來看,編程語言也會老去。如果你也懂其他編程語言,可以讓你走得更遠。

我一直在堅持一種編程原則:工作中使用的編程語言和工作之餘使用的肯定不一樣。我就是通過這種方式學會了很多東西:通過寫 Rust 代碼瞭解 Java 泛型原理;想要在 C++ 中實現依賴注入,所以對 Spring 也有了更深入的瞭解。

把“花了一個小時以上才解決的愚蠢錯誤”記錄下來
有些錯誤花了你一個小時以上才得以解決,比如“忘了加依賴”、“忘了加註釋”,把它們記錄下來,這樣以後不需要花那麼多時間就能找到問題所在了。

關於個人
如果累了,就要停下來休息
如果代碼寫不下去了,就停下來。如果無法再往前走了,就停下來。不要逼自己往前走,那樣只會讓事情變得更糟。

有一次,我在犯了偏頭痛的情況下繼續寫代碼。第二天,偏頭痛好了,但我不得不把之前寫的代碼全部重寫,因爲在偏頭痛時寫的代碼簡直就是一坨屎。

學會說不
有時候,你需要勇敢地說“不”。

有一次,我對我們的 CTO 說:“我可以做這件事,但我並不認同這麼做”。結果呢?結果那個 App 因爲我們不恰當的做法被禁了。

你要對自己的代碼負責
要做到這點很難,非常難。

寫出用來捕捉人臉或辨別種族的代碼並沒有錯,但你要想一下,這些代碼會被用在什麼地方。

成長過程很艱辛
無法通過編譯的代碼會讓我們感到挫敗,用戶的無理要求會讓我們感到生氣。

在遇到這些問題時,我們通常無法冷靜,而衝動只會讓我們自己陷入麻煩之中。

從麻煩中學習
你會感到挫敗、生氣、惱火……你會讓自己陷入麻煩之中。你也會看到其他人因爲這類事情招來大麻煩。

但你必須從麻煩中學習,不要輕易忽略它們。

我從挫折中學到的一件事是:當我感到沮喪時,就會變得很好鬥。現在,當我發現自己開始感到沮喪時,就會向別人尋求幫助。你可以看到別人也會有同樣的問題,不僅僅是你。

注意別人的反應
有時候,當我問別人一些事情時,他們的反應很激烈,就好像我在說他們的解決方案是錯的一樣。

這個時候我會補上一句:“我並不是說你們的方案是錯的,我只是感到有點疑惑”。

這樣你就不會惹上麻煩。

遠離“有毒”的人
你會發現,有些人雖然不會對你“閒言碎語”,但會在公開場合對其他東西——包括對其他人——“碎碎念”。

請遠離這樣的人。

你根本不知道他們的這種態度會讓你變得多麼沮喪。

你總是要經歷“英雄項目”
“英雄項目”是指爲了解決一系列項目問題而提出的另一個項目、規範變更或框架。

你利用空餘時間去做這些事情,可能只是爲了證明自己的觀點。

有時候,你會發現你的想法是錯的。

不要把“英雄項目”和“英雄綜合症”混淆起來了
我至少見過兩次了:有人聲稱如果他不在場就什麼事都成不了,或者他不需要別人的幫助就可以完成任何事情。

這就是所謂的“英雄綜合症”——這個人是唯一一個能夠解決所有問題的人。

但請你不要成爲這樣的人。

什麼時候可以考慮離職?
當一些奇怪的事情導致你無法及時完成項目,而你的老闆卻無法理解你。

當你的同事一直對你進行“微攻擊”。

當那個喜歡搞惡作劇的人無休止地說一些廢話,還有那個患上”英雄綜合症“的人……

這個時候,你可以考慮準備簡歷了,不管他們給你多少薪水,也不管項目有多好。

IT 這個圈子真的很小
IT 圈子真的很小。今天和你共事的人,在經歷了幾次工作更迭之後,可能會在 15 年之後又和你成爲同事。

便利貼真的很有用
我曾經多次嘗試”無紙化“,但到了最後發現有時候手頭有一個筆記本和一支筆其實很管用。

在博客上分享想法比保持安靜更好
你可能會覺得”我還沒有準備好分享自己的想法“,或者”這個想法很蠢,我不應該把它分享出來“。

你其實可以通過博客把想法分享出來,雖然你覺得它很愚蠢,但可能會比其他人的好。

把評論功能關掉
有些不懷好意的人會在你的博文底下搗亂,他們可能會說”這個想法很愚蠢“。所以,把評論功能關掉,不要讓這些人影響你。

把你不知道的東西記到清單裏
著名的物理學家 Richard Feynman 喜歡把他不知道的東西記在本子裏。

如果你發現了一些很酷的東西,並且想進一步瞭解它們,可以把它們記錄下來。

企業敏捷社區

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章