語意衝突
2011 年 8 月 4 日
聽過我和同事討論 功能分支 的人會知道,我們不太喜歡這種模式。我們反對的一個重要原因是,分支很容易,但合併很難。我們時不時會聽到一個論點,即現代 版本控制工具 讓合併變得足夠容易,因此功能分支是值得的。
現代工具在合併方面確實比我年輕時做得更好。此功能的一個很好的範例是合併重命名,它可以適當地合併我變更 lorem.rb
部分內容,而 Jez 將其名稱變更為 ipsum.rb
的情況。
這一切都很好,但它只解決文字衝突,而無法解決語意衝突。我所謂的語意衝突是指 Jez 和我做出變更,這些變更可以在文字層級安全合併,但會導致程式行為不同
最簡單的範例是重新命名函式。假設我認為如果將 clcBl
方法稱為 calculateBill
會更容易使用。使用現代重構工具,這很簡單:只要按 Shift+F6,輸入新名稱,該工具就會變更所有呼叫者。然而,如果 Jez 在他的功能分支中新增更多呼叫此方法,問題就會出現。當這兩個合併時,文字合併會順利進行,但程式不會以相同方式執行。

方法重新命名是一個簡單的範例,而且在靜態類型語言中也很容易找到,因為它會編譯失敗。但有許多更細微的語意衝突無法如此順利合併。假設我正在查看 calculateBill
方法,並意識到它除了計算帳單之外,還會將會計分錄傳送到會計系統。我不喜歡這個副作用,因此我將它拉到一個獨立的 notifyAccounting
方法中。然後,我可以找到所有 calculateBill
的呼叫者,並新增呼叫 notifyAccounting
。但 Jez 在他的分支中不知道這件事。
因此,這裡的第一點是,無論你的工具有多強大,它只能保護你免於文字衝突[1]。特別令人討厭的一點是,語義衝突更難發現且更難修復。
我們無法自動解決語義衝突。也許有一天工具將能夠處理其中一些衝突,但我懷疑一些棘手的衝突將永遠存在——至少在電腦能夠讀取我們的想法並自動推斷我們的意圖之前。然而,有一些策略可以顯著幫助我們應對這些衝突
其中第一個是SelfTestingCode。測試實際上是探測我們的程式碼,以查看它們對程式碼語義的看法是否與程式碼實際執行的內容一致。如果 Jez 預期他呼叫的程式碼會發生某些事情,並且對此進行了測試,那麼當他整合時,這些測試就會中斷。當然,這不是一個完美的回應。測試永遠不可能完美,但它們在實務上會發現許多語義衝突。一旦你發現衝突,它們也不會幫助修復衝突,但找到衝突是戰鬥中很大的一部分
另一種有幫助的技術是更頻繁地合併。如果 Jez 在幾個小時內而不是幾天內發現我的變更,他的困難會小得多。這樣,他就不再在舊語義上建立大量程式碼。這就是我們如此推崇持續整合的原因。
似乎有兩群人宣傳工具的概念,使功能分支變得可以忍受。一個是「企業級」VCS 的供應商。我們並不真正關心他們。另一群人是 DVCS(分散式版本控制系統)的愛好者。我對後一組人有點擔心。人們經常試圖根據 DVCS 如何簡化功能分支來證明 DVCS 的合理性。但那忽略了語義衝突的問題[2]。使用 DVCS 有很多很好的理由,因此沒有理由將一個好工具與一個有問題的技術結合起來。
備註
1: 而且如果我們更改完全相同的文字,除非你擁有類似 git rerere 的東西,否則合併工具通常也無法提供幫助。但這個問題遠小於語義衝突。
2: 如果你的功能在幾天內快速建置,那麼你會遇到較少的語義衝突(如果少於一天,則實際上與 CI 相同)。然而,我們不太常看到這麼短的功能分支。