很多開發團隊通常嚴重依賴于版本控制系統的分支功能。分布式版本控制系統讓分支操作更加方便。然而,在《持續交付》一書中描述的很多非常規言論中,就有一條是:“使用分支,你就無法做持續集成”。根據定義,如果你有代碼在某個分支上,那就沒有集成。有一種很常見的情況,會讓人很自然地想到利用版本控制工具的分支功能:那就是“對應用程序進行大規模改造時”。然而,還有一種替代這種真實分支的做法,技術上叫做“抽象分支(Branch by Abstraction)”。
抽象分支:在主干上進行以增量方式對軟件進行大規模改造的一種模式。
Paul Hammant在2007年就提到過用這種方式把OR mapping的方案從Hibernate切換到iBatis(詳見這里)。同樣,我曾經工作的一款商業產品(持續集成與敏捷發布管理平臺 Go)則從iBatis改到了Hibernate,這已經是兩年前做的事情了。我們也把產品的UI層慢慢地從“使用Velocity和JsTemplate”轉移到了“JRuby on Rails”上。
這兩種變化都是慢慢地增量式地完成的,在改變的同時,也做新功能的開發,但這并不妨礙我們每天向Mercurail的版本庫主干上提交數次代碼,甚至在切換過程中做了數次正式發布。我們是如何做到的呢?
從iBatis遷移到Hibernate
團隊決定從iBatis遷移到Hibernate,有兩個原因:第一,我們可以更高效地使用ORM,因為我們對產品數據庫的結構有絕對控制權,這樣就不用寫太多的定制SQL;第二,它有二級緩存,對性能有幫助。
當然,我們并沒想一次性把整個代碼庫都遷移到Hibernate上。我們的策略是:當開始增加新功能時,如果需要增加新的方法去訪問數據庫的話,就使用Hibernate來完成,當必要時,才將原有對iBatis的調用遷移過來。
對持久層邏輯的更新相對來說比較直接,因為產品 Go的代碼庫使用標準的分層結構,控制器層使用服務層,而服務層使用倉庫層(repositories)。因為所有需要訪問數據庫的代碼都利用repository pattern封裝在倉庫層中,所以每次將一個倉庫從iBatis改成Hibernate,增量式地完成修改是一件比較容易的事情。服務層根本不知道底下的持久層框架是什么。
我 的同事Pavan K.S.說:“抽象分支有一個嚴格要求,那是紀律性,即:開發人員不能再以任何借口添加原有模式的代碼。也就是說,作為第一原則,不要再增加iBatis查詢(盡管這么做可能更快更省事兒),必須用Hibernate來做。這是確保你進度的唯一方法。一種強制手段是在持續集成構建時只要發現新增了iBatis查詢, 就令持續集成構建失敗。并且,只能不斷減少這個閥值,絕不能增加。”
從Velocity和JsTemplate轉向JRuby on Rails
產品GO還從“以Java為基礎的UI軟件棧”轉向“JRuby on Rails”的軟件棧。這也有兩個原因:一是新的框架更容易寫測試,二是它會加速UI的開發。當然,這個變更也是增量式完成的。當在應用程序中創建新的頁面時,我們會使用JRuby on Rails,一旦做好以后,就讓應用程序的其它部分指向這個新頁面。
當需要對某個舊頁面進行大量變更時,我們就把它遷移到JRuby on Rails上。一旦做好,就把應用程序中所有指向這個頁面的URI都更改為這個新的頁面。此時,要把對應的舊頁面刪除。所以,當Go的界面大部分都是JRuby on Rails的實現時,仍舊有一些頁面是原有JAVA版的實現。
然而,只看頁面的話,根本不會覺察,因為它們的樣式是統一的,但從URI是能夠看出來的。所有使用/go/tab前綴的URI都會跳轉到舊的Velocity頁面上。其它頁面會跳轉到JRuby on Rails頁面上,當然它也同樣會使用原有界面所用的java 服務層。
抽象分支究竟如何操作呢?
抽象分支通過如下幾個步驟進行大規模增量式修改:
在你想改變的那部分代碼之上創建一個抽象層。
對其余部分的代碼進行重構,使其使用這個抽象層使用其之下的代碼提供的功能。
在新的實現代碼里實現一些新的類,讓其上的抽象層根據需要,選擇性的導向舊代碼或新增的類上。
剔除原有的舊實現。
清理,并重復前兩步,如果需要,可同時交付你的軟件。
一旦舊實現完全被代替后,如果你愿意,可以移除那個抽象層。
老馬(Martin Fowler)指出,這些步驟也可以變化一下。“在最簡單的情況下,你可以創建一個抽象層,然后重構,讓所有的代碼都調用它,然后再新寫一個實現,最后切換一下就行了。但是,還可以將它分開做。比如,不創建整個抽象層,而只是創建將要修改的功能的一個子集,遷移這部分代碼,然后再做下一部分(此時新舊代碼共存)。”
在上面iBatis/Hibernate的例子中,抽象層就是指那個倉庫層,它隱藏了持久層框架使用的細節。在JRuby on Rails的例子中,抽象層是Servlet Engine,通過URI的匹配,它可以決定是將Request分發到JRuby on Rails框架,還是標準的Java Servlets上。
盡管Go這個項目相對比較小,開發人員不到十個,而且到現在也僅有五年的時間,但是,這些原則完全可以應用于各種大小的項目上。即使在大型且分布式的團隊項目里,也可以成功地使用這種模式。
不可否認的是,抽象分支在開發過程上增加了開銷,而且當你的代碼庫結構性很差時,開銷會更多一些。為了能夠以這種方式做增量式變更,你必須仔細思考,一點兒一點兒地慢慢向前走。但是,在很多情況下,這種額外的工作量是值得的,越是大的重構,就越應該考慮使用這種抽象分支。
抽象分支的關鍵收益是你的代碼在整個結構調整的過程中都能夠正常工作,能夠做到持續交付。也就是說,你的發布計劃與架構上的調整完全解耦,因此,在任何時間點你都可以停止重構工作,做優先級更高的事情,比如發布一個你剛剛想到的非常好的功能特性。
對于抽象分支來說,需要定義一個終止策略,這一點非常重要。當你能夠做到“不完成全部的結構調整也可以發布”時,很容易產生一種傾向,即:一旦完成了重要部分的改造后,剩下的那部分尚未完成的工作,就放在那里不管了。然而,在系統中混合多種技術會讓系統更難維護,也要求團隊非常了解哪些地方還在使用舊有技術實現。這也許是一種可接收的權衡狀態,但至少要對整個團隊做到可見。
原文轉自:http://www.continuousdelivery.info/index.php/2013/01/04/branch_by_abstraction/