過(guò)去一年中,我坐在一位資深的軟件工程師旁邊,可以仔細地觀(guān)察他是怎么工作的。我們兩人經(jīng)常共同編程,使得這項觀(guān)察更為容易。此外,在團隊文化中,從背后窺探寫(xiě)代碼的人并不令人反感。以下是我所學(xué)到的:
如何命名
我首先著(zhù)手的是 React UI。我們有一個(gè)主要組件來(lái)放置其他所有的組件。我喜歡在代碼里加點(diǎn)幽默感,因此我想要將它命名為 GodComponent。當進(jìn)入代碼審查環(huán)境的時(shí)候,我才明白為什么命名這么難。
在計算機科學(xué)里有兩個(gè)難題:內存不足、命名、以及差一(off-by-one)錯誤。——Leon Bambrick
我每個(gè)命名的代碼段都有隱藏含義在里面。GodComponent 是所有我不必費心去尋找合適位置來(lái)存放那些垃圾的地方,它可以容納所有東西。如果我早早把它命名為 LayoutComponent,之后的我就會(huì )發(fā)現它所做的就是分配 layout,沒(méi)有狀態(tài)。
我發(fā)現命名好的另一個(gè)好處是:如果它看起來(lái)太長(cháng)了,就像 LayoutComponent 包含了很多業(yè)務(wù)邏輯層,我就知道是時(shí)候要重構了,因為業(yè)務(wù)邏輯層并不屬于這里。如果是以 GodComponent 命名,這里的業(yè)務(wù)邏輯層也不會(huì )和其他有所區別。
命名你的集群?以在服務(wù)器上運行的服務(wù)名稱(chēng)來(lái)命名更好,直到用它們來(lái)運行其他服務(wù)為止。我們最終以團隊的名字來(lái)命名服務(wù)器。
在函數上也是同樣的道理。doEverything() 是一個(gè)糟糕的名字,會(huì )有很多難以預料的后果。如果這個(gè)函數能夠做所有事情,那么在測試函數某個(gè)特定部分時(shí)將變得非常困難。因為不管這個(gè)函數有多大,你都不會(huì )覺(jué)得奇怪,畢竟這個(gè)函數應該做所有的事情。這時(shí)候就需要改名、重構了。
有意義的命名也有不太好的一面。如果名字的表意太強,結果掩蓋了一些功能上的細微差別怎么辦?例如:當你在 SQLAlchemy 中調用 session.close() 時(shí),這只會(huì )關(guān)閉會(huì )話(huà)但不會(huì )關(guān)閉底層數據庫的連接。
在這種情況下,可以以 x,y,z 來(lái)命名而不是 count(),close(),insertIntoDB(),這樣可防止為其賦予隱性含義并強制開(kāi)發(fā)人員仔細檢查它所執行的操作。
歷史代碼和下一名開(kāi)發(fā)者
你曾否看過(guò)一些代碼,覺(jué)得它們很奇怪?這些代碼為什么這么做呢?它們的實(shí)現一點(diǎn)都不合理。
我曾負責過(guò)遺留代碼庫。代碼中有諸如「當 Mohammad 發(fā)現情況時(shí)取消注釋代碼」這類(lèi)的注釋。這是在做什么?誰(shuí)是 Mohammad?
在這里可以做下角色轉換——想象下一個(gè)人來(lái)看我的代碼,他們是否會(huì )覺(jué)得奇怪?
同行審查可以某種程度上解決代碼注釋這個(gè)問(wèn)題。這讓我想到了上下文的概念:注意我團隊正處的上下文位置。
如果我忘記了這部分代碼,之后又回到了代碼工作上,沒(méi)有注釋的話(huà)我不能重新創(chuàng )建上下文,我可能只會(huì )想:「為什么他們要這么寫(xiě)?這沒(méi)有任何意義……哦,等等,是我寫(xiě)的?!?/p>
這里就是開(kāi)發(fā)文檔和注釋該出現的地方。
文檔和注釋
文檔和注釋有助于維護上下文和分享知識。
正如李在《如何構建好軟件》中所說(shuō),「軟件的主要價(jià)值不是編寫(xiě)它的代碼,而是編寫(xiě)它的人所積累的知識?!?/p>
比如說(shuō),我們有個(gè)似乎沒(méi)有人用過(guò)的、面向隨機客戶(hù)端的 API 終端。因為這些原因,我就應該把它刪除嗎?畢竟這是一個(gè)技術(shù)累贅。
如果說(shuō),在某個(gè)特定國家,有 10 名記者會(huì )一年一次將他們的報道發(fā)送到這個(gè)終端,怎么辦?你如何測試它?如果沒(méi)有開(kāi)發(fā)文檔(那時(shí)就沒(méi)有)就不能測試。所以我們沒(méi)有測試。我們刪除了那個(gè)終端。過(guò)了幾個(gè)月后,到了一年中發(fā)送的時(shí)間,因為這個(gè)終端已經(jīng)不存在了,10 名記者也就無(wú)法發(fā)送這 10 份重要報告。
雖然熟悉產(chǎn)品的人已經(jīng)離開(kāi)了團隊,但是現在代碼中有注釋解釋終端的作用。
據我所知,文檔是每個(gè)團隊都在努力的東西。不僅僅是代碼的文檔,還有關(guān)于代碼的流程。
自信地刪掉垃圾代碼
我過(guò)去很不喜歡刪除垃圾代碼或過(guò)時(shí)的代碼。我認為過(guò)去寫(xiě)的代碼都是神圣的。我的想法是:「他們寫(xiě)這些代碼的時(shí)候肯定有一些想法?!惯@是傳統和文化與第一性原則之間的碰撞,與刪除一年一次的終端發(fā)生的事相同。我在那里學(xué)到了詳細的一課。
我嘗試基于已有代碼進(jìn)行工作,但是資深工程師會(huì )嘗試解決掉它——全部刪除。一個(gè)永遠無(wú)法到達的 if 聲明?一個(gè)不應該調用的函數?是的,都消失了。
至于我呢?我只會(huì )把我的函數寫(xiě)在最上面。我沒(méi)有減少這些技術(shù)累贅,反而增加了代碼的復雜程度,以及誤導別人的可能。下一個(gè)人將事情拼湊起來(lái)會(huì )更困難。
現在我受到的啟發(fā)是:有一些代碼你可能不理解,也有一些代碼你知道永遠不會(huì )用。刪除那些你永遠都不會(huì )用的代碼,小心那些你不理解的代碼。
代碼審查
代碼審查對學(xué)習來(lái)說(shuō)非常有用。這是你寫(xiě)代碼和其他人寫(xiě)代碼時(shí)進(jìn)行的外部反饋循環(huán)。
兩種實(shí)現有什么區別呢?一種方法比另一種好嗎?每次代碼審查時(shí)我都問(wèn)自己:「他們?yōu)槭裁催@樣做?「。每當我找不到合適的答案時(shí),我就會(huì )去和他們談?wù)劇?/p>
在第一個(gè)月后,我開(kāi)始在同事的代碼中找到錯誤(就像他們對我代碼做的一樣)。同行審查對我來(lái)說(shuō)變得更有趣了——這是我期待的游戲——一個(gè)提高我代碼意識的游戲。
我的啟發(fā)是:在理解代碼如何實(shí)現前不要批準它。
測試
我非常喜歡測試,以至于如果沒(méi)有測試就將代碼寫(xiě)入代碼庫我會(huì )感到非常不舒服。
如果整個(gè)應用程序只做一件事(就像我所有的學(xué)校項目),那么手動(dòng)測試是可以的。但是如果該應用程序可完成 100 種不同的功能,那該怎么辦呢?我不想花半個(gè)小時(shí)來(lái)測試所有的功能,何況有時(shí)候還會(huì )忘記一些需要測試的地方。
所以就出現了自動(dòng)化測試。
我認為測試是一種文檔,是對代碼假設的文檔。測試會(huì )告訴我(或我之前的人)他們預想代碼是如何工作的,以及他們預期哪里會(huì )出錯。
所以,當寫(xiě)測試時(shí),我會(huì )記?。?/p>
在大多數情況下,以上的結論是在我在測試而不是實(shí)現的過(guò)程中想到的。
以下是我在 Google 衛生間小休時(shí)學(xué)到的例子:
僅僅編寫(xiě)這些測試并不能提高我代碼的質(zhì)量,而編寫(xiě)代碼卻可以。但是我從閱讀測試代碼中獲得了寫(xiě)更好代碼的直覺(jué)。
但是,并不只有這一種測試,這就是為什么有部署環(huán)境測試的原因。
你可以有完美的測試單元,但是如果沒(méi)有系統測試,就會(huì )出現以下的情況:
這同樣適用于已經(jīng)測試好的代碼:如果你機器上沒(méi)有你需要的庫,你會(huì )崩潰。
為了測試你需要:
如果測試和部署機器之間的環(huán)境不匹配,你就遇到麻煩了。所以這里就出現了部署環(huán)境。
這里的想法是嘗試捕獲單元和系統測試無(wú)法捕獲的錯誤。例如,請求系統和響應系統之間的 API 不匹配。個(gè)人項目與小公司的情況大不一樣。不是每個(gè)人都有資源來(lái)搭建自己的設備。然而,這個(gè)想法仍適用于像 AWS 和 AZURE 這樣的云供應商。
你可以為開(kāi)發(fā)和生產(chǎn)設置分開(kāi)的集群。AWS ECS 使用 docker 鏡像來(lái)部署,所以即使跨環(huán)境事情也會(huì )相對平穩。棘手的一點(diǎn)是其他 AWS 服務(wù)之間的集成。你是否可以在正確的環(huán)境中調用正確的終端呢?
你甚至可以更進(jìn)一步:下載其他 AWS 服務(wù)的備用容器鏡像并使用 docker-compose 來(lái)配置本地完整的環(huán)境。它會(huì )加速反饋循環(huán)。
設計
為什么我要將設計放到寫(xiě)代碼和測試的后面呢?設計本應該在第一位,但是如果我沒(méi)有在環(huán)境中寫(xiě)代碼和測試,我可能會(huì )不擅長(cháng)設計一個(gè)遵循環(huán)境特性的系統。
在設計系統時(shí),有很多事情需要考慮:
我需要把它轉成一個(gè)名為「需求收集」的合理清單。這個(gè)過(guò)程有點(diǎn)與靈活性的原則相悖——在開(kāi)始系統開(kāi)發(fā)之前,你可以設計多少部分呢?但是這是一種平衡——你需要選擇什么時(shí)候做什么。當然僅僅收集需求并不是所有需要考慮的事情。我認為,在設計中包含了開(kāi)發(fā)的過(guò)程也是值得去做的。例如:
我們最近為 BNEF 開(kāi)發(fā)了一個(gè)新的搜索系統。做這件事真的很棒。我開(kāi)始設計本地開(kāi)發(fā),學(xué)習 DPKG(打包和部署)和試圖解決部署機密信息的問(wèn)題。
誰(shuí)會(huì )想到對產(chǎn)品中的機密信息進(jìn)行部署會(huì )變得如此棘手呢?
而且我們不想進(jìn)行手動(dòng)操作。
最后我們使用了一個(gè)有角色訪(fǎng)問(wèn)控制的數據庫(只有我們的機器可以與數據庫對話(huà))。我們的代碼在啟動(dòng)時(shí)從這個(gè)數據庫中獲取秘密數據。這個(gè)能在開(kāi)發(fā)、測試和產(chǎn)品之間很好地復制——在各自的數據庫中都有機密。
同樣的,對于像 AWS 這樣的云供應商,這可能非常不同。你不必考慮太多機密。獲取你角色賬戶(hù),在用戶(hù)界面中輸入機密數據,在需要的時(shí)候你的代碼會(huì )找到它們。它簡(jiǎn)化了很多時(shí)間,這非???,而我很高興有經(jīng)驗領(lǐng)會(huì )這種簡(jiǎn)易性。
設計時(shí)考慮維護需求
設計系統是件令人興奮的事。維護系統呢?就沒(méi)那么有趣了。
我在維護過(guò)程中遇到了這個(gè)問(wèn)題:系統為什么會(huì )降級,以及如何降級?
有兩個(gè)原因可以解答為什么系統也會(huì )有降級的時(shí)候:
首先,系統不應當舍棄舊的東西,而是在已有的基礎上增加更多功能。系統更新傾向于增加而不是刪除。
其次,帶著(zhù)最終目標來(lái)設計。一個(gè)進(jìn)化到做不該做的事情的系統和一個(gè)從零來(lái)設計做同樣事情的系統一樣,沒(méi)有用。這是一種系統的倒退。因此需要對系統進(jìn)行降級。
現在我知道至少三種降低降級機率的方法:
部署
將功能進(jìn)行捆綁部署還是逐個(gè)部署呢?如果答案是將功能捆綁在一起,則會(huì )出現問(wèn)題。
接下來(lái)要問(wèn)的問(wèn)題是:為什么想要把功能進(jìn)行捆綁呢?
不管是什么原因,這是需要修復的流程瓶頸。
捆綁功能部署至少有兩個(gè)問(wèn)題
然后,無(wú)論你選擇什么部署過(guò)程,你總是希望你的機器像一頭牛而不是像寵物一樣。它們并不珍貴。你知道每臺機器上運行的是什么,以及如何在死機的情況下重新創(chuàng )建它們。當一臺機器死機時(shí),你不會(huì )心煩意亂,你只需要啟動(dòng)一臺新機器。你像牛一樣放養它們,而不是像寵物一樣養著(zhù)他們。
程序出錯的時(shí)候
當事情出錯時(shí),而且一定會(huì )有出問(wèn)題的時(shí)候,黃金法則是將對客戶(hù)的影響最小化。
當事情出了差錯,我自然傾向于趕快解決 bug。事實(shí)證明,這并不是最理想的解決方案。與其修復哪里錯了,即使只是「修改一行」,所做的第一件事應該是回滾版本?;氐街暗墓ぷ鳡顟B(tài),這是讓客戶(hù)恢復工作最快的方法。
過(guò)了這個(gè)時(shí)候,才應該看看哪里出了問(wèn)題并修復那些 bug。
在你的集群中出現一臺「垮掉」的機器也應當是同樣的做法——在試圖找出機器出了什么問(wèn)題之前,先把它停了,并標記它不可用。
首先找 bug 這種本能會(huì )引導我走上解決 bug 的漫長(cháng)旅途,反而偏離了讓客戶(hù)先恢復工作這一理想的目標狀態(tài)。有時(shí)候,我覺(jué)得它沒(méi)有工作的原因是因為寫(xiě)的代碼有問(wèn)題,而仔細閱讀每一行代碼后會(huì )陷入混亂,像是一種深度優(yōu)先搜索。
之后,我的啟發(fā)是,首先開(kāi)始廣度優(yōu)先搜索,然后再深度優(yōu)先搜索,去除最頂端的節點(diǎn)。能否用已有的資源確認:
在某次出錯的問(wèn)題上,我們以為機器上沒(méi)有正確安裝 nginx,但結果是配置被設置為了 false。
當然,我不需要總是這樣做。有時(shí)候錯誤信息已經(jīng)足以減少需要搜索代碼的區域。而且當我無(wú)法解決這個(gè)問(wèn)題時(shí),我嘗試并持續修改代碼以將問(wèn)題降到最低。修改的次數越少,我就能越快地處理實(shí)際問(wèn)題。
但是我現在還是會(huì )記錄花了 1 個(gè)多小時(shí)來(lái)解決的 bug:遺漏了什么?這通常是一些我忘記檢查的愚蠢錯誤,比如像設置路由、確保模式版本和服務(wù)版本匹配等。這是熟悉使用的技術(shù)堆棧的另一步,而且只有經(jīng)驗會(huì )告訴我為什么系統無(wú)法運行。
監控
這是我以前從未想過(guò)去做的事。說(shuō)句公道話(huà),在全職編碼之前,我從沒(méi)維護過(guò)系統。我只是搭建它們,使用 1 個(gè)星期后然后進(jìn)行下一項工作。
有兩個(gè)系統,一個(gè)有良好的監控,另一個(gè)并不那么好。我逐漸非常喜歡監控。如果我不知道 bug 在哪我就不能修改錯誤。其中一種最糟糕的感覺(jué)是從客戶(hù)那里知道有 bug。
「我做了什么?!我甚至不知道我的系統出了什么問(wèn)題?」
我認為監控由 3 個(gè)部分組成——日志、衡量標準和警報。
日志
以代碼中進(jìn)行日記記錄就像人寫(xiě)日志一樣,是一個(gè)進(jìn)化的過(guò)程。
你要找到你可能需要監控的東西,日志記錄下來(lái),運行系統。一段時(shí)間后,你會(huì )發(fā)現你沒(méi)有足夠信息來(lái)解決的 bug。這是增強日志記錄的好時(shí)機——你的代碼少了些什么?
我想你會(huì )憑直覺(jué)地知道什么東西很重要需要記錄,但是在我們的服務(wù)器中我和資深軟件工程師所記錄的東西有很多不同。我認為只要請求-相應日志就足夠了,但是他會(huì )有更多的記錄內容,比如查詢(xún)執行時(shí)間、代碼進(jìn)行的一些特定的內部調用,以及何時(shí)轉儲日志。一切都已經(jīng)解決了。
幾乎不可能在沒(méi)有日志的情況下進(jìn)行調試——如果你不知道系統的狀態(tài),你怎么重新創(chuàng )建它呢?
衡量標準和驚爆
衡量標準可以源于日志,也可以獨立于日志(例如向 AWS CloudWatch 和 Grafana 發(fā)送時(shí)間)。你可以決定你的衡量指標并在代碼運行時(shí)發(fā)送數字。
警報是把所有東西整合到一個(gè)的強大監控系統的粘合劑。如果一個(gè)衡量標準是當前產(chǎn)品中運行的機器數量,當這個(gè)數字降到 50% 時(shí),這是一個(gè)很好的警報——你知道有什么出錯了。
失敗計數高于某個(gè)閾值時(shí)?是的,又一個(gè)警報。
這里暗示了另一個(gè)需要養成的習慣。當你修復 bug 時(shí),你不僅僅關(guān)注如何修復 bug,而是你為什么不早點(diǎn)發(fā)現它呢?是否有布置警報?如何能夠更好地監控來(lái)避免類(lèi)似的問(wèn)題?
我還不知道如何監控 UI。即使吧組件測試到位,也還不足以了解出錯的情況。這些錯誤通常是由客戶(hù)來(lái)告訴我們的——這看起來(lái)不太對勁。
總結
在過(guò)去的一年里,我學(xué)到了很多東西。當我對這篇文章進(jìn)行回顧時(shí),我能夠更好地體會(huì )到我的成長(cháng)。希望你也可以從這里得到一些東西!
原文轉自:https://neilkakkar.com/things-I-learnt-from-a-senior-dev.html#when-things-go-wrong