重寫軟件會面臨哪些風險?

本文最初發佈於Ben Northrop的個人博客,經原作者授權由InfoQ中文站翻譯並分享。

重寫帶有一種欺騙性的誘惑,其邏輯看上去是這樣:

這個系統已經應用於生產環境,我們很顯然知道它是如何工作的,因此只要將其移植到一個更好的平臺,一旦我們完成這項工作,事情就會變得更簡單。

我們在本文將批判這種基於直覺的認識。你會看到,重寫絕非易事。儘管我們不受新應用程序部分挑戰的影響,但我們也會碰到前所未有的全新挑戰。爲了成功地完成重寫,我們必須應對這些挑戰,因此提前瞭解有什麼挑戰很有必要。

在冒險重寫之前,讓我們講述一個快速的起源故事,看看我們是怎麼走到這一步的?

開始

這一切都始於綠地開發( Greenfield Development )階段。在這個階段,我們萌生一個想法,開始構建一款功能強大的應用程序。經過數週或幾個月的不懈努力,我們最終向市場發佈了一款產品。當然,這個“市場”既可能是真正的付費用戶,也可能是一組內部業務用戶等等。

如果應用程序很受歡迎,在一段時間內,它會處於增強( Enhancement )階段,添加新功能,解決缺陷。這是個平衡階段,每個人都是快樂的。最終, 技術債務會不斷累積,我們開始發現,我們努力工作的回報在遞減——在過去,一週的開發足以添加一個完整的新功能,但現在,可能都不夠改變按鈕的顏色。

這時,我們可能就會產生疑問:投入時間和金錢去做一件註定失敗的事,這樣做是否值得。此外,自應用程序首次發佈以來,可能出現了一些令人興奮的新技術,我們可能對如何利用這些技術讓應用程序更具彈性、更易使用和更高效等有一些宏偉願景。所以,我們開始制定一個重寫計劃。其思想是將現有系統的開發短時間凍結,然後將資源轉移到替代系統上。我們會先打基礎(使用更現代的模式、工具、語言等等),然後將現有的功能遷移過去。用戶只需要安然度過“暫停”(即不獲得任何新的更新),但是當重寫的系統就位時,工作效率應該是以前的兩倍(或者更多!) 。

雖然這個“直接遷移(lift and shift)”計劃看起來直截了當,但它掩蓋了一些關鍵風險——技術、組織和心理方面的因素都會使這個重寫階段極不穩定。此外,這個階段拖得越久,我們成功(即交付替換品)的機會就越低。因此,讓我們看看其中隱含的一些危險。

風險一:雙倍的工作量

在這段旅程中,我們常常面對的第一個風險是重寫基本上是雙倍的工作量。

當然,在技術上,我們希望自己可以暫停以往的開發,只專注於在新平臺上編寫代碼,但現實(準確地說是業務)很難遵守這一原則。

或許是爲了贏得(或留住)一個重要客戶,業務人員要求我們現在就在已有系統中添加一些新特性。或者第三方系統改變了其API,而我們需要重構,或者出現了sev-1缺陷,或者新的政府法規發佈。

關鍵是, 生產系統幾乎不可能長時間處於靜止狀態 ,甚至在重寫期間也是如此。對舊系統進行維護是不可避免的,而這意味着並行開發。

這樣,第一個問題是並行開發違反了我們作爲開發人員最神聖的原則: 不要重複自己。對遺留系統的任何更改都需要移植到新系統中。例如,如果我們在舊棧中添加特性X,我們必須在新棧中再次添加X(但這次是用新的語言或框架編寫的)。這種重複工作也適用於X的測試、項目管理、構建和部署。本質上,無論對遺留系統進行更改需要花費多少時間或資源,在重寫階段,成本很容易翻倍。

而且,根據重寫工作的進度,更改(如功能、缺陷修復等等)可能發生在源代碼中尚未重寫的部分,所以團隊必須記住,到那裏的時候要把重寫的時間線後推。或者在相反情況下,更改發生在應用程序中已經重寫的部分,因此,團隊必須返回並更新剛剛遷移過的代碼(然後重新測試等等)。

無論哪一種方式,重寫過程中的並行開發都可能讓人覺得,團隊在地面上構建替代品的同時還要保證飛機的正常飛行。爲了乘客的安全,那架飛機必須留在空中,但如果我們花太多的精力去維護它,我們就永遠無法讓新飛機起飛。當然,負責這兩駕“飛機”的團隊通常是同一個,這就引出了下一個問題。

風險二:團隊分裂

假設需要做一些必要的工作來支持現有系統,那麼問題是:誰對此負責?通常,團隊會採用兩種方法來管理並行工作。一種選擇是,讓更多的初級開發人員留在維護模式中,從而解放高級開發人員來開發新系統。這是有道理的,因爲完成重寫通常需要更高層次的技術“無畏派”——掌握各種新技術並整合應用、配置環境、建立模式和約定等等,這些都是棘手的任務,可能會讓新晉開發人員深陷泥潭。

因此,團隊可能會決定進行這種類型的分裂,初級開發人員維護,高級開發人員繼續前進。但這可能只是短時間有效, 不用多久,高級開發人員還是會被拉回來幫助解決需要全員參與的關鍵問題或討論一些變化的影響。多年來,他們對舊系統的瞭解,以及他們與業務利益相關者、運維人員和其他人的關係,讓他們不可替代。他們不得不參與其中。即使這種“抽離”每週只佔用幾個小時,但上下文切換仍然會阻礙工作效率。當開發人員在舊系統和新系統之間來回切換時,重寫的時間線就會被推後。

爲避免這種情況,團隊可以採取另一種方法:組建一個新團隊,這樣更容易專注於重寫。這個“重寫團隊”的開發人員與現有系統的聯繫可以忽略不計(也許他們是顧問、新僱員等等),因此他們能更好地將自己隔離開來,並在新工作中保持高效。

問題解決了?沒有。第一個問題是根據定義,這些開發人員不太熟悉代碼庫,不具備領域知識,不知道事物是如何運轉的,也不知道爲什麼要這樣運轉。當然,他們有原始代碼作爲文檔,但這就像通過查看源代碼來學習編程語言一樣。這都是事實,不是講故事。因此,這個新團隊要麼會在理解現有系統的錯綜複雜之處時步履維艱,要麼會爲了繼續前進而做出錯誤的假設,要麼最終不得不拉攏原來的團隊。這些都不是富有成效的結果。

此外,這種“組建新團隊”的方法也會疏遠那些留下來負責支持遺留系統的資深開發人員,使他們感到好像自己因爲忠誠度和經驗受到了懲罰。新團隊使用現代化的技術開始全新工作,而他們還在毫無生氣的做着維護。這一點也不酷。因此,不出所料,這樣很快就會導致開發人員不滿,稍後是人才流失。我們需要設計一個團隊結構,既能支持舊系統,又能正確高效地構建新系統,讓每個人都滿意。但即使你讓合適的人上了合適的車,還是會有意外發生。

風險三:意外之事

現在,即使重寫的團隊配備了經驗豐富的開發人員(即那些幫助編寫遺留系統或至少有一些經驗的開發人員),在將現有的源代碼遷移到新平臺時,總還是會有意外。對於此類意外事件,Donald Rumsfeld提出了比較中肯的見解:

有報道說 ,我們總是對沒有發生的事情感興趣,因爲據我們所知 ,有已知的已知 ; 有些事情我們知道我們已經知道。我們也知道有已知的未知;也就是說,我們知道有些事情是我們不知道的。但也有未知的未知——那些我們不知道自己不知道的。縱觀我們國家和其他自由國家的歷史,後一類往往是最困難的一類。

通常,當開始重寫過程時,我們會設法估計需要付出多少努力。從這裏開始,我們會編目前兩類工作。當然,有一些代碼庫我們有直接的經驗,因此,我們可以更快(更準確)地做出估計。這些是已知的已知。但是,系統中也有一些地方,我們知道自己沒有直接的經驗,所以我們增加了一些緩衝。

比如,Joe編寫了註冊流程,但是他去年辭職,所以遷移可能需要更長的時間。

這些就是已知的未知。我們按計劃繼續進行,評估這兩種類型的工作,併爲已知的未知留出額外時間,直到就整個時間線達成一致。

然而,直到我們深入研究並開始遷移代碼時,我們才偶然發現第三種也是最有害的工作類別,即未知的未知。它可能是我們從來不知道其存在的1000行令人費解的業務邏輯,或者是我們不知道何人使用的一組報告,又或者是一些我們不知道的將整個系統集成在一起的“膠帶”。不管是什麼,我們最初的重寫計劃都從來沒有考慮到它,但我們此時已經走出太遠,無法回頭。

結果是,我們要分析這個(以前)未知的未知,看看“它到底做了什麼?”,然後進行處理(“我們需要把它遷移過來,還是可以拋在一邊?”)。這些額外的分析、討論和努力只會推遲最初計劃好的時間,降低業務發起人和客戶的耐心。

雖然其中一些未知的未知可以被消化吸收,但太多的話可能會使整個重寫工作陷入危的境地。通過更好的計劃和分解(稍後詳細討論),可以儘量減少這種未知的未知,但是要完全避免還是很困難的。不管怎樣,我們還會給自己造成另一種危險。

風險四:第二系統效應

通常,我們長期忍受現有系統的缺陷,以至於當有機會重新來過時,我們忍不住想讓一切變得更好,或者更完美。

幾十年前, Fred Brooks將這種趨勢稱爲第二系統效應( Second System Effect)。在《 人月神話》中,Brooks談到了系統架構師,根據他的觀察:

在設計第一個項目時,他會不斷地修飾和潤色功能。這些功能會被存起來,用於“下一個”項目。早晚有一天,第一個項目結束了,架構師對這類系統充滿信心,他相信自己已精通這一級別的系統,並時刻準備着開發第二個系統。

第二個系統是架構師們所設計的最危險的系統。當他着手第三個或第四個系統時,先前的經驗會相互驗證,他們就可以判斷出此類系統的通用特性,而系統之間的差異會幫助他識別出經驗中不具普遍性的部分。

這種影響可以通過幾種方式表現出來。首先,我們傾向於把重寫看作是一個消除我們在過去的系統中所積累的全部技術債務的機會。我們希望分解God類,修復不一致的變量命名,改寫和重組數據結構等等。基本上,我們認爲這是解決遺留代碼中所有小問題的機會,因爲將債務移植到新代碼庫中感覺不太合適。

此外,我們還很容易產生這樣的想法,不僅將重寫看作是對架構中有缺陷或缺少支持的的某些部分進行現代化的機會,而且還將其看作是一躍跨到技術最前沿的工具 。也許UI仍然是陳舊的AngularJS,並不能爲移動Web用戶提供很好的渲染,所以移植到一個更現代的Web框架似乎是合理的。但是,當完成這項工作後,我們又將後端分解爲微服務,並使用Go編寫它們!從本質上說,重寫就像一項法案——它可能只是一項減少槍支暴力的簡單法案,但當它通過併成爲法律時,西弗吉尼亞州已經有了五座新橋樑,俄勒岡州的農民也得到了種植大豆的補貼。

第二系統效應不僅存在於開發者中間。通常,業務涉衆也會採取同樣的策略,但是有他們自己的優先級。“我們還不如把這個用戶渴望已久的功能在重寫時添加,因爲無論如何我們都會修改代碼。”就像技術上的改進和潤色一樣,範圍變得越來越大,重寫的發佈日期也被推得越來越晚。

最後,初始系統的開發口號是“快速上線運行!”,而重寫時變成了“我們以後再也沒有機會做X、Y和Z了!”

風險五:重蹈覆轍

人們常說,將軍在和平時期會爲最後一場戰爭做準備。他們會回顧和反思過去作戰計劃中的缺陷和錯誤,併發誓絕不讓這些錯誤再次發生。然,後下一場戰爭就來了,和上次完全不同。他們的準備是徒勞的。重寫也會發生同樣的情況。我們非常清楚自己在第一個系統上的失誤,所以當設計第二個系統時,我們首先要確保自己不會重蹈覆轍。但人們很容易忽略,事情已經發生了變化。在我們重寫的時候,那些過去的錯誤可能不是問題,所以沒有必要去預防它們。與此同時,在重寫中使用的現代化技術引入了一整套我們還無法預見到的新問題。

在從事諮詢工作的早期,我幫助一個客戶建立了一個大型網絡應用程序,服務於幾千名內部用戶。這個應用程序是成功的,換言之,我們在預算範圍內按時交付了功能,但是,我們不知不覺把“馬”套在了錯誤的馬車上。那是單頁應用出現的早期,所以我們使用了一個名爲Google Web Toolkit的剛起步的框架,在當時,它非常酷。遺憾的是,在這個應用發佈一年左右之後,谷歌從GWT轉向了更好的技術(Angular),讓所有人(包括這個客戶)都困在了這種基本上不受支持的技術上。這種糟糕的情況,我們是很難預測和預防的。

幾年後,我有機會與一些仍然在那裏的開發者重新取得聯繫,他們告訴我,他們別無選擇,只能重寫這個應用程序。但當我問他們選了哪些技術時,我感到很驚訝。他們不想再被單頁面應用技術或一個曇花一現的框架所傷害,於是他們選擇了一項非常非常成熟且有十年之久的服務器端頁面渲染技術,儘管此時,單頁框架已經非常可靠,每個人都在使用,而他們那有着豐富用戶交互的應用程序本可以從那個模型受益。換句話說,他們重寫的核心驅動力是避免第一個系統的錯誤,但在這個過程中,他們錯過了現代Web框架的許多優勢。他們在打最後一場戰爭。

風險六:全有或全無

重寫的最後一個大風險是可以避免的,但通常還是無法避免。雖然我們現在認可了迭代開發的智慧和最小可行產品的概念,但通過重寫,很難找到一種方法來一次性交付所有的東西。例如,如果客戶一直在使用一個擁有100個功能的遺留系統,如果我們希望他們(愉快地)轉換,我們交付的新系統怎麼能夠少於100個功能呢?基本上,重寫的系統需要能夠完成舊系統所做的一切,因此,在我們回到迭代開發之前,我們似乎必須首先部署整個替代系統。這不僅取決於現有系統的大小,可能需要付出相當的努力。

此外,這種“全有或全無”的需求與業務人員持續不斷的需求相結合,會給團隊帶來難以置信的壓力。如果團隊在重寫全部完成之前不能交付任何東西,那麼他們就不能顯示任何有形的價值,直到替代產品推出。在某種意義上,業務被重寫工作俘虜了,他們要保持耐心,相信他們的開發團隊可以完成這項工作。這並非易事。如果客戶特別要求,或者團隊經常遇到意想不到的延遲和問題(例如,未知的未知等),將重寫的發佈日期推後,業務可能就會覺得必須施加壓力(這會迫使團隊走捷徑),或者乾脆放棄。

總結

最後,希望你已經瞭解了重寫階段可能存在的風險。雖然綠地開發階段肯定也有它的風險,但重寫也不是在公園裏散步。即使最有能力的團隊,也很容易被並行開發、團隊組織、特性和技術鍍金以及“大爆炸式”部署的挑戰拖垮。因此,我們必須對大規模重寫有充分的理解。在 下一章我們將看到,令人驚訝的是,情況並非總是如此。

原文鏈接:

http://www.bennorthrop.com/rewrite-or-refactor-book/chapter-2-the-risks-of-rewrites.php

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章