當前位置:
首頁 > 知識 > 我從資深軟體工程師學到的避坑大法

我從資深軟體工程師學到的避坑大法

選自neilkakkar

作者:Neil Kakkar

機器之心編譯

參與:陳韻瑩、一鳴

本文是彭博社的一位開發者所寫的文章,介紹了從一位資深工程師同事的身上學到的一些開發經驗。

過去一年中,我坐在一位資深的軟體工程師旁邊,可以仔細地觀察他是怎麼工作的。我們兩人經常共同編程,使得這項觀察更為容易。此外,在團隊文化中,從背後窺探寫代碼的人並不令人反感。以下是我所學到的:

編寫代碼

如何命名

我首先著手的是 React UI。我們有一個主要組件來放置其他所有的組件。我喜歡在代碼里加點幽默感,因此我想要將它命名為 GodComponent。當進入代碼審查環境的時候,我才明白為什麼命名這麼難。

在計算機科學裡有兩個難題:內存不足、命名、以及差一(off-by-one)錯誤。

——Leon Bambrick

我每個命名的代碼段都有隱藏含義在裡面。GodComponent 是所有我不必費心去尋找合適位置來存放那些垃圾的地方,它可以容納所有東西。如果我早早把它命名為 LayoutComponent,之後的我就會發現它所做的就是分配 layout,沒有狀態。

我發現命名好的另一個好處是:如果它看起來太長了,就像 LayoutComponent 包含了很多業務邏輯層,我就知道是時候要重構了,因為業務邏輯層並不屬於這裡。如果是以 GodComponent 命名,這裡的業務邏輯層也不會和其他有所區別。

命名你的集群?以在伺服器上運行的服務名稱來命名更好,直到用它們來運行其他服務為止。我們最終以團隊的名字來命名伺服器。

在函數上也是同樣的道理。doEverything() 是一個糟糕的名字,會有很多難以預料的後果。如果這個函數能夠做所有事情,那麼在測試函數某個特定部分時將變得非常困難。因為不管這個函數有多大,你都不會覺得奇怪,畢竟這個函數應該做所有的事情。這時候就需要改名、重構了。

有意義的命名也有不太好的一面。如果名字的表意太強,結果掩蓋了一些功能上的細微差別怎麼辦?例如:當你在 SQLAlchemy 中調用 session.close() 時,這隻會關閉會話但不會關閉底層資料庫的連接。

在這種情況下,可以以 x,y,z 來命名而不是 count(),close(),insertIntoDB(),這樣可防止為其賦予隱性含義並強制開發人員仔細檢查它所執行的操作。

歷史代碼和下一名開發者

你曾否看過一些代碼,覺得它們很奇怪?這些代碼為什麼這麼做呢?它們的實現一點都不合理。

我曾負責過遺留代碼庫。代碼中有諸如「當 Mohammad 發現情況時取消注釋代碼」這類的注釋。這是在做什麼?誰是 Mohammad?

在這裡可以做下角色轉換——想像下一個人來看我的代碼,他們是否會覺得奇怪?

同行審查可以某種程度上解決代碼注釋這個問題。這讓我想到了上下文的概念:注意我團隊正處的上下文位置。

如果我忘記了這部分代碼,之後又回到了代碼工作上,沒有注釋的話我不能重新創建上下文,我可能只會想:「為什麼他們要這麼寫?這沒有任何意義……哦,等等,是我寫的。」

這裡就是開發文檔和注釋該出現的地方。

文檔和注釋

文檔和注釋有助於維護上下文和分享知識。

正如李在《如何構建好軟體》中所說,「軟體的主要價值不是編寫它的代碼,而是編寫它的人所積累的知識。」

比如說,我們有個似乎沒有人用過的、面向隨機客戶端的 API 終端。因為這些原因,我就應該把它刪除嗎?畢竟這是一個技術累贅。

如果說,在某個特定國家,有 10 名記者會一年一次將他們的報道發送到這個終端,怎麼辦?你如何測試它?如果沒有開發文檔(那時就沒有)就不能測試。所以我們沒有測試。我們刪除了那個終端。過了幾個月後,到了一年中發送的時間,因為這個終端已經不存在了,10 名記者也就無法發送這 10 份重要報告。

雖然熟悉產品的人已經離開了團隊,但是現在代碼中有注釋解釋終端的作用。

據我所知,文檔是每個團隊都在努力的東西。不僅僅是代碼的文檔,還有關於代碼的流程。

自信地刪掉垃圾代碼

我過去很不喜歡刪除垃圾代碼或過時的代碼。我認為過去寫的代碼都是神聖的。我的想法是:「他們寫這些代碼的時候肯定有一些想法。」這是傳統和文化與第一性原則之間的碰撞,與刪除一年一次的終端發生的事相同。我在那裡學到了詳細的一課。

我嘗試基於已有代碼進行工作,但是資深工程師會嘗試解決掉它——全部刪除。一個永遠無法到達的 if 聲明?一個不應該調用的函數?是的,都消失了。

至於我呢?我只會把我的函數寫在最上面。我沒有減少這些技術累贅,反而增加了代碼的複雜程度,以及誤導別人的可能。下一個人將事情拼湊起來會更困難。

現在我受到的啟發是:有一些代碼你可能不理解,也有一些代碼你知道永遠不會用。刪除那些你永遠都不會用的代碼,小心那些你不理解的代碼。

代碼審查

代碼審查對學習來說非常有用。這是你寫代碼和其他人寫代碼時進行的外部反饋循環。

兩種實現有什麼區別呢?一種方法比另一種好嗎?每次代碼審查時我都問自己:「他們為什麼這樣做?「。每當我找不到合適的答案時,我就會去和他們談談。

在第一個月後,我開始在同事的代碼中找到錯誤(就像他們對我代碼做的一樣)。同行審查對我來說變得更有趣了——這是我期待的遊戲——一個提高我代碼意識的遊戲。

我的啟發是:在理解代碼如何實現前不要批准它。

測試

我非常喜歡測試,以至於如果沒有測試就將代碼寫入代碼庫我會感到非常不舒服。

如果整個應用程序只做一件事(就像我所有的學校項目),那麼手動測試是可以的。但是如果該應用程序可完成 100 種不同的功能,那該怎麼辦呢?我不想花半個小時來測試所有的功能,何況有時候還會忘記一些需要測試的地方。

所以就出現了自動化測試。

我認為測試是一種文檔,是對代碼假設的文檔。測試會告訴我(或我之前的人)他們預想代碼是如何工作的,以及他們預期哪裡會出錯。

所以,當寫測試時,我會記住:

記錄如何使用測試時用到的類/函數/系統。

記錄我所想到的會出錯的地方。

在大多數情況下,以上的結論是在我在測試而不是實現的過程中想到的。

以下是我在 Google 衛生間小休時學到的例子:

我在 #2 中遺漏了一些東西,那裡是 bug 出現的地方;

所以每當發現 bug 時,確保修復 bug 的代碼也有相應的測試(稱為回歸測試),用於記錄信息:這裡可能出現另一種錯誤。

僅僅編寫這些測試並不能提高我代碼的質量,而編寫代碼卻可以。但是我從閱讀測試代碼中獲得了寫更好代碼的直覺。

但是,並不只有這一種測試,這就是為什麼有部署環境測試的原因。

你可以有完美的測試單元,但是如果沒有系統測試,就會出現以下的情況:

這同樣適用於已經測試好的代碼:如果你機器上沒有你需要的庫,你會崩潰。

為了測試你需要:

有一台你用於開發的機器;

有一台你用於測試的機器;

最後,有一台你部署的機器(請不要用與開發程序使用同一台)。

如果測試和部署機器之間的環境不匹配,你就遇到麻煩了。所以這裡就出現了部署環境。

我們先有本地開發環境,在我的機器上是 docker;

然後有伺服器上的開發環境,機器上安裝了一系列的庫(和開發工具),我們在安裝了代碼的機器上進行開發。其他相關依賴的測試都可以在這裡進行;

接下來是 beta/stage 環境,它與生產環境完全一樣;

最後是生產環境,它是代碼運行和服務於實際客戶的機器上的環境。

這裡的想法是嘗試捕獲單元和系統測試無法捕獲的錯誤。例如,請求系統和響應系統之間的 API 不匹配。個人項目與小公司的情況大不一樣。不是每個人都有資源來搭建自己的設備。然而,這個想法仍適用於像 AWS 和 AZURE 這樣的雲供應商。

你可以為開發和生產設置分開的集群。AWS ECS 使用 docker 鏡像來部署,所以即使跨環境事情也會相對平穩。棘手的一點是其他 AWS 服務之間的集成。你是否可以在正確的環境中調用正確的終端呢?

你甚至可以更進一步:下載其他 AWS 服務的備用容器鏡像並使用 docker-compose 來配置本地完整的環境。它會加速反饋循環。

設計

為什麼我要將設計放到寫代碼和測試的後面呢?設計本應該在第一位,但是如果我沒有在環境中寫代碼和測試,我可能會不擅長設計一個遵循環境特性的系統。

在設計系統時,有很多事情需要考慮:

使用編號是多少?

有多少用戶?預期增長是多少?(即需要使用多少數據行)

未來可能出現的問題是什麼?

我需要把它轉成一個名為「需求收集」的合理清單。這個過程有點與靈活性的原則相悖——在開始系統開發之前,你可以設計多少部分呢?但是這是一種平衡——你需要選擇什麼時候做什麼。

當然僅僅收集需求並不是所有需要考慮的事情。我認為,在設計中包含了開發的過程也是值得去做的。例如:

本地開發如何運作?

怎麼打包和部署?

如何進行端對端的測試?

怎麼對新的服務進行壓力測試?

怎麼管理機密信息?

CI/CD 集成?

我們最近為 BNEF 開發了一個新的搜索系統。做這件事真的很棒。我開始設計本地開發,學習 DPKG(打包和部署)和試圖解決部署機密信息的問題。

誰會想到對產品中的機密信息進行部署會變得如此棘手呢?

你不能將這些信息存到代碼中,因為這樣任何人都能看得到。

把它們作為環境變數?這是一個好主意。但你怎麼把它們放在那裡?(每次機器啟動時訪問 PROD 機器來填充環境變數是一件痛苦的事情)

部署為機密文件?文件從哪裡來呢?怎麼進行填充呢?

而且我們不想進行手動操作。

最後我們使用了一個有角色訪問控制的資料庫(只有我們的機器可以與資料庫對話)。我們的代碼在啟動時從這個資料庫中獲取秘密數據。這個能在開發、測試和產品之間很好地複製——在各自的資料庫中都有機密。

同樣的,對於像 AWS 這樣的雲供應商,這可能非常不同。你不必考慮太多機密。獲取你角色賬戶,在用戶界面中輸入機密數據,在需要的時候你的代碼會找到它們。它簡化了很多時間,這非常酷,而我很高興有經驗領會這種簡易性。

設計時考慮維護需求

設計系統是件令人興奮的事。維護系統呢?就沒那麼有趣了。

我在維護過程中遇到了這個問題:系統為什麼會降級,以及如何降級?

有兩個原因可以解答為什麼系統也會有降級的時候:

首先,系統不應當捨棄舊的東西,而是在已有的基礎上增加更多功能。系統更新傾向於增加而不是刪除。

其次,帶著最終目標來設計。一個進化到做不該做的事情的系統和一個從零來設計做同樣事情的系統一樣,沒有用。這是一種系統的倒退。因此需要對系統進行降級。

現在我知道至少三種降低降級機率的方法:

將業務邏輯和基礎設施分開:通常是對基礎設施降級——當使用量增加、框架過時、出現零日漏洞等情況下;

圍繞系統維護建立流程。對舊的和新的組件都使用相同的更新。這可以防止組件之間出現差異,保持整個代碼「現代化」;

確保一直修剪你不想要的/舊的東西。

部署

將功能進行捆綁部署還是逐個部署呢?如果答案是將功能捆綁在一起,則會出現問題。

接下來要問的問題是:為什麼想要把功能進行捆綁呢?

部署是否花費過多時間?

代碼審查是否容易進行?

不管是什麼原因,這是需要修復的流程瓶頸。

捆綁功能部署至少有兩個問題

如果一個功能中有 bug,將妨礙另一個功能執行;

增加整體出錯的風險。

然後,無論你選擇什麼部署過程,你總是希望你的機器像一頭牛而不是像寵物一樣。它們並不珍貴。你知道每台機器上運行的是什麼,以及如何在死機的情況下重新創建它們。當一台機器死機時,你不會心煩意亂,你只需要啟動一台新機器。你像牛一樣放養它們,而不是像寵物一樣養著他們。

程序出錯的時候

當事情出錯時,而且一定會有出問題的時候,黃金法則是將對客戶的影響最小化。

當事情出了差錯,我自然傾向於趕快解決 bug。事實證明,這並不是最理想的解決方案。與其修復哪裡錯了,即使只是「修改一行」,所做的第一件事應該是回滾版本。回到之前的工作狀態,這是讓客戶恢復工作最快的方法。

過了這個時候,才應該看看哪裡出了問題並修復那些 bug。

在你的集群中出現一台「垮掉」的機器也應當是同樣的做法——在試圖找出機器出了什麼問題之前,先把它停了,並標記它不可用。

首先找 bug 這種本能會引導我走上解決 bug 的漫長旅途,反而偏離了讓客戶先恢復工作這一理想的目標狀態。有時候,我覺得它沒有工作的原因是因為寫的代碼有問題,而仔細閱讀每一行代碼後會陷入混亂,像是一種深度優先搜索。

之後,我的啟發是,首先開始廣度優先搜索,然後再深度優先搜索,去除最頂端的節點。能否用已有的資源確認:

機器啟動了嗎?

是否安裝了正確的代碼?

配置是否正確?

,像代碼中的路由是否正確?

模式版本是否正確?

然後進入代碼。

在某次出錯的問題上,我們以為機器上沒有正確安裝 nginx,但結果是配置被設置為了 false。

當然,我不需要總是這樣做。有時候錯誤信息已經足以減少需要搜索代碼的區域。而且當我無法解決這個問題時,我嘗試並持續修改代碼以將問題降到最低。修改的次數越少,我就能越快地處理實際問題。

但是我現在還是會記錄花了 1 個多小時來解決的 bug:遺漏了什麼?這通常是一些我忘記檢查的愚蠢錯誤,比如像設置路由、確保模式版本和服務版本匹配等。這是熟悉使用的技術堆棧的另一步,而且只有經驗會告訴我為什麼系統無法運行。

監控

這是我以前從未想過去做的事。說句公道話,在全職編碼之前,我從沒維護過系統。我只是搭建它們,使用 1 個星期後然後進行下一項工作。

有兩個系統,一個有良好的監控,另一個並不那麼好。我逐漸非常喜歡監控。如果我不知道 bug 在哪我就不能修改錯誤。其中一種最糟糕的感覺是從客戶那裡知道有 bug。

「我做了什麼?!我甚至不知道我的系統出了什麼問題?」

我認為監控由 3 個部分組成——日誌、衡量標準和警報。

日誌

以代碼中進行日記記錄就像人寫日誌一樣,是一個進化的過程。

你要找到你可能需要監控的東西,日誌記錄下來,運行系統。一段時間後,你會發現你沒有足夠信息來解決的 bug。這是增強日誌記錄的好時機——你的代碼少了些什麼?

我想你會憑直覺地知道什麼東西很重要需要記錄,但是在我們的伺服器中我和資深軟體工程師所記錄的東西有很多不同。我認為只要請求-相應日誌就足夠了,但是他會有更多的記錄內容,比如查詢執行時間、代碼進行的一些特定的內部調用,以及何時轉儲日誌。一切都已經解決了。

幾乎不可能在沒有日誌的情況下進行調試——如果你不知道系統的狀態,你怎麼重新創建它呢?

衡量標準和驚爆

衡量標準可以源於日誌,也可以獨立於日誌(例如向 AWS CloudWatch 和 Grafana 發送時間)。你可以決定你的衡量指標並在代碼運行時發送數字。

警報是把所有東西整合到一個的強大監控系統的粘合劑。如果一個衡量標準是當前產品中運行的機器數量,當這個數字降到 50% 時,這是一個很好的警報——你知道有什麼出錯了。

失敗計數高於某個閾值時?是的,又一個警報。

這裡暗示了另一個需要養成的習慣。當你修復 bug 時,你不僅僅關注如何修復 bug,而是你為什麼不早點發現它呢?是否有布置警報?如何能夠更好地監控來避免類似的問題?

我還不知道如何監控 UI。即使吧組件測試到位,也還不足以了解出錯的情況。這些錯誤通常是由客戶來告訴我們的——這看起來不太對勁。

總結

在過去的一年裡,我學到了很多東西。當我對這篇文章進行回顧時,我能夠更好地體會到我的成長。希望你也可以從這裡得到一些東西!

本文為機器之心編譯,轉載請聯繫本公眾號獲得授權。

------------------------------------------------

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 機器之心 的精彩文章:

港中文開源視頻動作分析庫MMAction,目標檢測庫演算法大更新
第四範式塗威威:自動機器學習求解三要素與發展趨勢

TAG:機器之心 |