Microsoft微軟工程師三年 — 雜想

Tobby Kuo
30 min readMay 23, 2022

--

翻了一下,距離上次寫 medium 也是接近三年了,幾乎就是開始工作後就直接停更了。

工作三年,非常的忙,但也隨手在自己的記事本上記下了很多零散的心得,曾經試圖把一些想法組織起來放在自己的 Instagram,方便日後回來查看,不過 Instagram 終究不是一個太適合做筆記跟存放長篇文章的地方。

在考慮其他存放筆記平台的同時,也一直覺得自己還沒準備好把這些各式各樣處於不同階段的想法放在公開的環境,但同時也很感謝網路上的各種前輩們的經驗分享(有點資訊焦慮的我可以說是看遍了無數中英文的網路資源,手機裡的統計就看了上萬篇的技術文章或軟體工程師心得文),於是還是決定趁最近工作狀態有些轉變的空檔,搬運一些想法上來 medium。

從學生的身份到工作三年,在公司也從菜鳥變成了 Software Engineer 2,從沒有團隊合作和產品經驗到現在能執行 溝通→設計 →實現 →測試 →部署→運維 的一套完整流程,不管是對程式語言、軟體架構、工程實務、團隊合作、系統設計… 等能力都大幅度地進步了一輪。要寫的話其實每個方向都想寫上不只一篇(光是前陣子花上一週讀完的這篇 Performance Improvements in .NET 6,就覺得對我的啟發超大,不吐不快),但超佛系更新的我也知道這不現實,寫一些經驗分享我也還不太確定怎麼拿捏界線,於是決定從我認為門檻比較低的開始,把一些沒有特定主題的雜想集結成一篇文章。

這篇雜想整理了我的筆記中一部分非技術相關內容,可以粗略地分成三種類別,一類是我對於軟體產業的觀察、一類是作為工程師對於生活周遭的想法,最後一大類是我認為優秀軟體工程師與一般軟體工程師的區別以及現階段我想努力的方向。有些內容會有點跳,主要是跟我的思維模式有關,很多時候我的思維都是比較發散並且做比較多聯想的。

抽象化的能力

關於抽象化能力之於工程師的重要性,很多文章都能看到,我尤其覺得 vgod 的追求神乎其技的程式設計之道 特別精彩。

即使我對於抽象化這件事的描述可能沒法這麼精彩,我也想透過自身的例子來反映我是何時開始逐漸瞭解到抽象化對於電腦科學或是各種學科的重要性、以及在工作中我體會到了哪些抽象化的應用。

抽象化與模組化這兩個概念有很多共同點,對我來說他們都做了所謂的「封裝」,封裝在電腦科學乃至其他領域都隨處可見,我認為封裝能帶來兩個比較明顯的好處:

  1. 幫助人類在有限的思考能力內(應該說是電腦跟人類有各自擅長的方向,人類更擅長擁有全局觀的高層次分析與思考,而電腦則不會漏了任何一件細節,而且計算超快不會失誤),將事情簡化來幫助理解。比如說我們可以封裝出一個在網頁前端做會員登入的方法,檢查瀏覽器有無 access_token 或 refresh_token,以此得出合法的 access_token 或者把用戶導向登入頁面。我們在設計業務邏輯時,就可以將流程簡化成這邊需要一個會員登入,而不是把這些複雜的細節加入高層次的設計之中。也就是 vgod 的文章里提到的,工程師們可以在不同的抽象層次上溝通並且根據需求切換到不同層次,在不同層次內不斷豐富討論細節。
  2. 透過達成共識的範圍切分,增加現代工程師協作軟體開發的能力。在學生時期自己寫程式還有在軟體公司工作的一大區別就是規模,一個人寫不出上千萬行的程式碼,但是一千個人可以,而在工程師規模擴大的同時,除了前後端對於接口要有清楚的定義跟切分,組與組之間或是任務與任務之間也需要有概念上的接口還有負責範圍的切分。藉由對職責的封裝,就可以避免很多程式新手的惡夢,改了A壞了B,修了B又壞了A,各組或是各人只需要保證滿足接口定義的行為即可。

講了這麼久的封裝,再來談回抽象化跟模組化。對我來說,抽象化更多時候是垂直分層的,一般常見的比喻就是金字塔或是千層派,這個層級的概念可以是被制定標準的,比如說常聽見的 OSI 網路七層模型,也可以是根據不同人的不同觀點而異,我的一層可能是你的兩層。而模組化很多時候是在一個抽象層內透過職責區分不同的功能模組,也可以是跨越了多個抽象層的一個功能的群組,比如說手機的相機模組,同時包含了光學、電子電路、驅動、影像處理這些不同的層次,但是服務於同一個功能。

在寫程式的過程中,很多抽象化都已經在不知不覺間發生了,而這也是我覺得抽象化最神奇的地方,透過這些電腦科學家們以及工程師前輩們精妙的抽象化設計,一個程式新手可以在不懂編譯器、作業系統、以及 CPU 指令集的情況下,寫出滿足自己需要的 Python 程式並且成功運行。即使對於一個經驗豐富的工程師來說,也得利於抽象化而能夠只專注在自己想專注的領域,比如基於 gRPC 開發一套複雜的分散式微服務系統,也是建構在已經存在已久的 TCP 協議之上,進而不用去煩惱網路掉包等問題。

在抽象化能力上,我覺得可以簡單區分成兩個維度:

  1. 理解現存抽象層的能力:在公司與一個很資深的 IC(Individual Contributor,就是非管理職的工程師)聊天時他跟我提到,一個工程師的成長並不來自於他學了多少新技術或框架,也無關他寫了多少程式碼,而是這個工程師能不能時常問為什麼,以及是否時常花時間思考這些技術與框架背後的本質。 如果不能夠了解這個技術或框架在試圖解決哪個抽象層次的什麼問題的話,可能就會陷入無窮盡追逐新技術的循環。而理解了這個層次存在的目的、以及這個層次目前面臨有哪些難題、還有這個層次現存的解決方案後,學習新出現的技術就可以更快的做出一些觀念上的映射,也能夠有比起其他人更深入一層的理解。比如說我們從早期靜態的前端網頁發展過來,jQuery 解決了什麼問題,又為什麼需要像 knockout.js 這樣的資料綁定框架,而 React 的 virtual DOM 又為何崛起,以及怎麼最近 Next.js又談回 SSR(server-side rendering)。在有了一定的理解抽象層能力後,我認為這個工程師就有能力寫出更好的程式碼,即使還是寫寫簡單的 console app 或是爬蟲程式,也因為能夠注意到更多被隱藏起來的細節,而寫出更有執行效率或是更與想像一致的行為(很多程式的問題就是來源於「跟我想的不一樣」,我認為大多是因為不了解哪些細節被隱藏了所致),而很多時候遇到了 bug 或是沒見過的報錯內容,也可以在 troubleshooting 的過程中更容易定位到出現問題的層次以及解決的方案(起碼更知道怎麼下搜尋關鍵字!)。
  2. 設計抽象層的能力:雖然一般來說,一個軟體工程師的工作內容並不會跨越太多層級,但是在自己所在的抽象層內,也需要更細緻的抽象分層而達到好讀、好維護、好協作、不易出錯的效果。Android 開發的 MVVM、網頁開發的 MVC、常見的 design patterns工廠模式觀察者模式、時下很流行的容器化技術、還有從多執行緒計算到分布式計算最近又跑回瀏覽器渲染的 actor model,我認為都可以想像成是長久經驗累積出來能滿足通用場景的分層設計。而做為最理解每間公司業務需求的工程師,選擇出合適的分層設計亦或是自己動手設計就是工作內容中很重要的一環。實務上我覺得還挺難的,常常受限於自己的經驗有限,設計出來的架構能滿足當下需求卻不能很好地服務未來產生的需求,而導致在不洽當的地方補上不洽當的程式碼,寫起來不太暢快也不好梳理邏輯。在設計軟體架構時,我覺得一個比較可以遵循的大原則是單一職責原則(Single responsibility principle),大模組負責一個大的邏輯上職責,而裡面的小模組負責小職責,單一方法則負責一個邏輯上不可分割的單元,當有一個模組沒辦法用一句話簡單的描述他的職責時,不僅代表設計上可能不適當,也代表跟同事溝通時無法確保對方跟你 on the same page。

對於運維的敏感度

在閱讀 Software Engineering at Google 一書時,裡面提到了一個聽起來很簡單但是卻啟發我很多的概念,也就是 Programming 跟 Software Engineering 的區別,主要就是對於程式生命週期的期望,Programming 關注的是怎麼寫出好程式,通常比較不在意後續怎麼維護這個程式,而 Software Engineering 之所以能變成一個工程領域,差別就在於這些為了延長軟體生命週期而發展出的各種工程實踐。書中提到,在就學時期,寫的程式基本上都活不過一個學期,而軟體公司發行的服務或軟體,基本上都是希望能夠無止盡的一直運行的。在 vgod 的其他文章中有提到,在 scale up 方面,要寫出一個長得像 Facebook 的網頁對於很多工程師來說是很容易的,但是要把規模擴大到幾十億的用戶就變成一件極其複雜的事情,我想這個道理也能應用在生命週期上,要寫出一個能運行五年十年的軟體,難度比起活一個學期的要難上不只幾十上百倍。很多人包含我,在上學時期寫程式時是不用考慮升級套件、函式庫的問題的,也有經歷過升級套件引發一系列連環報錯的惡夢,於是總是想著能運行得好好的則能不升級就不升級。而現實是,工作中很多時候會「被迫」升級套件,比如說選用的版本被通報了一個新的安全漏洞,或是隔壁組引用的套件依賴了一個新的版本等等。除了升級套件外,還得不斷的迭代新功能的同時不影響用戶(很多我們日常使用的產品如 Google Search 或 Facebook 都是一天不只部署一個新版本的!)、演練不曾發生過的流量高峰、並且開發團隊要能容忍人員的來來去去,於是乎,學會怎麼應對軟體在整個生命週期中遇到的各種狀況,就變成了一個非常重要且困難的議題了。

對於工程師而言,處在運維階段的時間(或是指程式跑在線上的時間)是處在開發階段時間的數百倍,而運維這個概念通常卻是出了學校之後才開始學習的,在上學階段根本很難想像 oncall(值班)的存在,至少對我來說至今仍然覺得像 Microsoft, Google, Facebook 等科技巨頭隨時都有上千個工程師守在電話前等待(?)問題發生是一件很魔幻的事。所以很可以想像的是,不同工程師之間對於運維的理解還有能力上的差距是非常巨大的,至少對我來說,能在設計或是開發階段就考量到日後運維難易程度的工程師就是非常優秀的。

我認為關於運維這件事,其實也是有個很完整的流程的, Google 也在運維這件事上發展出一個很完整的領域跟科學,稱為 Site Reliability Engineering (SRE)。廣義上來說運維包含了很多領域,包含如何確保開發環境跟線上環境盡量一致(還記得那些在筆電上開發完部署到 linux 機器上的各種問題嗎?)、持續整合與持續部署(CI/CD)、軟體遙測(Software Telemetry,泛指如何在本地或遠端收集系統的運行指標跟日誌)、即時警報、壓力測試、紅白隊演練、供應鏈安全… 對於很多領域我也不是很了解,但我也想以自己比較有經驗的軟體遙測為例,舉出我對一些工程師運維能力方面的觀察。

我之前所在的團隊主要負責 Outlook/Exchange 後端系統分布在全世界幾十萬台伺服器上面軟體遙測的運行與管理,基本上業界流行的軟體遙測都是採用分層的設計,延時越小(秒級別或是小於秒級別)的遙測數據傳輸量越小,而延時越大(小時級別或甚至天級別)的遙測得益於錯峰、壓縮、批次處理的幫助能夠傳輸更大的數據量。並沒有哪種遙測數據更重要的說法,只能說是不同類型的遙測適合不同場景,延時小的數據往往需要更精準地事先定義好用途與採集方式,但能夠讓我們對於正在發生的事情有很快的應對,多作為報警用途,而延時大的數據則可以保留下更詳細的資訊,事後使用的彈性很大,多應用於分析的場景。

因為所有問題放到分散式系統裡都會變得很複雜,所以我先把軟體遙測舉例限縮在用筆電開發、部署到一台虛擬機上。最開始寫程式時,console.log() 或是 print() 總會淪為列印 Hello World 或是中間運算結果的工具,很少會考慮使用這些工具來幫助理解程式的實際運行狀況,甚至沒用過或見過 console.err() 或 Logger.Warning() 等方法。而很多人(對,這些都包含我)第一次的程式遙測經驗,大概就是寫好程式後興奮地部署到了線上,卻因為某種沒考慮到的邊緣情況(edge cases)程式異常地結束了,而因為在自己的筆電上沒法重現這個情況,於是在程式中加入了一些「檢查點」列印出當下的狀況,再透過 ftp、ssh 到機器上或是使用雲平台提供的交互介面查詢這些列印結果。這樣的行為其實比較容易導向一個負面的循環,也就是 程式崩潰→加上日誌→發現問題→修復問題→新的程式崩潰。而在軟體產業中,如此常見的程式崩潰當然是比較不能被接受的,一個擁有運維經驗和敏感度的工程師便能夠根據可能會發生的意外,配置好相應的遙測以及規劃意外發生時的自動(error handling 或 auto-healing)或人為處理流程。

在現在工程界一個比較常聽到的說法就是,永遠要預期每台機器、每個零件、每個程式、每個 system call 都有失敗的可能,我們的目標不是設計出一個完美不會失敗的系統,而是設計出一個有能力容忍失敗(fault-tolerant)的系統。在電腦科學的領域我們發展出了很多方法來避免單點失敗(single point of failure,SPOF,指的是系統裡的單一個模組故障導致整個系統故障),而對於身為工程師的我們來說,利用自身的經驗和素養來預期可能會發生的故障並針對其設計出更可以被運維的系統就是能力的一種展現。

在公司裡,其實也很常見遇到了錯誤才補上日誌的狀況,但也有時候能見到極度少見的錯誤被 CI 系統抓出來、系統異常後透過自我修復回到了健康狀況、或是看到某個很有經驗的工程師配置的日誌很完整的反映了數十萬台機器上一天只有幾台機器發生的問題,這時候就會不禁讚嘆對方在運維方面的能力,也會慶幸自己有這麼可靠的同事。記得有個主管就跟我說過(記不清楚原話了),大家都能寫出很漂亮跑得很好的程式,但遇到問題就是見真章了。

我想像中的工程師

有趣的是,我大學唸的是土木工程學系,畢業後大部分的同學也是做工程師,上學的時候教授也教我們怎麼當工程師。於是我時常在想,我現在做的軟體工程師跟土木工程師,在本質上有什麼相同點。其實這樣子的思考對我的幫助滿大的,我能夠在兩個比較熟悉的領域裡做一些概念上的比較,常常刷新我對於軟體工程師的認識。

以前總是聽到,工程師看重的是解決問題的能力。當然這句話我現在覺得不是這麼精確,因為設計師(平面設計師、工業設計師、產品設計師…)的工作本質上也是解決問題,但我覺得可以作為一個探討工程師的出發點。我想嘗試把工程師的工作定義為「以現存的工程手段解決商業上的需求」,而這個定義中不管是工程手段還是商業需求兩項都是不可或缺的。

在工作中有個很有趣的觀察,就是你不管丟給軟體工程師什麼問題,他都會嘗試用寫程式的方式來解決問題。如果有個相對不合理的用戶需求,大部分的工程師會透過設計出一個超級複雜的系統來滿足需求(所謂 over-engineering),但是同樣的問題丟給產品經理,則可能會通過與客戶溝通來把需求改的相對合理一些。我想表達的是,用工程上的手段解決問題自然是我們這群工程師的天性以及舒適圈,但作為一個商業公司的員工,也必須了解到沒有什麼系統是沒了商業需求後還需要存在的。

工程師中的「工」字,我覺得可以對應到工業界的「工」字,進而跟學術界做出對比。在工業界是相對沒有新東西的,很多人都說工業界在流行的「新技術」多半落後於學術界的研究十年或以上,但工業界有趣的地方就是他的目的是服務於人們的需求,應用的領域永遠是在第一線的、並且是更有彈性的。在 BYVoid 的這篇 在Google的這四年 中也提到了這一件有趣的事,很多在學術界裡非常難以攻克的難題,在工業界可以被「降維」到能被解決的程度,因為在工業界是不需要證明一個做法能在任何情況下都有效,只需要在用戶會產生的情況下都有效就夠了。

在我的工作經驗中,沒有一個軟體工程師做的工作內容是獨一無二的,充其量都是「在我們公司的商業目的下也做一套跟別人很像的系統」,我寫 CRUD 他也寫 CRUD,我做搶購系統他也做搶購系統,我們搞一套分散式存儲他們也搞一套分散式存儲。沒有人是從零到有打造一套從未有人見過的系統(當然排除那些最最頂尖的工程師們),相對地,大部分人的工作都是找出現有的組件中適合自己公司場景的,拼湊出一個能用的系統,在上面添加一些業務邏輯,並隨著時間的推進慢慢的把這一套系統變成最適合自己公司的系統(但也只會有這麼一套系統,我們永遠也不會知道平行時空裡會不會自己已經架出了另一套更合適的系統)。在這樣的描述下,工程師的任務確實就是「以現存的工程手段解決商業上的需求」,而不會是研究出一套新的理論或是打造出一個跟商業無關的超強系統。也很直觀地,一個工程師認識多少現存工程手段(這個認識講的是比較深入的,包含這些手段的前提假設、長短處或是存在的目的)以及選擇手段的判斷力也就決定了這個工程師解決問題的能力。

在這篇講述領域設計(Domain-Driven Design)的文章 關於 Domain-Driven Design 以及他的魅力 中,這句話我覺得也能很好的描述工程師的本質:

在這個時代,寫程式從來沒有那麼簡單過,但要解決的問題也空前的困難。

很多經驗更豐富的工程師都會說現在是 low-code 時代,在現在高階程式語言、雲服務、sever-less 概念、容器化… 的幫助之下,很多工程師可以在寫很少的程式碼或甚至只在網頁上拖曳物件就完成一個完整的系統(近半年我的很多工作都是在 Azure Data Factory 上點點按按就完成的)。但這並不意味著現在當工程師就變得多簡單,雖然我們手中的工具進化了,但我們要解決的問題也隨著時代越來越複雜了。比如說開發一個 Android app,現在回頭去看七八年前的 app 多半會覺得看起來不太好用,但當時也不覺得不好,現在人們對於 UX(User Experience)的標準高了很多,Infinite List 導致手機卡頓、用戶資料在本地的加解密保存、畫面渲染阻塞了用戶互動等更複雜的問題都可能是公司用戶留存率下降的原因,也是我們透過更好用的工具解放了雙手後需要解決的核心問題。

選擇的能力

前面提到了工程師在日常工作中會花上很時間在做選擇,也提到了做選擇是需要相應的能力與經驗的。畢竟如果方案都定下來了,我不認為擁有五年經驗的工程師與二十五年經驗的工程師在執行力上會有太大的差距,但定方案這個行為本身,就會牽涉到這位工程師手上選項的多寡、對於需求的認識、過往的經驗、對未來的分析能力、還有判斷力。不好的系統設計很容易帶來越滾越大的技術債、處處可見的漏洞、特別易見的系統瓶頸、甚至是之後打掉重練的命運。而在一個設計良好的系統上工作,不僅可以很順利地在上面迭代新功能,還可以在出現問題時有更直觀的解決方案。

所謂選擇,就意味著沒有最佳解的存在,往往需要結合當下的時空背景才能做出判斷。工程師們很愛把「trade-off」一詞掛在嘴邊,就是你拿了什麼代價去交換的意思,我覺得一個很常見的例子就在資料/事件的流處理(stream processing)上,如果不能忍受丟失資料可能就得忍受資料重複、如果不想丟資料又不想重複資料就要接受性能上的損失、如果要提升系統的吞吐量(throughput)就得犧牲每一筆資料的延時(latency),魚與熊掌。又或者是分散式系統裡面的 CAP 理論,想要資料的強一致性(strong consistency)就得在某些時候透過分散式鎖或是 2PC(2-phase commit)等機制讓系統退化到較少的並行度(Parallelism)跟可用性(Availability)、追求高可用性的話多半就得接受最終一致性(eventually consistency)對於用戶的影響。

剛進公司時還沒有太多參與或是主持 Design Review 會議的經驗(以及寫 Design Doc 跟做系統設計的經驗),網路上也很少相關的資源(很有趣,你能搜到上百篇如何做 Code Review 的文章,卻沒有幾篇如何做 Software Degisn Review 的文章),所以我也是慢慢摸索出怎麼做這些事,當然我的同事們也都是。前陣子在主管們的鼓勵之下我主持了一場橫跨多組的討論會,主題就是「如何做好 Design Review」。裡面有些怎麼做 Design Review、或者說怎麼做選擇的重點很值得參考:

  1. 你的 Goal 與 Non-Goal:在開始設計階段時,如同前面關於抽象化所提到的,劃分範圍(Scope)是非常重要的。時常我們會以為我們對於這個專案的目標已經很明確了,但其實並不然。比如說我們知道這次專案的目標是引入 CDN (content delivery network) 來改善用戶在前端網站上的 TTI(Time to Interactive),但是我們知道怎麼樣算是專案已經做完了嗎?TTI 降低了 20% 算達成目標了嗎?又或者我們網站 TTI 的主要瓶頸沒有辦法透過引入 CDN 來改善呢?關於制定目標有很多不同的方法論,可以設置具體的 use cases、預先定義量化指標… 這邊就不繼續探討。而 Non-Goal 也是一個我比較近期才接觸到的觀念,這邊放我一個搜尋到覺得不錯的定義,A potential goal or requirement which is explicitly excluded from the scope of a project. 定義範圍內要做什麼很重要,定義範圍內不做什麼也很重要,適度的限縮專案的範圍可以有助於之後做出更精確的決策,才不會陷入這也好那也不錯的窘境。
  2. 你的 Core Priorities:制定好專案的範圍後,釐清大家心目中的優先級這個步驟也是很必要卻很容易被忽視的,這個步驟會很大程度地影響到之後的考量點,比如說著重實現難易度的方案可能就會跟著重成本的方案截然不同,繼續上面引入 CDN 的例子,很多時候我們很難說明如果引入 CDN 會導致需要加上超級複雜的資料同步邏輯或是成本超級高是不是一件能被接受的事。而且比較尷尬的是很多「優先級」是不會被直接提出來的,但是卻又無形中影響著我們的決定,比如說人力(engineering efforts)或預算,很多時候我們選擇這個設計是因為對於我們來說實現相對簡單,或是使用的人月(people-month)是主管能夠接受的,但因為我們沒有在討論之初明確地提出對於人力的考量,而導致之後彼此之間有意見分歧時更難說服或理解對方。
  3. 你的 Stake Holders(利益相關者):上面提到了做選擇之前得先釐清「大家」心目中的優先級,而這個「大家」又是個很模糊且容易有爭議的描述。有時候會遇到組員不理解組長做的決定、或是已經設計完畢到了實現階段才有人反對方案(經典 behavior question!)的情況,歸根究底都是因為沒有在適當的時間點找來適當的人一起討論。我認為在做選擇或是做 design review 時的一個重點就是討論,要是沒有來來回回而是設計者的所有提議都能被接受的話,就很難稱之為在做選擇。而討論的主要目的其實就是讓大家互相交流不同的想法和立場,讓我們在盡量擁有比較多資訊(包含前提假設、限制、前人的經驗、現有方案…)後再做出一個能基本滿足各方要求的決定。對於如何明確定義一個專案的 Stake Holders 以及適當的與會人員,我目前還沒有一個比較好的想法。

處處都是建模

建模(modeling)對於我來說一直是一個很獨特並且也不斷在刷新的概念。我對於建模這件事的最初印象就是在撰寫後端服務的時候那些 model 資料夾底下的一個個 class 檔案(不知道大家有沒有跟我類似的經歷),學生時期跟著網路上的教學一步步做時,就曾遇到這樣的步驟去對應資料庫裡一張一張的表建立一個一個的 class 檔案,那時也並不知道這樣做的原因是什麼。而在學習 OOP(Object-oriented programming,物件導向程式設計)的書本中也很常見 vehicle → wheeled vehicle → car → toyota 這樣的例子(其實我一直覺得這例子很爛,真的沒辦法幫助初學者理解寫程式時 OOP 扮演著怎樣的角色),也是很多人對於建模的初次探索。

隨著寫的程式和看的書越來越多,慢慢能夠體會建模這件事對於寫程式乃至電腦科學的重要性。如同前面提到的,寫程式的目的是為了解決在現實生活中的問題,而建模這件事就相當於銜接現實生活中存在的事物以及他們的行為和程式邏輯間的橋樑。如果今天我們想要模擬某條道路在不同車流量下的平均車速,我們不太可能只把一輛車當成 x, y 座標上的一個點,我們會希望給這個代表車子的物件(object)賦予他的長寬高,並且給他設定紅燈停綠燈行這樣子的行為,慢慢地,在我們追求模擬準確度的同時我們的程式也越來越貼近現實,有了根據不同時段而有不同模式的紅綠燈、有了限制不同的內外線車道、有了天氣等等外在條件,這就是建模。

我認為物件導向一部分就是指寫程式時用這種建模的方式來思考,而且很多時候我也覺得建模這件事跟 OOP 是相輔相成的。有時候我們在 OOP 程式語言的能力範圍內去做建模,並且我們的思維模式也被這些定義好的 OOP 模式(封裝、繼承、多型等等)引導,有時候人們對於建模這件事有了新的理解,OOP 程式語言也會在新的版本中加入新的功能/標準。因此,我覺得有時候光是在使用 OOP 程式語言如 C++, Java 或 C# 時,探索程式語言本身的標準都能夠提升自己的建模能力,並不是說每個語言看了看 for loop、if-else、宣告變數後就是熟悉了這門語言,很多時候去了解這些語言的設計方針才是更重要的。比如說當你在看 Java 裡面的這條繼承關係 Object → Collection → List → ArrayList,以及你看到 ArrayList 實作了 Serializable, Cloneable, Iterable, RandomAccess 這些介面後,透過比較 List 和 ArrayList 的相同相異之處或是比較其他實作了 RandomAccess 的類,也能去體會這門程式語言的設計者對於這些不同資料結構的想法。

建模這觀念有時候對我來說很具體,有時候又非常的抽象。具體的例子有很多,比如說電影劃位系統、Uber 即時顯示車輛位置等,我們嘗試把現實生活中實際看得到摸得到的東西嘗試用一些簡單的資料型別(string, integer, float 這種 primitive type)拼湊成一個電腦世界裡的物件,適當的抽取重要資訊並去掉雜訊(很多時候我們只在乎計程車在哪個路段上的哪個位置、不在乎他在哪個車道上或是這台車有多大),然後裝到適合它的地方(memory or disk, sql or no-sql)以利後面的運算或是資料傳遞。

抽象的例子則很有趣,有些東西我認為是電腦科學教了我怎麼去認識這個世界的,比如說在社群平台上,人與人的關係可以被表達成一個巨大的 graph(就是由節點與線組合成的資料結構,每個節點跟線都能有不同的屬性),當然我們可以從很多不同的視角來認識這個關係網絡,而 graph 只是其中的一種。在我學習 graph 之前,我可能也會有比較模糊的想法去認為人與人之間的關係可以連接成一種網狀結構,而認識了 graph 這種模型後,我可以更清楚的了解這些線段也能區分成單向的或是雙向的並且帶有不同屬性,認識了 GQL(Graph Query Language)後,對於怎麼去有效率地走訪(traverse)一個人際網絡我也有了新的認識。其實這個過程就有點像是我「複用」了前人的思想結果。

有時候我們還得去對非常抽象的東西建模,比如說「用戶行為」。一個很常見對用戶行為建模的例子是當我們在前端使用 GA(Google Analytics,Google 提供用於追蹤網頁狀況和用戶行為的工具)或是開發類似系統時,很難以想像的是我們要怎麼把這些資料存起來,要有哪些 Columns 才能反映出這些行為,怎麼樣去設計能反映真實情況又能便於分析和展示?如果我們把每一次的頁面點擊行為記下一條 log,要怎麼在查詢時把一系列的操作關聯上?在這之上我們發展出了 session 的概念(當然你也可以說 session 是便於後端服務保留用戶資訊)來模擬用戶打開頁面到關掉頁面的一個用來裝一系列用戶行為的「容器」。還有其他例子像 Call Stack、DAG(Directed Acyclic Graph)、Pub-Sub(Publisher/Subscriber)都是我認為對抽象事物建模的展現。

在前面有提到,建模的過程中我們也會適當地去除雜訊,去除一些不需要特別關注的資訊,這個過程除了可以讓我們更專注在必要的資訊還有減少空間使用跟程式複雜度外,有時候還能有「奇效」。這邊講的奇效是我自己認為的,並不一定事情就是這樣子的。在我的觀察裡,當我們在建模的過程中只留下必要資訊後,我們在撰寫程式時會能夠比較容易地去「降維」或是「映射觀念」,你可能會發現很多我們想處理的事情在本質上是可以相通的,我們可以簡單的利用一些已知的觀念或解法來解決一個新出現的問題。如同我前面提到的 design patterns,除了我認為這是一種前輩們歸納出來的常見抽象層設計外,同時我也認為這是前輩們歸納出來我們常遇到問題的解法。像觀察者模式可能早期就是從訂閱報紙這樣子一個比較簡單的現實生活中的行為開始慢慢演變,到現在應用的領域也越發地廣,像是 YouTube 訂閱這種比較高層次的設計、或是非同步(asynchronous)通知推播這種用戶體驗、消息佇列(Message Queue)這種中介層(MiddleWare)的誕生、以及 Node.js 裡面 events 相關的實現。

在建模這件事情上,我一直覺得「購物車」是一個很有趣的例子,當思考到購物車這個功能時,很自然的會想到超市或是大賣場裡的那種購物車,我相信最開始的購物車功能也是這麼來的。慢慢地,網購世界裡的購物車開始發展出了自己的特色,並且有越來越標準化的趨勢。不管是在蝦皮、淘寶還是 Momo,購物車裡你都能夠很清楚的看到來自不同商家的商品被展示在一起,並且結帳時可以只選擇購物車內的部份商品,還會標記出正在舉辦不同優惠活動的商家,這些都是傳統購物車上看不到的功能。到了現在基本上不管訪問了哪個電商網站,都不需要去學習購物車如何使用,也不太會發現哪家的購物車有什麼特別不一樣的功能,我覺得這就有點像我們見證了一種經典的設計模式的誕生。

軟體的假設

前面有提到,很多在學界相對困難的問題,經過一些「假設」或「限制」之後在工業界能夠變成簡單很多的問題。因此在我們開發一個軟體或一個系統時,前提假設幾乎是無處不在的。就像在進行系統設計面試時一樣,我們可以很輕易的在面試中假設某資料的讀寫數量比是 100:1,從而讓我們的設計更適合這個題目的目標。在現實的開發過程中,更多時候前提假設不是這麼明顯的,當然我們還是會有很多來自自己或是同事們的假設,比如說預期的用戶數量或是 QPS(Queries Per Second,每秒查詢數量),但很多人容易忽視或是沒搞明白的就是我們所依賴的各種輪子(套件、中介層、雲服務)所引入的限制跟前提假設。比如說當我們引入了一個分散式的 Document Store 雲服務如 Amazon DocumentDBAzure CosmosDB,我們只要寫少少幾行程式碼就可以享受雲平台所提供的高擴展性(Scalability)及高可用性,但我們卻可能因為沒有仔細閱讀過服務本身提供的資料一致性保證(Data Consistency Guarantee)而導致有 Data Quality 問題時無從下手,或者是沒有研究過該服務的 Performance Best Practice 而遇上本不該遇到的效能瓶頸。

雖然很難做到,但很多時候我會認為搞清楚使用工具的前提假設是一件本來就應該要做的事,畢竟我們就是要用這些工具做事的人。而我認為真正困難的是,當一個假設改變之時,我們是否有能力做出相應的改變。這樣子的描述可能有點不好理解,我嘗試舉一個例子:早期的數位相機和手機,我們能看到在每年的發表會中越來越大的感光元件、光圈和鏡頭尺寸,而從某一個時刻開始,各大廠商的努力重心慢慢的不在這些硬體上了,我們有了透過傳統演算法或是機器學習演算法而實現的夜拍模式或是實時調色,我認為這些技術其實一直都是存在的(至少以前我們也會把數位相機拍的照片導入到 Photoshop 調一下),但是受限於早期晶片的運算能力,在以前這些技術是不太能作為一個選項放到手機裡的。一位厲害的工程師應該要能很清楚這些前提假設與設計決定之間的因果關係,並且時不時重新審視這些假設,當發現了一個我們依賴的假設發生變化時,能夠有能力思考那些曾經被封印的選項。我相信大部分的工程師,如果過去三年都在努力改進鏡頭的能力,那大概第四年也會這麼做的,即使這已經成為了一個相對吃力且沒效率的選項。

剛剛提到除了工具的前提假設之外,還有我們自己設置的假設,對於這類型的假設我覺得沒有什麼不好,畢竟我們總要先有個起頭,有了這些初始條件後確實能幫助我們更快地做出軟體的初始版本。不過在之前我自己開發的經驗來說,其實這些假設通常是很難準確的,就像肯德基大概沒想到蛋塔會這麼受歡迎之類的(誤),很多時候用戶的行為會跟想像的差別甚大,所以我覺得適當地引入 Feedback Loop 跟 Data-Driven 是很重要的,當我們的服務上線之後,用戶其實也在不斷地用他們的行為教導我們他們喜歡怎樣的體驗,常常我們會說要以用戶為核心,卻不斷地忽略這些明顯的訊號。

便於人類的設計

這個段落我還沒想得很清楚,就是一些我覺得有趣的觀察。

在寫程式時,偶爾能發現一些對於電腦相對沒意義卻明顯是便於人類的設計,比如說根據不同用途切分出不同的程式碼檔案,其實很多時候在編譯階段也是被合成回一份大檔案(像是 C 語言裡的巨集),但是透過不同的文件夾結構以及檔案命名,確實能很大程度地幫助我們找到想找的程式碼片段和組織我們的思考。

或者是所謂的可讀性(readability),也是指給人類去讀而非機器去讀,可讀性我認為可以分成程式碼的可讀性或是資料的可讀性。程式碼的可讀性通常會跟程式碼風格(Coding Style)扯上關係,變數和函數的命名或是空白鍵和 Tab 之爭其實對於最後編譯出來機器碼沒有什麼區別,但是確實會很大程度地影響工程師之間協作的生產力,Google 內部還有所謂的 Readability 認證 來確保線上運行的每一行程式碼都有一定的程式碼風格上的把關。資料的可讀性這件事我也是最近才比較有感覺,以前在開發簡單的網頁前後端的時候大部分交換資料的格式就是 JSON,到了最近有經驗跟 Avro、Parquet、Protobuf 這些資料格式打交道,才體會到 JSON 或是 XML 這種格式用比較大的存儲空間換來的資料可讀性和彈性的好處。像 Protobuf 這種對儲存空間大幅度優化過的格式直接打開是肉眼看不懂的 binary 形式的,需要另外消耗一些計算資源才能夠轉換成人類看得懂的資料,這其實對於 Debug 或是設定檔這種講求方便或是對存儲空間不敏感的場景來說滿不方便的。關於這些不同資料格式之間的比較,我個人很喜歡 Design Data-Intensive Application 這本「聖經」中的介紹,這裡也有 jyt 前輩的心得筆記

其實這些便於人類的設計,大多都是犧牲了一點不管是計算上還是存儲上的效率來換取人類的方便(其實另一個角度來說也是增進了開發時的效率),而這些犧牲也時常是可以被優化掉的,像是編譯時額外付出的計算資源就是一次性的,不需要每次程式運行時都付出這筆代價。

小結

呼,好不容易寫了一個段落。

有時間的話可能會再上來修修改改,也繼續想想之後想寫的主題。

雖然說這篇文章主要就是一個自我紀錄用途,但如果有人能看得開心也很好,也歡迎能夠一起留言交流意見,我也滿希望可以在這些主題上找到能一起討論的夥伴。

--

--

Tobby Kuo

Microsoft Software Engineer, focus on infrastructure developments for big data in distributed environments.