天天看點

Swift 5.1:颠覆!将你的代碼減少一半

全文共7044字,預計學習時長14分鐘

Swift 5.1:颠覆!将你的代碼減少一半

圖檔來源:unsplash.com/@max_duz

Swift 5.1增加了許多新功能,其中一些功能有望徹底改變編寫和建構Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)将依賴注入代碼減少一半?

本文讨論了Swift Property Wrappers,并示範一種可大大簡化代碼的方法。

Swift 5.1:颠覆!将你的代碼減少一半

背景

現代軟體開發是一種有關項目管理複雜性的練習,架構是我們試圖實作這一練習的方法之一。 反過來,架構實際上隻是一個術語,用于描述如何将複雜的軟體分解為易于了解的層群組件。

Swift 5.1:颠覆!将你的代碼減少一半

是以,我們将軟體分解為可以輕松編寫的簡化元件,隻做一件事(單一職責原則,SRP),并且可以輕松測試。

然而,一旦擁有了一堆部件,就必須将所有部件重新連接配接在一起,才能形成一個工作應用程式。

以正确的方式将部件連接配接在一起,就能得到一個由松散耦合的元件組成的整潔架構。

但如果連接配接的方式出錯了,最終隻能得到一個緊密耦合的亂碼,其中的大多數部件都包含許多子元件建構和在内部運作的方法的資訊。

這使元件共享幾乎不可能實作,并且同樣無法輕松地将一個元件層換成另一個元件層。

這樣的情況令人左右為難,在嘗試簡化代碼時使用的工具和技術最終卻使我們的生活變得更加複雜。

幸運的是,可以使用另一種技術來管理這個額外的複雜層,該技術被稱為依賴注入,基于一個稱為控制反轉的原理。

Swift 5.1:颠覆!将你的代碼減少一半
Swift 5.1:颠覆!将你的代碼減少一半

依賴注入

本文無法對依賴注入作出完整且詳盡的解釋,簡單來說,即依賴注入允許給定元件向系統要求連接配接到完成其工作所需的所有部件。

這些依賴項将傳回到完全成型并準備使用的元件。

例如,ViewController(視圖控制器)可能需要ViewModel(視圖模型)。 ViewModel可能需要一個API元件來擷取一些資料,這些資料又需要通路身份驗證系統和目前的會話管理器。ViewModel還需要一個具有依賴關系的資料轉換服務。

ViewController不涉及這些東西,也不應該涉及,隻需與它完成工作所需的元件進行對話。

為了示範所涉及的技術,本文将使用一個稱為Resolver的強大的輕量型依賴注入系統。如果你使用其它任何DI架構也可。

如果想了解更多資訊,請參閱Resolver GitHub存儲庫上的Dependency Injection指南,以及Resolver本身的相關文檔。

傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Introduction.md?source=post_page---------------------------

Swift 5.1:颠覆!将你的代碼減少一半

簡單執行個體

使用依賴注入的非常基本的視圖模型如下所示:

class XYZViewModel {private var fetcher: XYZFetching    
private var service: XYZServiceinit(fetcher: XYZFetching, service: XYZService) {        
self.fetcher = fetcher        
self.service = service    }func load() -> Image {        
let data = fetcher.getData(token)        
return service.decompress(data)   }}      

以上列出的是view model需要的元件,以及一個初始化函數,其作用基本上是将傳遞給模型的任何元件配置設定給模型的執行個體變量。

這稱為構造函數注入,使用該函數可確定在無法執行個體化給定元件時不用給它所需的一切。

現在有了view model之後,如何獲得是view controller?

Resolver以在幾種模式下自動解決這個問題,這裡最簡單的方法是使用一種稱為Service Locator的模式......這基本上是一些知道如何定位且請求服務的代碼。

class XYZViewController: UIViewController {    
private let viewModel: XYZViewModel = Resolver.resolve()    
override func viewDidLoad() {       ...    }}      

是以viewModel要求Resolver“resolve(解析)”依賴關系。解析器使用提供的類型資訊來查找用于建立所請求類型的對象的執行個體工廠。

請注意,viewModel需要一個fetcher和一個提供給它的服務,但view controller完全不需要這些東西,隻需依賴注入系統處理所有這些淩亂的小細節。

此外還有其他一些好處。例如,可以運作“Mock”方案,其中資料層被替換為來自應用程式中嵌入的JSON檔案的mock資料,這樣的資料在開發,調試和測試時都便于運作。

依賴系統可以在背景輕松處理這類事情,所有的view controller都知道它仍然擁有所需的視圖模型。

Resolver文檔示例傳送門:https://github.com/hmlongco/Resolver/blob/master/Documentation/Names.md?source=post_page---------------------------

最後請注意,在依賴注入術語中,依賴關系通常稱為服務。

注冊

為了使典型的依賴注入系統工作,服務必須注冊,需要提供與系統可能要建立的每種類型相關聯的工廠方法。

在某些系統中,依賴項被命名,而在其他系統中,必須指定依賴項類型。但是,解析器通常可以推斷出所需的類型資訊。

是以,解析器中的典型注冊塊可能如下所示:

func setupMyRegistrations {    
register { XYZViewModel(fetcher: resolve(), service: resolve()) }    
register { XYZFetcher(session: resolve()) as XYZFetching }    
register { XYZService() }    
register { XYZSessionManager()}      

注意第一個注冊函數注冊XYZViewModel并提供一個工廠函數來建立新的執行個體。 注冊的類型由工廠的傳回類型自動推斷。

XYZViewModel初始化函數所需的每個參數也可以通過再次推斷類型簽名并依次解析來解決。

第二個函數注冊XYZFetching協定,通過建構具有自己的依賴關系的XYZFetcher執行個體來滿足該協定。

該過程以遞歸方式重複,直到所有部件都具有初始化所需的所有部件并執行他們需要的操作。

Swift 5.1:颠覆!将你的代碼減少一半

問題

Swift 5.1:颠覆!将你的代碼減少一半

化繁為簡 圖檔來源:unsplash.com/@emileseguin

然而,大多數現實生活中的程式都是複雜的,是以初始化函數可能會開始失控。

class MyViewModel {var userStateMachine: UserStateMachine    
var keyValueStore: KeyValueStore    
var bundle: BundleProviding    
var touchIdService: TouchIDManaging    
var status: SystemStatusProviding?init(userStateMachine: UserStateMachine,         
bundle: BundleProviding,         
touchID: TouchIDManaging,         
status: SystemStatusProviding?,        
 keyValueStore: KeyValueStore) {self.userStateMachine = userStateMachine        
self.bundle = bundle        
self.touchIdService = touchID        
self.status = status        
self.keyValueStore = keyValueStore    }...}      

初始化函數中有相當多的代碼,這是是必需的,但所有代碼都是樣闆檔案。如何避免這種情況?

Swift 5.1:颠覆!将你的代碼減少一半

Swift 5.1和Property Wrappers

幸運的是,Swift 5.1為我們提供了一個新工具稱為Property Wrappers(正式稱為“property delegates”),該工具作為提案SE-0258的一部分在Swift論壇上提出,并添加到Swift 5.1和Xcode 11中。

Property Wrapper的新功能使屬性值能夠使用自定義get / set實作自動包裝,是以得名。

請注意,可以使用屬性值上的自定義getter和setter來執行其中一些操作,但缺點是必須在每個屬性上編寫幾乎相同的代碼,即更多樣闆檔案。如果每個屬性都需要某種内部支援變量,那就更糟了。 (還有更多樣闆檔案。)

@Injected Property Wrapper

是以,在get / set對中自動包裝屬性聽起來并不令人興奮,但屬性包裝器将對我們的Swift代碼産生重大影響。

為了示範,我們将建立一個名為@Injected的Property Wrappers并将其添加到代碼庫中。

現在,回到“失控”示例,看看全新的物業包裝給我們帶來了什麼。

class MyViewModel {@Injected var userStateMachine: UserStateMachine    
@Injected var keyValueStore: KeyValueStore    
@Injected var bundle: BundleProviding    
@Injected var touchIdService: TouchIDManaging    
@Injected var status: SystemStatusProviding?...}      

就是這樣。 隻需将屬性标記為@Injected,每個屬性将根據需要自動解析(注入),由此初始化功能中的所有樣闆代碼都消失了!

此外,現在從@Injected注釋中可以清楚地看出依賴注入系統提供了哪些服務。

這種特殊類型的注釋方案在其他語言上應用時,最明顯的是在Android上的Kotlin中程式設計以及使用Dagger 2依賴注入架構。

履行

屬性包裝器實作很簡單。 我們使用Service類型定義一個通用結構,并将其标記為@propertyWrapper。

@propertyWrapperstruct Injected<Service> {    private var service: Service?    
public var container: Resolver?    
public var name: String?    
public var value: Service {        
mutating get {           
 if service == nil {              
 service = (container ?? Resolver.root).resolve(                    
Service.self,                    
 name: name                )            }            return service!        }       
 mutating set {            service = newValue        }    }}      

所有屬性包裝器都必須實作一個名為value的變量。

當從變量請求或指派時,Value提供屬性包裝器使用的getter和setter實作。

在這種情況下,服務被請求時,我們的值“getter”将檢查這是否是第一次被調用。 如果是這樣,當通路包裝器代碼時,請求Resolver根據泛型類型解析所需服務的執行個體,将結果存儲到私有變量中供以後使用,并傳回該服務。

當想要手動配置設定服務時,我們還提供了一個setter。 在某些情況下,這可以派上用場,最值得注意的是在進行單元測試時。

該實作還公開了一些額外的參數,如名稱和容器,更多的是在一秒鐘内實作。

更多執行個體

屬性包裝器的實作很簡單。使用服務類型定義一個通用結構,并将其标記為@propertyWrapper。

class XYZViewController: UIViewController {   
 @Injected private var viewModel: XYZViewModel    
override func viewDidLoad() {       ...    }}      

将ViewModel精簡到最基本的代碼為:

class XYZViewModel {   
 @Injected private var fetcher: XYZFetching    
@Injected private var service: XYZService    
func load() -> Image {        
let data = fetcher.getData(token)        
return service.decompress(data)   }}
      

至注冊碼也被簡化,因為構造函數參數被左右删除......

func setupMyRegistrations {    
register { XYZViewModel() }    
register { XYZFetcher() as XYZFetching }    
register { XYZService() }   
 register { XYZSessionManager()}
      

命名服務類型

解析器支援命名類型,它允許程式區分相同類型的服務或協定。

這也展示了一個有趣的property wrappers屬性,讓我們來看看這個。

常見的用例可能需要兩種不同視圖模型中的view controller,該選擇取決于它是否已經傳遞資料,是以應該以“添加”或“編輯”模式操作。

注冊可能如下所示,兩個模型都符合XYZViewModel協定或基類。

func setupMyRegistrations {    
register(name: "add") { NewXYZViewModel() as XYZViewModel }    
register(name: "edit") { EditXYZViewModel() as XYZViewModel }}
      

然後在view controller中:

class XYZViewController: UIViewController {@Injected private var viewModel: 
XYZViewModelvar myData: MyData?    
override func viewDidLoad() {        
$viewModel.name = myData == nil ? "add" : "edit"       
 viewModel.configure(myData)        ...    }}
      

請注意viewDidLoad中引用的$ viewModel.name。

在大多數情況下,我們希望Swift假裝包裝的值是屬性的實際值。但是,使用美元符号為屬性包裝器添加字首使我們可以引用屬性包裝器本身,進而獲得對可能公開的任何公共變量或函數的通路權限。

在這種情況下,設定name參數,第一次嘗試使用視圖模型時,該參數将傳遞給Resolver。解析器将在解析依賴關系時傳遞該名稱。

簡而言之,在屬性包裝器上使用$字首可以讓我們操縱和/或引用包裝器本身。你會在SwiftUI中看到很多這樣的東西。

Swift 5.1:颠覆!将你的代碼減少一半

為什麼是“注入”?

不少人會問:為什麼使用“注入”一詞?既然代碼使用Resolver,為什麼不将它标記為@Resolve?

理由很簡單。我們現在正在使用Resolver,主要是因為我們寫了它。但我們可能想在另一個應用程式中共享或使用我的一些模型或服務代碼,并且該應用程式可能使用不同的系統來管理依賴注入。比如,Swinject Storyboard。

“注入“成為一個更中性的術語,需要做的就是提供一個新版本的@Injected屬性包裝器,使用Swinject作為後端,一旦使用,就可固定化。

其他用例

Property Wrappers将來的更多用途展現在Swift上。

SwiftUI廣泛使用依賴注入,除此之外,Cocoa和UIKit中的标準類提供了一些額外的包裝器也不足為奇。

我們會想到圍繞使用者預設值和鑰匙串通路的常見包裝器。想象一下用下列代碼包裝任何屬性:

@Keychain(key: "username") var username: String?      

并從鑰匙串自動擷取支援你的資料。

過度使用

然而,就像任何酷炫的新錘子一樣,我們冒着過度使用它的風險,因為每個問題看起來都像釘子一樣。

有一次所有東西都變成了協定,然後開始了解何時能最好地使用協定(比如資料層代碼),然後再退出。 在此之前,C ++添加了自定義運算符,我們突然試圖找出user1 + user2的結果可能是什麼?

實作Property Wrappers時的關鍵問題是問自己:我是否會在所有代碼庫中廣泛使用這個包裝器? 如果是這樣,那麼Property Wrappers可能是個不錯的選擇。

或者至少減少其占用的空間。 如果建立一個如上所示的@Keychain包裝器,可以在與KeychainManager類相同的檔案中将它實作為fileprivate,進而避免在整個代碼中到處随意穿插。

畢竟,現在使用它簡單得就像:

@Injected var keychain: KeychainManager      

我們不想要每個模型看起來都像這樣的版本:

class MyModel {    
@Injected private var fetcher: XYZFetching   
 @Injected private var service: XYZService    
@Error private var error: String    
@Constrain private var myInt: Int   
 @Status private var x = 0   
 @Status private var y = 0  }      

然後讓下一個檢視代碼的開發人員争先恐後地弄清楚每個包裝器的作用。

Swift 5.1:颠覆!将你的代碼減少一半

完成塊

property wrappers隻是Swift5.1和Xcode 11中引入的許多功能之一,有望徹底改變編寫Swift應用程式的方式。

SwiftUI和Combine得到了媒體大幅的關注,但特别是在真正開始使用SwiftUI和Combine之前,property wrappers就将大大減少在日常程式設計中編寫的樣闆代碼量。

與SwiftUI和Combine不同,property wrappers可以在早期版本的iOS上使用! 不隻是iOS 13。

Swift 5.1:颠覆!将你的代碼減少一半

留言 點贊 關注

我們一起分享AI學習與發展的幹貨

歡迎關注全平台AI垂類自媒體 “讀芯術”

Swift 5.1:颠覆!将你的代碼減少一半

(添加小編微信:dxsxbb,加入讀者圈,一起讨論最新鮮的人工智能科技哦~)

繼續閱讀