《Software Engineering at Google》ch 1 — What is Software Engineering?

一個沒那麼肥的肥宅
今天的天空,有點藍
14 min readDec 6, 2021

前言:對於軟體工程的興趣隨著職涯的年齡與日俱增,恰好前陣子發現 Google 出了關於軟體工程的經驗談。《Software Engineering at Google》這一系列的文章是想分享我閱讀《Software Engineering at Google》這本書的筆記。透過汲取更多前人的經驗,來讓自己對於軟體工程方面的 scalability 能夠更有感觸。希望可以給自己的學習歷程留下一些什麼,也希望對想了解這方面知識的人有一些幫助。

程式設計與軟體工程之間有著本質上的不同,至少包含了時間、規模與取捨的差別。在 Google 裡,人們時候會這樣子說:「軟體工程是程式設計與時間的結合」。不能否認程式設計的確是軟體工程中相當重要的一部分,但這並不是唯一重要的事,軟體工程除了開發之外,還需要考慮修改、維護、與時間等等性質。

這裡提供一種思考時間對於程式的影響:嘗試問自己「你預期你的程式碼的壽命有多長?」。通常存在時間越短的程式碼,有很高的機率不需要思考如何適應新版本的函式庫、作業系統、硬體或是程式語言,而當「時間」這個維度納入考慮之後,「改變」這件事情就顯得格外重要。假設程式碼能夠存在數十年,那它非常有可能需要面對改變,而這正是軟體工程與程式設計最主要的差別。

造成這樣差異的根本原因是軟體產業中的永續性 ( sustainability )。專案如果是足夠永續的,所代表的意義是軟體可以在其生命週期,有能力面對不論技術或是商業理由的各種變化。

另一種對於軟體工程的看法考慮的是規模。有多少人參與?隨著時間進行,他們參與的是開發或是維護的哪一部分?一個程式設計的工作通常指的是個人的產出,但軟體工程看的更意味著團隊的合作。有鑑於此,早期也出現過足以表達這樣概念的觀點:「軟體工程指的是多個人所參與的多個版本的開發」。也因此,人與人之間的溝通也格外重要,這樣的概念在《人月神話》一書中就已強調過。

我們也能夠從複雜度的角度來區分軟體工程和程式設計之間的不同。在軟體工程中,我們經常需要評估各種可行的方法之間的優缺點,團隊的領導者更是要思考組織逐漸規模化的所造成永續性與管理的額外成本等的議題。

在軟體工程中,幾乎找不到任何一種完美的解決方案能夠通用在所有遇到的問題上。因此,Google 所學習的經驗可能不一定適用於你,但在本書中,Google 會介紹公司內數萬個工程師所嘗試去建立與維護軟體的方法,希望也能讓其他人有所獲益。

時間與變化

當一個新手剛開始學習程式時,所寫的程式碼的生命週期可能只有數小時到數天。程式作業往往只會被寫出來一次就可以交差,幾乎不會有重構,更遑論長期的維護性。這些程式碼通常在第一次落地之後,就不會重新建置或是執行。當然如果在更高等的教育訓練階段,我們可能會需要組一個團隊共同完成某個專案,這些開發者或許會需要重構一些程式碼,但是他們不太可能會需要面對太大範圍的改動。

我們也發現在軟體產業中,手機端的應用程式有較短的生命周期,新創的工程師可能會選擇投注心力在立即性的回饋而不是長期的投資,畢竟公司不一定能夠存活到能享受那些投資於基礎設施而需要時間才能發揮功效的益處。

另一方面,有些成功的專案卻可能有無限的生存週期:我們無法合理推測 Google Search、Linux kernel、或是 Apache HTTP Server 專案的終點。對於大部分的 Google 專案,我們必須假設他們能夠一直存活下去。只要他們繼續存在著,這些壽命很長的專案往往成長為與程式作業或是新創開發不一樣的樣貌。

圖 1–1 代表的正是這樣的概念。如果一項程式設計的工作只需要數個小時,那麼有任何維護性是合理而需要考慮的嗎?如果你正在做的是只需要執行一次的 Python script,那麼你需要考慮新的作業系統出來的時候該不該升級嗎 ?當然不用,這一點都不重要。但是反過來說,Google Search 就曾經在 1990 年代遇到作業系統版本的問題。

Figure 1–1. Life span and the importance of upgrades

從圖中可以看到,當程式逐漸從單次執行成長到數十年的專案,這之間的某個時間點,升級的重要性會突然增加。對於那些一開始沒有將升級的規畫考量在內的專案,這種轉變非常痛苦:工程師從來沒有對這項專案做過類似的事情,因而對那些嘗試要做升級的工程師來說經驗非常缺少,此外因為數年的累積,因此這一次的升級往往比過去平常逐漸升級的工作量要大得多。最後,經過一次這樣的經驗,企業往往會選擇要馬全部捨棄砍掉重來,要馬選擇永遠不再升級。捨棄掉痛苦固然是相當自然的選擇,但是更好的做法會是投入更多的資源來讓這件事情變得不那麼令人感到痛苦。這全部都依賴於你升級所需要的成本、其所帶來的價值、專案的預期生命週期。

挺過第一個大型的升級,並且能夠穩健地繼續向前行,才是一個專案長期永續性的精隨。永續性需要的是對於必然的改變有縝密的計畫與管理。對於 Google 裡面的許多專案,我們相信經過了非常多次艱辛的嘗試與錯誤之後,已經逐漸達成這項目標。

具體來說,短期的程式設計與較長的產品生命週期之間的差異到底在哪裡呢?隨著時間的推移,我們必須更仔細察覺介於「剛好可以運作」以及「可以維護」之間的差別。關於這些議題很難有完美的答案,畢竟,保持軟體長期的可維護性確實是一項艱鉅的挑戰。

Hyrum’s Law

倘若你正在維護某個會被其他工程師使用的專案,那麼 Hyrum’s law 可能會是個相當重要的事情。

With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.

指的是「當一個 API 有足夠多的使用者時,其所保證的事情就不會太重要,因為系統中所有可被觀察的行為都會被某個使用者所決定。」

在我們的經驗中,這是隨著時間經過而改動軟體的主要因素。這個定律是個相當實務上的規則,作為一個 API 的擁有者,你或許可以多少在介面上取得一些彈性跟自由。但是實際上,某個改動的複雜度以及困難度取決於使用者能夠找到某些該 API 中可以觀測的行為有多少用處。如果使用者不能夠依賴於此的話,那麼你的 API 將會變得相當容易改變。

Hash Ordering

大部分的工程師都知道 hash table 並不是明顯有序的。因此,若你詢問「我能否假設我的 hash 容器能夠輸出特定的序列」,有些專家可能會回答「不行」,但是更精確的回答是「如果你的程式碼壽命很短,在硬體、語言特性、資料結構等等都不改變的狀況下,你的假設是正確的。但是如果你無法知道程式碼的壽命有多長,那麼你就不能這樣假設」。這其實就是關於「可以運作」與「正確」的差異。

當在程式設計時,多考慮「目前可以運作」以及「永遠可以運作」,我們就能夠看出某些清楚的關係。如果將程式碼視為高度可變的人工產物,那麼可以將程式設計的風格進行分類:那些依賴脆弱且尚未發布的 features 的程式碼,通常會被視為 hacky 或是 clever,而如果遵循最好的實務原則以及對於未來更具有規劃的,才是真正 clean maintainable。這兩者的存在都有其目的,但工程師該如何選擇則是與該程式碼的生存週期所有莫大的關係。如果 clever 是讚美的意思,那麼代表這代表的是「程式設計」的範疇;然而在「軟體工程」中,clever 更像是一種負面的指控。

為什麼不能將目標設定在「不要改變」?

如果你的某個專案完全都只用 C 語言並且完全沒有其他外部的相依姓,你可能有一點機會避免任何形式的重構或是痛苦的升級,畢竟 C 語言存在的目的之一,就是提供了穩定性。

然而大多數的專案都會與當時空背景所使用的科技有關連。大多數的程式語言也比 C 經常容易改變得多,每一個你所使用的科技元件,從處理器、網路函式庫到程式碼,可能都有某種程度上的安全問題或是嚴重的 bugs。

效能的進步還會更進一步加深這種情形。我們想要將資料中心配備更具有成本效益的設備,像是提升 CPU 的使用效率,然而,早期 Google 所使用的演算法與資料結構在現在設備上卻沒那麼有效率:linked-list 或是 binary search tree 可以良好的運作,但是 CPU cycles 與記憶體延遲之間不斷地擴大,影響著程式碼的「效率」。即使向後相容性能夠確保舊的系統能夠正常運作,卻不能保證舊的最佳化仍然是有幫助的。

上述提到的議題就是為什麼沒有投資在永續性的長期專案所會面對的風險。我們必須有能力去面對這些議題。改變不必然是好事,不應該為了改變而改變,但我們得有能力面對改變。

規模與效率

考量程式碼倉庫的永續性:「當你的組織的程式碼倉庫是永續時,代表的是你有能力安全地改變任何你應該要改變的事物」。如果產生改變伴隨著的是超額的成本,那麼這樣的改變很有可能被延遲發生,當這樣的成本隨著時間經過而超越線性的增長時,就代表遇到了規模化的問題 ( 在這裡,可以規模化指的是相對於人類互動而言小於線性地增長 )。當你的專案規模成長兩倍時,你是否需要兩倍的工時?

人力並不是唯一需要規模化的有限資源。正如同軟體本身在計算、記憶體、儲存、頻寬都需要規模化,軟體開發也同樣需要規模化。這不僅包含程式設計師投注的時間,還要考量能夠加強開發流程的計算資源。如果計算成本的成長已經超越線性了,那麼你很有可能處於一個不夠永續的狀態。

程式碼庫,作為軟體組織裡面最珍貴的資產,同樣也需要面對規模化的議題。如果你的建置系統或是版本控制系統超越線性地擴增,那麼到某個時間點你很有可能無法應付。這些問題像是「建置的時間要多久?」、「拉下一個全新拷貝的專案褲要多久?」、「新版本的程式語言升級的成本是多少?」,這些問題像極了溫水煮青蛙的情形:問題慢慢惡化而且不會在某個時間點才突然顯現,唯有對規模化有足夠認知的人才有可能關注這些議題。

無法規模化的策略

其實,不需要太多的實務經驗也能夠快速察覺到哪些策略是無法規模化的。大部分的時候,只需要思考某位工程師的工作份量,並且想像組織擴充 10 或是 100 倍的狀況。當組織變成 10 倍大時,這位工程師是否需要多做 10 倍的工作?工作內容是否會與程式碼倉庫一樣倍數增加?如果以上問題的回答為「是」的話,我們是否有任何機制協助自動化或是最佳化那樣的工作內容呢?如果沒有,那麼有相當高的機率需要面對擴充性的問題。

傳統的分支開發方式也會隱性地遇到規模化的問題。一個組織可能會發現 merge 巨大的 features 進入主要分支的時候,容易造成產品的不穩定性。領導者可能會主張「merge 的時候我們要有更嚴格地掌握,我們因此應該減少 merge 的頻率」,進而確保任何的分支應該要足夠「完整」才能夠被 merged 進去主要分支,因此可能會多做一些額外的工作來確保。這樣的分支管理方式可能適合小組織,但當組織擴張且分支數也擴張時,將會看到一些不段增加的額外付出成本來完成相同的事情。我們將在第 16 章看到為了應對規模化,較佳的另一種分支管理方式。

可以規模化的策略

那麼有哪些策略可以讓使得組織的擴充性增加呢?或是說,哪些策略可以提供相對於組織變大來說,超越線性規模增長的價值呢?其中一個我們喜歡的方法是給予 infrastructure 團隊更多的能力,並且保護他們的能力能夠讓 infrastructure 安全地改變。這有點像是後面章節提到的 Beyoncé Rule:「如果你喜歡,你就應該為他加上 CI test」。

我們還發現了分享與溝通會協助組織規模化。工程師在論壇上面討論與回答問題,知識就很容易散播出去,新的專家就會出現。如果你有一百個工程師在撰寫 Java,那麼一個樂於助人、願意回答問題的 Java 專家可以馬上讓這一百個工程師寫出更好的 Java 程式碼。我們將在第 3 章節進一步討論。

左移

考慮一個開發者對於在軟體開發中所會經歷的時間軸,由左至右包含從概念、設計、開發、測試、提交、部署等等,當能夠在越左邊就找到問題並修正,成本就越低,這樣的概念可以用圖 1–2 表示。

Figure 1–2. Timeline of the developer workflow

雖然這個詞彙似乎一開始是用在不應該將安全性推遲到開發的最終階段才進行考慮,但是這樣簡單的概念也會在本書中出現相當多次。由靜態分析以及程式碼審查所找到的 bugs 會比在產品量產階段找到的 bugs 花費要更少,因此提供那些強調品質、可靠性與安全性的工具與模式是我們基礎設施團隊的一項重要目標,希望藉由這些工具協助大家能夠盡可能在越左側就找到不足之處。

取捨與成本

什麼是成本?「成本」所指的不只是錢而已,還包含了資源的成本 ( ex: CPU時間 )、個人的成本 ( ex: 工程實作的精力 )、機會成本 ( ex: 不採取這項行動的成本是多少 )、社會成本 ( ex: 這個選擇會對社會產生的多少影響 )… 等等。

白板筆或許是個典型的例子。在相當多的組織中,白板筆被視為相當重要的物品,他們的數量相當稀缺,即使有也可能沒水而無法使用。在會議中你有多常因為缺乏白板筆而造成困擾?你有多常有很多想法卻因為沒有白板筆而無法釐清思緒?在 Google 的工作區域附近,像是白板筆這類的辦公用品通常會放在未上鎖的櫃子中,不用太多的注意就可以容易找到各種不同顏色的白板筆。因為我們認為腦力激盪時不要被這種小事分心會比走來走去只為了找適合的白板筆還要重要得多。

我們總說,「Google 擁有數據驅動的文化」。事實上,那只是個簡化:即便有些時候沒有數據,仍然會有證據前例參數可以作為考量。做一個好的決定,就是好好地衡量可以得到的輸入並且據此作出取捨合理的決策。因此,在一個工程團隊中,大部分的決定應該要遵從下面的原則:

  • 我們這麼做是因為我們必須要這麼選擇 ( 合法的需求、客戶的需求 )
  • 我們這麼做是因為這是根據當時的證據所能做得最好的選擇 ( 透過適當的決定 )

而不應該是由於「我們這麼做是因為我說的」。

做決策的考量

當我們在衡量資料的時候,我們發現通常有兩種情況。第一種是都能夠被量測或預測的數據,這通常伴隨著取捨,如 CPU 與網路、美金與 RAM、或是兩周的工程時間與資料中心的 N 個 CPUs;另一種則是較為隱性或是不知道怎麼量測的資料,如「不知道需要花多少的工程時間完成這項任務」、「一個設計不良的 API 所造成的工程成本」、或是「對於選擇某個產品的社會影響力」。

對於第一種,任何一個工程師都可以且應該進行分析。「如果我花了兩周將這個 linked-list 改成較高效率的資料結構,我將會多用 5GB 的 RAM 但能夠省下 2000 個 CPUs,我該這樣做嗎?」,注意到,這其中還需要考慮到人力成本 ( 工程師兩周的支援 ) 以及機會成本 ( 在那兩周中,該工程師可以做什麼哪些其他事情 )。

至於第二種情況,通常沒有絕對的答案。我們依賴於經驗、領導精神、過去的前例作為參考。儘管投資了相當多的研究在將其量化,但必須要了解的是,並不是任何事情都能夠被量化的。這些通常都很重要,卻較難以管理。

重新審視決策與錯誤

數據驅動 ( data-driven ) 文化的其中一個好處是承認錯誤的必要性。每個決策會在某個時間點根據當時可獲取的資料產生,然而隨著更新的數據出現,情境跟著改變,之前所需要做的假設不復存在,一個決策是否有錯誤,或是決策的當下相當合理但時間過了之後而逐漸不適合的情形,也會逐漸明朗。這對於壽命較長的組織尤其重要:時間不僅僅會驅動技術或是軟體系統的改變,對於數據的改變同樣會影響決策。

結論

本書中將討組織或是個人該如何讓軟體提高可維護性的策略與工具。Google 花了相當多的精力來保持這樣的程式碼倉庫與文化,這可能不是唯一的一條路,但確實能夠透過實例證明其可行性。我們希望這能夠導引出更廣泛的討論:你該怎麼樣維持程式碼能夠繼續運作下去?

--

--