做者:Soroush Khanlou,原文連接,原文日期:2016-10-13
譯者:Cwift;校對:Crystal Sun;定稿:千葉知風redux
重構是一個持續性的過程。然而,在頻繁的重構過程當中還須要保證開發的功能可用。若是不能保證,代碼就不能被按期部署,這會使得你的代碼與團隊中其餘人的代碼保持同步變得更加困難。swift
即使如此,有一些重構難度額外大。在某些特定的狀況中,單例的特性會使其與不少不一樣的對象耦合,很難從工程中把單例刪掉。數組
許多單例,特別是那些命名不好勁的單例,會逐漸積累無關的行爲、數據以及任務,只是由於向單例中增長這些比向其餘對象中添加容易不少。session
若是想拆分一個影響深遠的單例,或者想測試使用單例的代碼,就會有不少工做要作。你想用更小、更好的對象慢慢地替換掉單例的引用,可是在徹底完成以前不能刪除單例自己,由於還有其餘的對象依賴它。ide
最糟糕的是,不能將單例的行爲和方法提取到另外一個對象中,由於它們依賴於單例內部的共享狀態。換句話說,若是單例沒有任何共享狀態,你能夠在每次調用時建立一個新的實例,問題就立馬解決了。單元測試
有一個單例,包含了許多不一樣的職責和一堆共享的狀態,應用中不少部分都在使用此單例。如何才能在不刪除單例代碼的狀況下解除應用對這個單例的依賴?測試
須要一種新的方法來引用單例:將單例的海量職責拆分紅不一樣的片斷,這些片斷將以單例中的(view)來表示,而不是對單例自己的結構進行實際變動。這個片斷的行爲和數據能夠用協議來表示。fetch
想象一下這種單例在一個虛擬的購物應用中:.net
class SessionController { static let sharedController: SessionController var currentUser: User var cart: Cart func addItemToCart(item: Item) { } var fetchedItems: [Item] var availableItems: [Item] func fetchAvailableItems() { } }
這個單例至少有三種職責。咱們想要拆解它,可是代碼庫中有數十個類引用了這個對象的屬性和方法。若是爲每一『片』職責定義一個協議,就能夠開始拆分它了。翻譯
protocol CurrentUserProvider { var currentUser: User { get } } protocol CurrentCart { var cart: Cart { get } func addItemToCart(item: Item) } protocol ItemFetcher { var fetchedItems: [Item] { get } var availableItems: [Item] { get } func fetchAvailableItems() }
SessionController
能夠遵照下面這些協議,無需任何額外的工做:
class SessionController: CurrentUserProvider, CurrentCart, ItemFetcher { //...
得益於 Swift 的協議擴展,咱們能夠將任何純粹依賴於協議的功能轉移到協議擴展中。舉個例子,availableItems
多是數組 fetchedItems
中 status
屬性爲 .available
的元素集合。咱們能夠把它從單例中移到具體的協議中:
extension ItemFetcher { var availableItems: [Item] { return fetchedItems.filter({ $0.status == .available }) } }
經過這種作法,咱們開始給單例瘦身,提取不相關的細節。
有了這些協議,就能夠在應用中使用了。第一步是找到全部用到單例的地方,把單例提取成一個實例變量:
class ItemListViewController { let sessionController = SessionController.sharedController //... }
接下來能夠把它的類型改爲具體的協議類型:
class ItemListViewController { let itemFetcher: ItemFetcher = SessionController.sharedController //... }
如今這個類從技術上來講仍舊能夠訪問單例,以受限制的方式訪問,很明顯這個類只須要使用單例的 ItemFetcher
切片。還沒完呢,下一步,使用 ItemFetcher
初始化這個類:
let itemFetcher: ItemFetcher init(itemFetcher: ItemFetcher) { self.itemFetcher = itemFetcher super.init(nibName: nil, bundle: nil) }
如今,這個類不知道初始化時使用的 ItemFetcher
是什麼類型的。能夠是這個單例,可是也能夠是別的類型!這被稱爲依賴注入。它容許咱們向視圖、控制器和其餘對象中注入備用的依賴,這將讓這些對象的測試變得更加容易。必須更新整個程序中該視圖控制器的初始化方式,使用新的構造器。
理想狀態下,試圖控制器的初始化只在協調器內部完成,因此應減小傳入的單例的數量。若是不使用協調器,那麼有可能導航控制器中第四個視圖控制的任何依賴都須要通過前三個視圖控制器的傳遞。這很不理想。
如今,該處理最困難的部分了:必須在程序中全部引用了單例的地方重複這個操做。這是一個乏味的過程,一旦你弄清楚了各類任務和協議以後,就剩下死記硬背了。(一個提示:若是有一個 CurrentUserProvider
,可能須要一個單獨的 MutableCurrentUserProvider
。只須要從當前用戶讀取數據的對象也不須要具備寫入的權限。)
一旦改變了對單例的全部引用,就拔掉了單例不少的牙齒了。能夠刪除單例的靜態單例訪問器屬性,而後視圖控制器和其餘對象將只能使用向它們中傳入遵照協議的對象了。
從這裏開始,你有幾個選擇。如今能夠輕鬆地把單例分解成全部輕量級任務。
你能夠把 ItemFetcher
定義爲類而不是協議,而後把全部代碼從 SessionController
中轉移到新的 ItemFetcher
類中,而後傳入類替代單例。
你能夠將 ItemFetcher
保留爲協議,而後建立一個名爲 ConcreteItemFetcher
的類,而後把單例中的代碼添加到該類中。這個方案給了你更多的選項,能夠注入遵照 ItemFetcher
協議的其餘對象,適合作單元測試、屏幕截圖測試、演示一個應用以及其餘用途。工做量會更大,可是同時也更加靈活。
經過在單例上建立職責的「切片」,你能夠將單例拆解成任務組件,同時不改變單例自己的結構。可使用依賴注入讓對象只能獲得但願它們使用的東西。最後,可使單例再也不具備單例的訪問器,而後在優良代碼的美好日落中自由騎行。
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg。