iOS遺留系統重構實踐

lene4902 8年前發布 | 7K 次閱讀 數據庫 CoreData iOS開發 移動開發

背景

在一個有著良好分層結構的系統中,每一層都有它自己的職責:顯示層負責響應用戶事件,調用業務層的邏輯,最后做數據呈現;業務邏輯層負責業務規則與數據處理;數據訪問層封裝底層數據庫的操作,網絡訪問層與其并列,負責網絡請求、json解析等等。無論是MVC、MVVM、VIPER,歸根結底都是在”單一職責“、“關注點分離”、“高內聚低耦合”的原則下變化,只是表現形式和涵蓋的層次各異。

而在我們的代碼中,幾乎所有的顯示層對象,包括ViewController、ViewModel,甚至View里面都混雜了大量的CoreData API調用,直接進行數據庫操作。

粗略統計了一下,系統中一共有25個類與NSManageContext緊緊耦合。形成了下圖中混亂的局面:

(點擊放大圖像)

面對這種情況,我們首先要做的就是解耦。

方案選型

我們最先排除掉的是重寫這種簡單粗暴的方式。表面上看來,我們可以通過重寫得到一個干凈利落的方案,層次結構清晰,職責分離;但與之相伴的是巨大的風險:

  • 范圍不可控——遺留系統的難點就在于牽一發而動全身,影響范圍極廣。稍不留神,重寫的工作就會如野火燎原般蔓延開來,不可收拾。

  • 長時間無法上線——在整個過程中,直到最后完成的那一刻之前,系統會處于一直不可用的狀態。漫長的時間里,所有的新功能都被阻塞,不能交付。沒有哪個產品團隊能承擔這樣的結果。

第二個被排除掉的方案是特性分支。把重寫的工作放到分支上完成,其他人繼續在主干上開發新特性,直到重寫結束再合并回主干——這種做法確實比直接重寫要好上那么一點點,因為新特性還是可以不受影響的;但長期沒有跟主干合并的分支,在經歷上四五個月的重寫之后,天知道到最后要花多長時間來處理合并沖突?

既想減小對系統的影響,又想不影響新功能上線,又不想處理大量的合并沖突,最后的方案就只剩下了一種,那就是抽象分支(Branch by Abstraction)+特性開關(Feature Toggle)。

抽象分支

抽象分支這個名字的緣起是針對版本庫分支而言的,它允許開發者在一條“抽象”的分支上并行工作,無需創建一條實際的分支,從而避免無謂的合并開銷。

Martin Fowler 和 Jez Humble 都曾在多年前撰文介紹過這個重構方案

  • http://martinfowler.com/bliki/BranchByAbstraction.html

  • http://continuousdelivery.com/2011/05/make-large-scale-changes-incrementally-with-branch-by-abstraction/

它的工作原理很簡單:當我們想要替換掉系統中的某個組件——名為X——時,首先為X組件創造一個抽象層,這一層里面可能會有大大小小若干接口或是協議,把系統中對X組件的訪問都隔離在抽象層之下,系統只調用抽象的接口/協議,不會接觸到具體的API實現。如下圖所示。

這一步我們可以通過提取方法、提取類和接口等重構手法來完成;這以后系統就徹底跟X組件解耦了,它依賴的只是一組抽象接口,而非具體實現。這時候,我們就可以著手在這個抽象層下面,進行新組件的開發工作,讓它也實現同一套接口即可。

這之后,我們再使用特性開關(其原理及實現見下節),讓這個抽象層在生產環境下調用舊組件,測試環境下調用新組件,從而在完全不影響交付的情況下,完成對新組件的測試。測試結束后,就可以打開開關,讓系統在線上使用新組件,等徹底穩定后,把開關代碼和舊組件代碼全部刪掉,替換工作就完成了。

在上述整個開發過程中,任何一個階段都可以做到細粒度的任務分解,然后小步提交,每次提交都自動觸發單元測試和集成測試,保證不影響現有功能。在頻繁提交的情況下,也不會出現大量的代碼合并沖突,無論是做組件替換還是新特性開發,開發人員都可以基于同一套代碼庫工作。這就大大減少了對系統的沖擊和交付風險。

特性開關

先看一段代碼:

在這個例子中,我們要替換一個Storyboard的布局和相關ViewController的功能,耗時很久,如果直接在主干上修改,就會直接影響到現有的App,在功能完成之前都無法上線;如果拉一條分支出來做,未來就又會有大量的合并沖突。使用如上的特性開關就會避免上述問題。

當shouldDisplayNewSearchResultsScreen的值返回為真,就使用新的Storyboard,返回為假,就使用舊的Storyboard。這樣一來,只要開關處于關閉狀態,未完成的功能就是對用戶不可見的,我們就既可以在開發環境下自測,也可以部署到測試環境下做驗收測試,還可以針對開關為真的情況寫對應的單元測試,讓每次代碼提交都有持續集成驗證。這期間還可以繼續發布新版本,用戶完全感知不到影響,直到我們決定打開開關為止。

特性開關可以有多種實現方式。

  • 預編譯參數

在預編譯參數中傳值,讓不同的xcconfig文件傳入不同的值,然后在代碼中做判斷。

我們系統中絕大部分的特性開關都是用這種方式實現的。

  • NSUserDefaults

有些功能可能對App有破壞性的影響,即便是設成只對Internal Target可見,也會影響到QA的回歸測試。我們給Internal Target做了個Developer Settings界面,讓開發人員可以自己修改開關狀態,把開關的值存放在NSUserDefaults里面,默認返回false,只有在界面上手工切換之后才會返回true。測試和開發互相不受影響。

我們向Realm遷移的特性開關使用的就是這種方式。

  • 服務器取值

配置參數的值也可以通過服務器下發。這種做法的好處是比較靈活,在啟用/禁用某項功能的時候不需要發布新版本,只需要后臺配置,缺點是會增加集成和后臺開發的工作量。

  • A/B測試

還有一個辦法是使用第三方的A/B測試服務,如果缺少后臺開發人員的話,這也是一個選擇。但第三方的穩定性往往就會成為制約因素,Parse為推送通知提供過A/B測試服務,但是它到了17年就會被關閉了;我們用Amazon的A/B測試框架用了一段時間,然后Amazon也宣布今年8月份停用……目前我們還在尋找備選方案。

具體實現

在具體落實抽象分支和特性開關的時候,一共分成了如下幾個階段:

建立數據訪問層

我們首先把跟數據請求有關的操作從ViewController中提取成一個方法,放到另一個對象中實現,以便日后替換。然后把所有的數據訪問的方法都提取成一個協議,讓數據層之上的對象都依賴于這個協議,而不是具體對象。這樣一來,原先的ViewController就從下圖中的樣子:

變成了這樣:

為數據對象提取協議

除了數據訪問的代碼以外,我們還把所有的數據對象上的公有屬性和方法都提取了相應的協議,然后修改了整個App,讓它使用協議,而不是具體的數據對象。這也是為以后的切換做準備。

使用Realm實現

前兩步完成之后,我們就建立起了一個完整的抽象層。在這層之上,App里已經沒有了對CoreData和數據對象的依賴,我們可以在這層抽象之下,提供一套全新的實現,用來替換CoreData。

在實現過程中,我們還是遇到了不少需要磨合的細節,比如Realm中的一對多關聯是通過RLMArray實現的,并不是真正的NSArray,為了保證接口的兼容性,我們就只能把property定義為RLMArray,再提供一個NSArray的getter方法。種種問題不一而足。

切換開關狀態

上篇文章說到,我們在遷移過程中的特性開關是用NSUserDefaults實現的,在界面上手工切換開關狀態。這樣的好處是開發過程不會影響在Hockey和TestFlight上內部發布。直到實現完成后,我們再把開關改成

  • (BOOL)shouldUseRealm { return isInternalTarget; }</pre>

    讓測試人員可以在真機上測試。回歸測試結束之后,再讓開關直接返回true,就可以向App Store提交了。

    數據遷移

    這個無需多說,寫個MigrationManager之類的類,用來把數據從CoreData中讀出,寫到Realm里面去。這個類大概要保留上三四個版本,等絕大部分用戶都已經升級到新版本之后才會刪掉。

    后續清理

    特性開關是不能一直存活下去的,否則代碼中的分支判斷會越來越多。我們一般都會在上線一兩個星期之后,發現沒有出現特別嚴重的crash,就把跟開關有關的代碼全都刪掉。

    在第一步建立數據訪問層的時候,我們創建出了一個特別龐大的PersistenceService,它里面含有所有的數據訪問方法。這只是為了方便切換而已,切換完成后,我們還是要根據訪問數據的不同,建立一個個小的Repository,然后讓ViewModel對象訪問Repository讀寫數據,把PersistenceService刪掉。

    最后形成的架構如圖所示:

    總結

    首先,要勇敢面對遺留代碼庫,團隊里一定要有人站出來跟大家說,我們不能讓代碼繼續腐爛下去,我們要有清晰的目標和正確的策略,在重構中讓優秀的設計漸漸涌現。這才是正途。

    要有正確的方法

    在遺留代碼中工作,Long-Term Refactoring是不可或缺的。人們需要預見到在未來的產品規劃中,哪些組件應當被替換,哪部分架構需要作出調整,把它們放到迭代計劃里面來,當做日常工作的一部分。抽象分支和特性開關在Long-Term Refactoring可以發揮顯著的效果,它們是持續交付的保障。

    設計會過時,但設計原則不會

    很多技術決策都不是非黑即白的,它們更像是在種種約束下做出的權衡。時光會褪色,框架會過時,脫離了具體場景,今天的優秀設計也會淪落成明天的遺留代碼,但設計原則有著不動聲色的力量。我們無法預見未來,只能根據當前的情況做出簡單而靈活的設計。這樣的設計應當服從這些設計原則:單一職責、關注點分離、不要和陌生人說話……讓我們的代碼盡可能保持高內聚低耦合,保證良好的可測試性。

    標題

    Q:單元測試與集成測試,采用的是哪些工具呢?是Xcode自帶的嗎?

    A:單元測試針對oc用的是Kiwi,針對swift用的是Quick

    Q:李劍老師說的repo具體是什么,就是把persistentanceService拆分的是什么?

    A:repo具體來說就是針對不同的數據對象封裝的讀寫操作的類,比如代碼中有person, event等等,那就會有PersonRepo, EventRepo。先前為了FeatureToggle方便,我們是把所有數據操作集中在persistentanceService里面。但是這個類就太大了,在切換完成后我們要分拆。

    Q:數據訪問層是一個單例抽象的嗎?在這一層封裝了所有數據訪問的方法嗎?

    A:在遷移過程中數據訪問層是一個單例對象,遷移完畢后根據具體職責不同,再拆分成更小的對象

    Q:一個特別龐大的PersistenceService,以后是怎么拆分的。是根據具體的業務拆分么?

    A:主要是根據所要讀寫的數據對象不同而拆分。如果出現需要讀寫多個數據對象的情況,如果邏輯不會重用,我們一般就都讓ViewModel來處理,如果需要重用,就再提取一個類出來做。

    Q:realm坑多嗎?

    A:坑不少,跟CoreData相比,學習曲線很低,也很靈活。但是處理對象關聯關系的時候有點繞。然后它目前對fine grained notification的支持也不好,給我們的抽象層帶來了不少麻煩。

    Q:是出于什么原因考慮使用realm的?相比coredata和fmdb之類的有什么比較么?為什么不用sqlite,再數據存儲上realm的效率跟sqlite有什么優點么?

    A:CoreData學習曲線太高,而且我們都覺得它的設計已經陳腐了,如果直接用sqlite,最常見的庫也就是fmdb了,可是fmdb跟Java里面jdbc也沒啥差別,不想手工來做讀取ResultSet,一點點構造對象這種事情,還是希望有一個ORM

    Q:中間層的構建有什么好的經驗,如何保證中間層的健壯性 ?

    A:中間層的構建,我覺得比較重要的是要有明確的界限,職責清晰。在跟第三方庫集成的時候,要考慮到如果有一天要去掉這個庫或者替換它,會有多大的難度。

    我沒有太多的可以泛泛而談的東西,只能說架構這種東西都是權衡,在各種約束下的權衡。比如在本文的例子中,當CoreData被Realm所替換以后,抽象層還要不要保留?ViewModel應該直接調用Repository,還是RepositoryProtocol?有人會覺得這一層抽象就好比只有單一實現的接口一樣,沒有存在的價值,有人會覺得幾年后Realm也會過時被新的數據庫取代,如果保留這層抽象,就會讓那時候的遷移工作變得簡單。但無論怎么做,過上一兩年后,新加入團隊的人都可能會覺得之前那些人做的很傻。我們只能說盡量服從設計原則。

    Q:原來的一堆代碼,本來就沒有model之類的單一職責類,代碼本來就嚴重耦合,分享的直接就來替換下層實現,那中間層就代碼本來就不具有,怎么把新的mode引入又不影響新功能的開發?

    A:我們的代碼嚴重耦合,體現在視圖層直接訪問數據庫上,我們首先提取出一個persistenceService,把數據訪問的代碼做封裝,這樣視圖層跟數據庫就有了隔離。替換完成后,再把persistenceService拆成一個個小的repository,這樣就有了一個良好的數據訪問層。再接下來,按照抽象分支和特性開關的做法,架構可以一步步優化出來。

    Q:從coredata進行的數據庫層遷移,有考慮過magicalrecord嗎?為何選擇realm。理論上這樣成本更低,magicalrecord是基于coredata的,使用也很廣泛。

    A:我記得我們問過他是否推薦magicalrecord,他的回答是他建議使用Realm……另外就是我們對CoreData的這一套設計已經受夠了,migration成本也高。

    來自: http://www.infoq.com/cn/articles/ios-legacy-codebase-refactor

 本文由用戶 lene4902 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!