2011年2月5日 星期六

版本控制隨筆 (2) 關於開發支線

對於沒有版本控制系統使用經驗的人,可能還沒意識到二種需求的衝突面:開發成果必需被持續地追蹤且版本控制系統內的狀態同時保持最穩定的狀態。

因為,我們尚未提到開發支線的概念。個人認為版本控制系統最大的特色與差異並非集中式或分散式之別,是如何處理開發支線及使用開發支線會糟遇的各種衍生問題。若您試著使用版本控制系統,而沒有利用過任何開發支線的功能。您就是在預設的開發支線上工作,以 CVS 來說就是尚未設定開發支線標籤的情況。以 Subversion 來說,通常就是 trunk 目錄。trunk 目錄可以是想像的,或實際上建立的目錄。它的中譯即為主幹、樹幹的意思。隱喻著樹枝會伴隨著開發而萌發。以 GIT 來說,通常就是 master 開發支線。對於 GIT 或 Mercurial 來說,它們預設就建好了開發支線。開發支線的概念,對他們來說是明確且必需的功能。所以,他們在設計上,對於開發支線的操作都思考著如何以較低的成本(例如:儲存空間、建立的效率)來產生開發支線。

在您漸漸意識到開發支線的存在後,這時解釋開發需求的衝突就變得有意義。假設,在我們的開發活動中,僅使用一個開發支線(預設的開發支線)。在沒有建立程式碼提交規範的情況下,任何能操作版本控制系統的開發人員,都能隨時將手邊的程式或任意檔案提交給版本控制系統。團隊人數越多,修訂版本也就累積地越快。而這使得該開發支線上的程式碼,永遠處於不穩定的狀態。不穩定的狀態,最顯性的表徵就是當您由版本控制系統提取最新的專案內容時,因為存在著許多不完程式程式或組態設定造成編譯上的錯誤。這樣的錯誤傷害開發者的生產力。要替尚未完成的程式碼排除錯誤是徒勞無工的,何況對於同時使用版本控制系統的開發者,他們並不需要關注到同樣的軟體元件。

由於使用單一開發支線的情境下,毫無限制地提交新的開發結果很可能使專案最新的狀態毀損(Broken)。有種消極的對策:規範開發者在提交任何內容前,確定它是正確的、可進行編譯的。這是許多使用單一開發支線的團隊會使用的策略。不過總是有部分開發者無法確定自己的內容是否符合要求,這使得軟體品質時常因此而降低。在極端的例子,這類的版本控制系統使用者,會被取消提交權限,建議他提供修補檔案(Patch File),在經過驗證後由具有權限的使用者進行提交的動作。這種策略,勉強滿足使軟體/專案最新的狀態維持在較穩定狀態的需求。但在運用版本控制系統的開發流程中,並不能輔助開發者在開發活動尚未穩定之前的版本控制需求。在程式還未穩定前,只能依賴開發者逕自手工般地管理嗎?這樣消極的工作流程,讓需要加強訓練的生手與重新進行觀念調整的開發者缺少了成長的機會。即便,開發團隊已經採用此種保守的策略應付單一開發支線的使用情境,仍無法保證專案的穩定度。只是滿足在任何時間提取出最新的狀態時,編譯工作能順利地執行。為了達到較高的穩定度,更進一步要求,開發者在新功能(Feature)或是功能改善(Enhancement)被完整地實作與驗證後才能進行提交的工作。

這樣保守的工作流程規範,使得版本控制系統的採用,就像是學生被要求在合理的時間點,向 FTP 上傳已經完成的作業一樣。但是這種情況會在善用多個開發支線時轉變,您可以既保守,卻極積地使用版本控制系統的便利。將特定的開發支線定為穩定版本專屬的,僅在將已穩定狀態的變更合併(Merge)至此開發支線。而一般的開發活動,則是由此穩定的開發支線產生的分枝來進行開發。因此,維護產品品質的開發支線只有在合併經過多次檢驗的開發成果時才會用到,除了這個理由只剩下急迫性較高的錯誤修補會動到它。建立開發支線是為了滿足日常的開發活動,原因大致為:修正程式的錯誤、替程式進行完整的複察(Code Review)與重構(Refactoring)、嘗試不同的實作方案或是實作新的功能。當然,我們也能隨意地建立開發支線來測試任何的新想法,程式開發的過程中線出的活動,通常伴隨著有創意的點子產生。

對於使用開發支線的最大困擾是在開發活動告一段落,當開發者試著要將變更合併至另一個開發支線的時候,這時可能會產生程式碼衝突(Conflict)。衝突的產生,其實就是同一個地方,出現了些微的差異,導致版本控制系統無法透過演算法來決定該合併成什麼樣子。因此,標示檔案狀態為「衝突」是版本控制系統通知您,需要您的協助、請決定新的版本要是什麼樣子的途徑。只有全新的檔案、與舊有程式無關的全新的實作不會產生衝突。只要明白衝突的意義,就會知道,這是個決策的時候,而非新問題產生的時刻。不同的開發類型,有不同的方式降低衝突的機率。但不良的寫作習慣,卻會導致各種開發類型、分工都提高了衝突發生的機會。若您曾閱讀過重構的相關書籍,您可能已經聯想到有哪些壞味道(Bad smell)肯定會提高衝突發生的機率。個人認為最嚴重的,有立即重構必要的是:霰彈式修改(Divergent Change)與發散式變化(Shortgun Surgery)。一個會讓特定的程式常常產生變化,簡單地說,您設計的類別不符合單一責任原則。負責的越多,修改的理由就越多。另一種是,要增加一個新功能卻要改變許多的程式,即使他們的關係沒有那麼深厚。無論是同一個區間時常被改變,或著改變會動到的圍範太大,皆使得衝突發生的機會大增。經過這番的「提醒」相信您已經知道如何,輕易地製造產生許多衝突的開發支線,只要您掌握住通則,也能順利地避免。

版本控制隨筆 (1)

由於工作上的需要,我在從事開發工作後便開始接觸了版本控制系統。Subversion 是我第一個接觸的版本控制系統。那時,對於版本控制系統的體會大概就是:哇!這實在非常方便。我可把程式寫一個段落,然後將它上傳到一個伺服器裡。即使,我不小心殺掉了電腦裡的資料,只要重建好開發環境,再將專案由伺服器上抓回來就能繼續工作。這是我對版本控制系統最先感受到的好處。事實上,也是對版本控制系統的錯誤理解。要做到同樣的功能,用 email 或 ftp 上傳檔案,其實是一樣的事情。有太多技術能做到把檔案便利地保存至遠端,所以我向自己提出這樣的疑問:是哪些特質使得版本控制系統,不同於哪些將檔案方便上傳至伺服器呢?這個問題的答案,同時也是拒絕使用分散式版本控制工具的常見迷思之所在。

將檔案保存至中央的伺服器,只是版本控制系統實作的一種型式。通常我們稱這一類版本控制系統為集中式的版本控制系統,如 Subversion 或 CVS 都是屬於這一類。它們都需要依賴一個中央的伺服器來處理版本控制的行為。版本控制系統的存在,並不只是為了讓使用者方便地將檔案上傳至伺服器,上傳至伺服器只遷就於版本控制系統的實作模型的需要產生的副作用。這個行為,我們稱為提交(Commit)。提交是指您決定好將目前對於檔案(有時包含目錄)的變更,記錄在版本控制系統,每一筆記錄就是一個修訂版本(Revision)。對版本控制系統來說,它必需準備一個資料庫來存放這些記錄,這個特殊的資料庫被稱作檔案庫(Repository)。

修訂版本是個相當好的想法,因為它不同於一個時間點。當有人提出 qrtt1 你前天改的那個版本好像有問題?這時就得傷腦筋,前天的那時候的那一個改變產生了問題呢?或是其實不是前天,而是發生在昨天呢?這時版本控制系統能發揮的作用就是在哪一個修訂版出了問題。作為開發者能將修訂版本回溯到指定的修訂版本。這時版本控制系統發揮了作用,讓我們能將專案回復到特定的狀態,而針對確認問題的重演(Reproduce)行為才得以精確實履行。對於專案活動來說,適當地使用版本控制系統。您得先掌握專案議題與修訂版本的關係。新的功能(Feature)實作的可能會產生一連串的修訂版本。當功能達到一個里程碑時,我們也能用修訂版本記錄這個有意義的狀態。要檢驗該功能是否完備,就是要檢查該修訂版本(或該本版本之後)的專案狀態。因此,我們習慣將議題追蹤系統與版本控制系統做若干的結合。至少,我們能在議題完成的評論欄位附註修訂版號。而臭蟲回報(Bug Report)時,也是相類的作法。當 QA 回報什麼功能有問題時,在最新的狀態下若能重演問題,那就能附註在最新的修訂版本下能從演此問題。若該功能能對於至過去的議題,那就能找問題發生的修訂版本,這能輔助面對問題時,找出發生的根源與時間點。修訂版本,持有的資訊不僅是專案的變更記錄,還包含變更者與變更的時間。這讓問題的發現與觀念調整的對象皆同時獲得。

儘管在我描述中的修訂版本是如此便利,但某些版本控制系統的修訂版本並不適合這樣使用。以 CVS 來說,它的修訂版本是每一個檔案擁有自己的修訂版本。這在使用上會產生許多困擾,因為我們必需找出某個特定的時間點,各別檔案對應的修訂版本將它們各別回復成我們設想的狀態。若您想要擁有一個代表整體改變的修訂版本,在 CVS 下的替代方案就是使用標籤(Tag)。對 CVS 的使用者來說,標籤就是替一群檔案目前修訂版本取別名的動作。但這在使用上算是額外的動作,開發者有可能會遺忘下標籤的作動,喪失在適當的時機建立修訂版本的機會。而 Subversion 與其他較新的版本控制系統就有變更集合(Changeset) 的概念,它將同時被提交的檔案視為一個修訂版本(有些版本控制系統的設計者,甚至以較嚴謹的交易機制來實作,使得提交的具大 atomic 的性質)。因此,這樣的修訂版本就自然地產生出來,您不需要額外進行下標籤的動作。

回過頭來,先前提過版本控制系統允許您透過提交(Commit)產生修訂版本(Revision)。您可以這樣想:每一個修訂版本都是一份檔案庫記錄的快照(Snapshot),就像照相一般,將那個時刻的狀態保存下來。我們能利用版本控制系統提取(Checkout)的功能,取出最新的資料庫記錄,或著特定的修訂版本。在某些情況,版本控制系統亦提供利用標籤產生修訂版本別名的功能。別名就如同朋友之間的暱稱,指稱暱稱,如同指稱本人。因此,我們向版本控制系統提取記錄時,可以使用修訂版本代號,也可以使用標籤。除此之外,稍後將介紹的開發支線(Branch)也能作為提取的目標。

檔案庫(Repository)、修訂版本(Revision)、提取(Checkout)、提交(Commit)與標籤(Tag),這些概念與動作即為版本控制系統使上用的基本觀念,同時為版本控制系統提供的基礎建設。然而,這些基本的功能是無法滿足軟體開發的實際需要。對開發團隊來說,最舒適的狀態是我們知道我們永遠有能夠穩定的工作版本能使用。而對應到採用版本控制系統的情境來說,我們有個穩定的版本,它的標籤是 Release-0.1 (實際上對應的修訂版號可能是 Rev 1981)。無論什麼時候,我們都能提供客戶最穩定的版本。同時,我們的工作伙伴也能進行新功能的開發。儘管正在開發的功能,還沒告一段落,我們都希望版本控制系統能輔助所有的開發活動能被記錄起來。