外商公司大型專案重構的技術經驗分享

Engine Bai
8 min readJan 8, 2024

--

今天要分享的是我加入外商公司之後立刻負責的大專案,這個專案主要目標是要統一我們程式專案裡面的兩套依賴注入框架,方便起見以下文章都會使用 DI (Dependency Injection) 來做簡述。

這篇會講五大面向以及關鍵收穫:

  1. 概括整個專案挑戰是什麼
  2. 定義問題跟動機
  3. 進行技術選型和評估流程
  4. 技術提案和審查
  5. 如何順利轉移
  6. 關鍵收穫

整體挑戰

DI 算是底層的基礎建設,如果要統一 DI 的話就會帶來很廣泛的改動和影響,加上程式專案到達一定規模、又是跨國的團隊合作,讓這整個專案面臨了四大挑戰:

  1. 專案的目標跟動機為何:我們為何要統一 DI?不統一的話又會有什麼問題?這些都是要先釐清的。
  2. 如何技術選型和評估:從了解專案的需求、考量技術上的取捨,到最後選擇出一套「合適」的工具。
  3. 如何做技術提案、說服團隊:從上述技術選型後,就會選擇出一套工具,接下來就要進行技術提案,要說服團隊你所選的工具是有解決問題的、而且不會帶來其他衍伸問題或副作用,在這過程能解釋不同的疑慮、說服其他人就相當有挑戰。
  4. 如何順利轉移:成功說服團隊後,接下來就是要進行轉移,該如何轉移才能順利並且降低轉移過程中的任何風險就十分重要。

今天我們不講太概略的技術概念,像是:「DI 是什麼、導入 DI 有什麼好處」以及「Koin / Dagger 的用法」,相信各位都是優秀的工程師,學會這些概念以及使用套件絕對不是什麼大問題的。

目標跟動機

我們的程式專案裡面有兩套 DI 框架: Dagger / Koin。我們的目標是想要把 DI 框架統一,然而不是所有人都覺得需要統一,所以我們必須要先把動機定義清楚來

在同一個專案裡面有兩套 DI 套件,會造成下列的問題發生:

  • 增加專案的複雜度、使得維護變得更加困難。
  • 讓維護者困惑,到底我該使用哪一套框架來做依賴注入?
  • 不同的工具也有不同的上手時間、學習曲線也不同。

這現象可以概括到「同一專案裡面有使用多套相似功能的套件」。

再者,Dagger 的缺點也是我們發起這專案的原因之一,Dagger 的缺點如下:

  • 複雜、學習曲線高,上手時間長:看看下者 Dagger Codelab 教學的範例,簡單的登入功能就要弄如此複雜的 DI 元件圖 Orz,用過的應該感同身受,沒用過的歡迎體驗一下。
Source: Dagger Codelab
  • 囉唆的語法:要注入一個物件要寫很多元件和冗長的程式碼。(以下範例為單純在 MainActivity 取得 User)
  • Dagger 會編譯時期就產生好 DI 相關的程式碼,所以會拉長編譯建置時間。

明確了解我們的動機和問題後,我們就可以開始進行技術選型了。

技術選型

我們探討了兩個主流的 DI 框架:

  • Hilt: 由 Google 官方所在維護的工具,而且也推薦 Dagger 使用者轉移過去,所以我們列為優先考慮。
  • Koin: 我們目前程式專案已經有在使用,加上在 Android 圈是非常知名的 DI 框架,所以我們也列入考慮。

我們首先必須確認這兩個框架符合我們的專案需求,這邊我們採用做 POC (Proof of Concept) 的方式才驗證,以下是我們的專案需求以及兩個 DI 對應的支援結果:

DFM = Dynamic Feature Module 以下都簡稱 DFM.

透過 POC 我們也了解到這兩個 DI 的優缺點以及限制:

Hilt 優缺點 & 限制
Koin 優缺點 & 限制

在 POC 轉移的過程當中,我們觀察到 Hilt 存在一個關鍵的限制,就是對不支援 DFM,然而這個是我們重要的專案需求,所以我們轉向考慮 Koin,但是 Koin 會有兩個缺點:「執行效能」和「執行時期閃退」,我們必須針對這兩個缺點想出一些因應措施來。

執行效能

針對效能損失,我們採用下列三個方法來評估影響程度:

  • 觀察使用到 Koin 的真實產品:我們參考了一些有使用到 Koin 真實 App,我們挑選使用者規模和 ShopBack 相近 (千萬下載,百萬 MAU) 的 App,看看有沒有明顯的效能損失。
  • 效能測試專案:我們使用開源的 DI 效能測試專案來測試,這個專案使用 DI 來取得生成 Fibonacci 數列物件 Fib 450 ,這個數列物件生成的時間複雜度是 O(2^n) 指數成長,足夠耗時可以用來測試效能。
Source: https://realpython.com/fibonacci-sequence-python/

以實際數據來看,結果確實是 Koin 的效能比 Dagger 差,Koin 在建立 dependency graph 的平均時間是在 2ms 左右。值得注意的是,這個測試是在 O(2^n) 的複雜度下執行的,而在實際專案中,我們的時間複雜度是 O(n) 線性時間,時間會遠小於 2ms。

  • 分析 Koin 程式碼:我們發現在註冊物件的時候,Koin 採用的是 graph 的深度優先搜索 (Depth-First Search, DFS) 去遍歷和解析物件的依賴。因此,所以我們推斷時間複雜度為線性時間,即 O(|V| + |E|)|V| 是物件的數量、|E| 是依賴的數量。

基於上述評估的結果,我們認為這個效能差距是微不足道、完全可以接受的。

執行時期閃退

Koin 在執行時期才會解析物件的依賴、生成該物件,如果在這過程當中出現錯誤(像是無法忘了註冊物件、無法生成其中一個依賴物件…等)就會造成閃退,為了解決這個問題,我們採取了兩個策略:

  1. Unit Test: 我們撰寫了一些 Unit Test 來嘗試取得所有要註冊的物件,測試 DI 設定是否有錯誤,這樣可以在 CI 時期就發現問題。
  2. Regression Test: 這仰賴 QA 團隊做完整的回歸測試。

這兩種方法可以幫助我們在開發期間就可以發現 DI 相關的問題,確保我們在執行時期不會發生閃退。

經過上述評估過程之後,Koin 在我們的實際專案裡面可以完全滿足我們的需求,而且我們有提出一些解決 Koin 缺點的方案,我們決定採用 Koin 作為我們專案的 DI 框架,接下來就要開始準備做技術提案。

技術提案和審查

選定要使用 Koin 之後,我們就要來準備提案給委員會來決定是否要進行這個專案。技術提案流程走的是 RFC (Request For Comment),提案者寫一份 RFC 來描述和討論技術提案,送交我們的技術委員會,委員會由各個國家的一位資深工程師代表組成,委員會和該國的工程師群討論,針對可能存在疑慮或需要進一步澄清的問題加以討論,最後如果沒有問題就會通過,就可以開始進行專案。

在這過程當中,可以看到不同面向的思考,其中當然有一些比較「激烈」的討論,像是針對 Koin 效能的疑慮…等,然而在這過程當中我看到了不同國家的同仁表現出成熟、工作環境包容和支持多元意見的一面,藉此擴展我們對這個看待問題的視野,其中包含了:

  1. 公司的每一個人,都把使用者和產品放在心中,這也是我們會有不同的觀點的原因,這也是為什麼我們有 RFC,這是為了讓大家可以針對提案提出不同的觀點,這樣可以讓我們可以針對這些觀點做出考量,並且減少風險。一個通過的 RFC 不代表是都贏,或者一個被拒絕的 RFC 不代表都輸,RFC 這是一個知識分享的過程,讓我們可以從中學習。
  2. 對問題總是嚴苛,而對人總是溫和。專注於找到最佳解決方案,而不是專注於對付不同觀點的人。相信每個人都在盡力解決問題,並且達成我們的共同目標。

如何順利轉移

為了順利轉移,又不會阻礙開發步調,我們必須採用逐步轉移,為了要採用這方法,我們必須要讓 Dagger 和 Koin 可以互通,轉移前後一樣還可以互相取得物件,也就是可透過 Dagger 拿到在 Koin 新註冊的物件,也可以透過 Koin 拿到既有在 Dagger 註冊的物件。這部分我們做了一個橋接的物件來達成:

接著我們把所有類別都在 Koin 註冊,然後透過這個橋接類別轉接回去 Dagger,當使用 Dagger 的地方都換成 Koin 語法之後,我們就可以漸漸把類別從 Dagger 拔掉,這樣的方式讓我們能實現逐步轉移的目標。

關鍵收穫

  • 取捨 Trade-off:在做技術評估時,我們應該採用「實用主義」而非「理想主義」,理當考量專案需求和技術上的優缺點,盡可能針對缺點設計出對應的措施,但不是在缺點上瘋狂打轉。
  • 大局思維 Big Picture Thinking: 我們應該放大格局、拉高視野來看整個團隊和專案的問題和狀況,大家都是在同一個團隊工作、維護著同一個專案。
  • 尊重團隊決議 Ship as a Team: 所有團隊成員應該成熟地尊重整個團隊做出的最終決定,即使個別成員最終還是不認同。

--

--

Engine Bai

白昌永 (大白) Software Engineer, Athlete, Learner. Focused on Mobile / Backend / AI + ML