全文共7044字,預計學習時長14分鐘git
圖片來源:unsplash.com/@max_duzgithub
Swift 5.1增長了許多新功能,其中一些功能有望完全改變編寫和構建Swift代碼的方式。那麼,如何使用Swift 5.1 Property Wrappers(屬性包裝器)將依賴注入代碼減小一半?算法
本文討論了Swift Property Wrappers,並演示一種可大大簡化代碼的方法。編程
背景後端
現代軟件開發是一種有關項目管理複雜性的練習,架構是咱們試圖實現這一練習的方法之一。 反過來,架構實際上只是一個術語,用於描述如何將複雜的軟件分解爲易於瞭解的層和組件。bash
所以,咱們將軟件分解爲能夠輕鬆編寫的簡化組件,只作一件事(單一職責原則,SRP),而且能夠輕鬆測試。微信
然而,一旦擁有了一堆部件,就必須將全部部件從新鏈接在一塊兒,才能造成一個工做應用程序。session
以正確的方式將部件鏈接在一塊兒,就能獲得一個由鬆散耦合的組件組成的整潔架構。架構
但若是鏈接的方式出錯了,最終只能獲得一個緊密耦合的亂碼,其中的大多數部件都包含許多子組件構建和在內部運做的方法的信息。app
這使組件共享幾乎不可能實現,而且一樣沒法輕鬆地將一個組件層換成另外一個組件層。
這樣的狀況使人左右爲難,在嘗試簡化代碼時使用的工具和技術最終卻使咱們的生活變得更加複雜。
幸運的是,可使用另外一種技術來管理這個額外的複雜層,該技術被稱爲依賴注入,基於一個稱爲控制反轉的原理。
依賴注入
本文沒法對依賴注入做出完整且詳盡的解釋,簡單來講,即依賴注入容許給定組件向系統要求鏈接到完成其工做所需的全部部件。
這些依賴項將返回到徹底成型並準備使用的組件。
例如,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---------------------------
簡單實例
使用依賴注入的很是基本的視圖模型以下所示:
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實例來知足該協議。
該過程以遞歸方式重複,直到全部部件都具備初始化所需的全部部件並執行他們須要的操做。
問題
化繁爲簡 圖片來源: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和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中看到不少這樣的東西。
爲何是「注入」?
很多人會問:爲何使用「注入」一詞?既然代碼使用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 }複製代碼
而後讓下一個查看代碼的開發人員爭先恐後地弄清楚每一個包裝器的做用。
完成塊
property wrappers只是Swift5.1和Xcode 11中引入的許多功能之一,有望完全改變編寫Swift應用程序的方式。
SwiftUI和Combine獲得了媒體大幅的關注,但特別是在真正開始使用SwiftUI和Combine以前,property wrappers就將大大減小在平常編程中編寫的樣板代碼量。
與SwiftUI和Combine不一樣,property wrappers能夠在早期版本的iOS上使用! 不僅是iOS 13。
留言 點贊 關注
咱們一塊兒分享AI學習與發展的乾貨
歡迎關注全平臺AI垂類自媒體 「讀芯術」
(添加小編微信:dxsxbb,加入讀者圈,一塊兒討論最新鮮的人工智能科技哦~)