利用 Swift 5.1 新功能實作 Fluent Interface 讓程式碼更易讀流暢!


最近,SwiftUI 正如火如荼地在全世界進行公開測試。如果你也有經意或不經意地接觸到 SwiftUI,那你可能會發現,它在設定 View 性質的語法上,跟我們以前學過的很不一樣。

一般在設定物件的時候,我們通常是這樣寫的:

但是在 SwiftUI 裡,我們卻得這樣寫:

差別很大對不對?先不要管句型的細節,光就排版的美感來說,後者不只比前者簡潔許多,它的縮排也很清楚地告訴讀者:後面三行都是第一行的子句,重點還是在第一行。反之,前者的每一行縮排都一樣,看不出重點,而且每一句還都得重複寫一次 imageView 這個變數名,相當累贅。很明顯的,後者的可讀性比較高。

SwiftUI 所採用的這種寫法並不是什麼新的發明。事實上,著名的程式設計架構專家 Martin Fowler 早在 2005 年的時候就已經給它一個名字了,叫做 Fluent Interface ,或者流暢界面。為什麼這樣稱呼呢?首先,它是一種操作物件的界面,跟屬性界面一樣,是一種寫作風格,而非一種全面性的架構。再來,這種風格的重點就在於它的易讀性與流暢性。因為它不需要用一個暫時的變數來操作物件,所以寫出來的程式碼雜音很少。

在 Swift 5.1 以前

流暢界面本身其實是針對物件導向程式語言的寫作風格,所以不管是 Objective-C,還是一開始的 Swift 都可以把物件的界面設計成流暢界面。不過,這些語言並沒有像對屬性界面的支援一樣,特別去支援流暢界面。所以,如果要支援流暢界面的話,我們必須要手動去改每一個屬性。

舉例來說,如果原本的型別界面長這個樣子:

那我們必須手動給每個屬性加上這樣的方法:

為什麼要回傳 self 呢?因為如此才能實現方法鏈 (Method Chaining),一個方法接一個方法地呼叫下去,而不用開新的陳述句。

這樣的做法對開發者來說負擔不小,因為每次屬性界面一改,我們就得跟著去改流暢界面的方法。或許有天 Xcode 會內建自動產生流暢界面的功能,又或者某位熱心人士會寫出相關的 Xcode 擴充套件,但目前,我們還是得手動去同步兩種不同的界面。

但如果你用的 Swift 版本是 5.1 版以上的話,你還有另一個選擇。

Dynamic Member Lookup

在 4.2 版的時候,Swift 新增了一個比較少被提到的功能:Dynamic Member Lookup,或者動態成員查詢。這個功能基本上就是讓我們能夠用屬性語法,去存取原本需要用字串存取的值。比如說,如果原本的型別定義是這樣:

實作動態成員查詢的方式是這樣的:

接著,我們就可以像直接存取 Person 實體的屬性一樣,存取它的 info 屬性內容:

這個功能主要是設計來支援跟 Python、Javascript、或 Ruby 等動態語言的互通性 (interoperatibility),算是比較少見的案例,所以也不多人談。但為什麼我會在這邊提到呢?

因為在 Swift 5.1 裡,這個功能升級了。

Swift 5.1 的 Key Path Member Lookup

在 Swift 5.1 中,除了字串之外,現在也可以用 key path 來當作動態成員查詢的媒介

假設我們把 Person 的定義改成這樣:

加上 Key path member lookup 的方式如下:

現在,我們可以這樣存取 person.info.name 了:

Wrapper Type

如果你有開 Xcode 照著實作的話,可能會發現一件事:打出「person.」之後,「name」也會出現在自動完成的清單裡面!這是因為編譯器現在可以從 Key path 去查詢所有的目標、以及它們的型別了。正是因為如此,Key path 成員查詢的應用範圍比字串成員查詢還要大很多。

舉例來說,它最主要的設計目標,就是拿來給所謂的包裝型別 (Wrapper Type) 用:

你可能會問:這樣有什麼意義呢?

關鍵在於 subscript(dynamicMember:) 這個下標方法。當我們用 key path 成員查詢來存取 content 的任何屬性時,每次都會經過 subscript(dynamicMember:),所以我們有機會在這裡做額外的處理。另外一點很重要的是,subscript(dynamicMember:) 的回傳值是沒有限制型別的。綜合這兩點來說,所謂的包裝型別,其實就等於是一個轉換器

比如說,我們可以定義一個專門回傳屬性型別的包裝型別,類似 type(of:) 的功能:

其它的運用包括把值語義(Value Semantics) 轉換成參照語義 (Reference Semantics) 等,不過那已經超出本文的主題了。

等一下!本文的主題不是流暢界面嗎?Key path 成員查詢與包裝型別又跟流暢界面有什麼關係呢?

Wrapper Type 與 Fluent Interface

其實講到這邊,只剩下一步就可以把兩件事串起來了。

讓我們回頭看看流暢界面的實作方式:

簡單來說,就是把屬性界面轉換成一個回傳 Self 的 Setter 方法。

而包裝型別的本質是什麼?轉換

也就是說,我們可以設計一個包裝型別,去把所有可寫入的屬性都轉換成回傳 Self 的 Setter 方法:

接著,只要把任何實體FluentInterface 包起來,它的所有可寫入屬性就都會變成流暢界面了:

怎麼辦到的?拆開來看就知道了:

拿來改寫一開始的例子的話:

我們只寫了一個型別,就省下了手動實作的許多麻煩,是不是心情都流暢起來了呢?

但說實在的,每次要用流暢界面的時候,都要把物件包起來再解開來,還是有點不舒爽。這兩個步驟雖然基本上是無法避免的,但我們可以想辦法讓它更流暢一點。

自訂運算子與你

自Swift 推出以來,就有自訂運算子 (Custom Operator) 這個頗有爭議的功能。它帶給開發者自行設計語法的自由度,但一旦被濫用的話,程式碼很容易變得一團亂,而且難以維護。簡單來說,它是一個需要小心使用的功能。

運算子可以拿來幹嘛呢?其實運算子不只包含加減乘除、或比較大小等數學操作,也可以拿來做任何其它事情。比如說,我們就可以用 ! 來強制解開 Optional

同樣的道理,我們也可以設計用來包裝與解開 FluentInterface 的運算子。我的選擇是 +-,但你也可以嘗試別的組合。

如此宣告之後,一開始的例子就可以改成這樣了:

是不是跟 SwiftUI 的風格幾乎一樣了呢?


iOS 開發者、寫作者、filmmaker。現正負責開發 Storyboards by narrativesaw 此一故事板文件 app 中。深深認同 Swift 對於程式碼易讀性的重視。個人網站:lihenghsu.com。電郵:[email protected]

blog comments powered by Disqus
訂閲電子報

訂閲電子報

AppCoda致力於發佈優質iOS程式教學,你不必每天上站,輸入你的電子郵件地址訂閱網站的最新教學文章。每當有新文章發佈,我們會使用電子郵件通知你。

已收你的指示。請你檢查你的電郵,我們已寄出一封認證信,點擊信中鏈結才算完成訂閱。

Shares
Share This