Sourcery - Swift元編程實踐,告別樣板代碼

前段時間發現了一個十分強大的工具:Sourcery,它很好的解決了我在Swift開發中遇到的一些問題,在中文社區中sourcery彷佛並非頗有名,因此這裏特意寫一篇文章來做介紹。本文大體分爲三個部分:git

  • 元編程的概念和做用
  • Sourcery的原理和基本使用
  • Sourcery和Codable的實踐

什麼是元編程

不少人可能對元編程(meta-programming)這個概念比較陌生,固然有一部分是由於翻譯的問題,這個「元」字看起來實在是雲裏霧裏。若是用一句話來解釋,所謂元編程就是用代碼來生成代碼程序員

這句話能夠從兩個層面上來理解:github

  • 在運行時經過反射之類的技術來動態修改程序自身的結構。好比說咱們都很是熟悉的Objective-C Runtime。
  • 經過DSL來生成特定的代碼,這一般發生在編譯期預處理階段。

OC有着十分強大的Runtime特性,在運行時能夠查看和修改一個對象的全部成員,因此有了Mantle之類JSON轉Model的庫;甚至能夠在運行時添加、刪除、替換一個類型中的方法,固然也能夠動態的添加類型,因此有了AspectsAOP。這些應用均可以概括爲元編程的範疇,由於它們的功能都是經過在運行時修改程序自己來實現的,這一特性爲咱們節省了不少重複的樣板代碼。shell

而Swift是一門靜態強類型語言,沒有OC這樣強大的運行時特性,雖然Swift也能夠接入OC Runtime,可是那很容易讓你的代碼變成「用Swift寫的OC」,並且對運行時的修改容易讓程序變得難以理解。既然這樣,再來看看Swift自身的反射機制,Swift提供了一個名爲Mirror的類型用來在運行時檢查對象的屬性,可是一方面Mirror只能查看不能修改,另外一方面它的性能不好,文檔中也建議僅在Debug的時候使用。編程

因此說第一條路子在Swift中是走不通了,只能從另外一個方面來尋找答案,所幸的是已經有了一套成熟的解決方案,那就是下面要介紹的Sourcery。swift

Sourcery

簡單來講Sourcery是一個Swift代碼的生成器,它可以根據咱們預先定義好的模板來自動生成Swift代碼。app

基本使用

定義模板

以官方的Demo爲例,好比說你有一個自定義的類型:工具

struct Person {
	var name: String
    var age: Int
}
複製代碼

想要爲這個類型實現Equatable協議,必須在==方法中依次比較每個屬性的相等性:性能

extension Person {
	static func ==(lhs: Person, rhs: Person) -> Bool {
        guard lhs.name == rhs.name else { return false }
        guard lhs.age == rhs.age else { return false }
        return true
    }
}
複製代碼

一般咱們的項目中都會有大量的Model類型,若是要爲它們都實現Equatable,會帶來大量重複的工做。並且若是你在一個類型中添加了新的屬性的話,必須同步修改它的Equatable實現,不然可能會出現難以預料的Bug。ui

Sourcery能夠將咱們從這些繁瑣的樣板代碼中解放出來,首先咱們須要爲全部的Equatable實現定義一個統一的模板,這部分是經過一門名爲Stencil的語言來編寫的。Stencil是一門專門爲Swift設計的模板語言,語法十分簡單,對於上面代碼能夠定義這樣的模板(模板的編寫推薦使用vscode加上stencil插件):

{% for type in types.implementing.AutoEquatable %}
extension {{type.name}}: Equatable {
    static func ==(lhs: {{type.name}}, rhs: {{type.name}}) -> Bool {
        {% for variable in type.storedVariables %}
        guard lhs.{{variable.name}} == rhs.{{variable.name}} else { return false }
        {% endfor %}
        return true
    }
}
{% endfor %}
複製代碼

代碼中出現的AutoEquatable是預先定義在咱們本身代碼中的一個協議,只是一個做爲標記用的空協議:

protocol AutoEquatable { }
複製代碼

它的做用是讓咱們可以在模板中找到須要的類型,只需將自定義的Person類型聲明爲實現AutoEquatable,以後在模板中就能夠經過types.implementing.AutoEquatable找到目標類型,而後經過type.storedVariables來遍歷類型中的全部儲存屬性生成對應的比較代碼。

代碼生成

定義了模板以後就能夠經過這個模板來生成代碼了,首先在系統中安裝Sourcery:brew install sourcery。以後運行下面的指令:

sourcery \
   --sources ./YourProject \
   --templates ./YourTemplates \
   --output ./YourProject/AutoGenerated.swift
複製代碼

其中--source指定了工程的根目錄,--templates指定存放模板文件的目錄,--output將生成的代碼輸出到指定路徑,除了命令行也能夠經過一個.sourcery.yml文件來定製參數,這裏就再也不展開介紹了。

以後就能在工程的路徑下看到一個名爲AutoGenerated.swift的代碼文件,它包含了這樣的內容:

// Generated using Sourcery 0.12.0 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
extension Person {
	static func ==(lhs: Person, rhs: Person) -> Bool {
        guard lhs.name == rhs.name else { return false }
        guard lhs.age == rhs.age else { return false }
        return true
    }
}
複製代碼

生成的代碼文件是須要參與編譯的,記得將它添加到工程中。

接着,咱們能夠將代碼生成這一步整合到Xcode的編譯流程中,在Build Phases添加這樣一個腳本(這裏我把sourcery二進制文件也加到了工程目錄中):

Run Script

須要注意的是這個腳本必定要添加在Compile Sources以前,不然新生成的代碼沒法參與編譯。在這以後只要咱們的類型實現了AutoEquatable,不管是添加仍是刪除屬性,每次Build代碼就會自動更新,免去了手動修改的困擾。

以上的Equatable只是做爲示例,完整的版本請看官方提供的這個模板AutoEquatable.stencil

原理

從上面的例子中能夠看出來,Sourcery之因此如此強大,關鍵在於模板解析時可以獲取咱們代碼中的全部類型信息,這使咱們在編寫模板的時候得到了極大的自由度。Sourcery使用了兩個關鍵的技術來實現這一切:Stencil和SourceKitten。

Stencil

在以前的介紹中也提到了,Stencil是一門用Swift實現的專門爲Swift設計的模板語言,它的語法十分簡單,只解析下面這三種語法模式:

  • {{ ... }}:變量語法,將中間的部分做爲變量(或變量的表達式)來解析,解析後的值會做爲結果插入到模板中的相應位置上。
  • {% ... %}:標籤語法(Tag),標籤用來表示一些具備特殊功能的語法,好比用來實現判斷的if和循環的for
  • {# ... #}:註釋語法,不會出如今解析後的結果中。

除此以外還有一個名爲Filter的概念,它的語法是這樣的:{{ "stencil"|uppercase }}。符號|左邊是輸入的變量,右邊就是一個Filter,這裏輸出了字符串的大寫形式。Filter本質上是一個輸入和輸出都是Any的方法,好比說上面的uppercase在源碼中對應是這樣的:

registerFilter("uppercase", filter: uppercase) // 注入一個Filter

func uppercase(_ value: Any?) -> Any? {
    return stringify(value).uppercased()
}
複製代碼

一樣模板解析時能夠訪問的變量也是在運行時注入到Stencil環境中的。Stencil有着十分強大的擴展性,github上有一個這樣的庫StencilSwiftKit,爲Stencil擴展了許多更加便捷的語法。

SourceKit

Xcode對Swift和OC的處理有一點不一樣的地方,OC的編譯器是在Xcode進程中執行的,而Swift的編譯器是在一個獨立的進程中進行的,所涉及到的一系列編譯工具的集合稱爲SourceKit,編譯的結果經過XPC與Xcode進行通訊。

這樣一來就有機會對編譯中間的結果作一些分析,SourceKitten就是這樣一個開源庫,它與SourceKit進行交互並將代碼的語法結構轉換成JSON的形式返回。利用SourceKitten,Sourcery能夠獲取代碼中全部類型的相關信息,並將它們做爲變量注入到了Stencil的上下文環境中,因此咱們才能在模板中用{{ types }}這樣的方式遍歷代碼中的全部類型。

在Codable中的實踐

下面所介紹的Demo已上傳至個人Github:AutoCodableDemo

Codable是Swift4引入的對JSON解析的原生支持,與ObjectMapper之類的第三方庫相比,它能夠自動地解析Model中的屬性,若是你的數據模型和JSON結構徹底一致的話,使用起來將會很是簡單。

然而現實每每並非這麼美好,不少時候須要對解析作一些自定義,這樣一來操做將會變得十分繁瑣,要自定義KeyPath首先得爲類型定義一個實現了CodingKey的枚舉,這個枚舉中要包含全部的屬性字段,即便這個屬性不須要自定義;而若是要作更加複雜的自定義的話還得本身實現init(from decoder: Decoder)encode(to encoder: Encoder)方法,併爲全部的屬性實現decode和encode操做。

顯然這些代碼具備很高的重複性,很是適合使用Sourcery來自動生成:

AutoCodable

首先在項目中定義一個AutoCodable類型:

protocol AutoCodable: Codable { }
複製代碼

在模板中找到全部實現了AutoCodable的類型,並在擴展中爲它們自動加上一個包含了全部屬性名的枚舉:

enum CodingKeys: String, CodingKey {
    {% for var in type.storedVariables %}
        case {{var.name}} {% if var|annotated:"key" %}= "{{var.annotations.key}}"{% endif %}
    {% endfor %}
}
複製代碼

Sourcery提供了一個名爲annotation的機制,能夠在代碼中以註釋的形式向模板提供一些必要的數據,只須要在某個變量或是類型的定義前加上一行這樣的註釋:

// sourcery: key = "value"
var something: Int
複製代碼

Sourcery會將這種格式的註釋解析出來,以key-value的方式添加到模板中該變量所對應的annotations屬性上,經過這種方式能夠在代碼中爲模板解析提供一些自定義的數據。

使用

讓你的自定義類型實現AutoCodable

struct Person: AutoCodable {
    var myName: String
}
複製代碼

AutoCodable實現瞭如下功能:

  • 自定義字段名稱: 在須要自定義字段名稱的屬性前加上這樣一個annotation

    // sourcery: key = "my_name"
    var myName: String
    複製代碼
  • 設置屬性默認值: AutoCodable容許你爲屬性提供默認值,當JSON中的該字段解析失敗時該屬性會被設置爲默認值,而不是拋出錯誤,有了默認值以後該屬性再也不須要定義成可選類型:

    // sourcery: default = true
    var something: Bool
    複製代碼
  • 忽略某個字段: 被忽略的屬性不會參與JSON的Encode和Decode,另外被忽略的屬性必須帶有一個默認值:

    // sourcery: skip
    var something: Int = 0
    複製代碼
  • 支持將Int解析成Bool類型:

    Codable在解析JSON的時候對於類型是有嚴格要求的,若是一個屬性的類型是Bool,在JSON中對應的字段值是Int類型的話會拋出一個類型錯誤(不像OC中的Mantle會自動轉換)。 雖然Codable的這個作法無可厚非,然而在咱們的實際項目中已經有大量的後臺接口數據使用1和0來表示true和false了。因此在這裏AutoCodable針對Bool類型作了處理,支持將Int類型的值解析成Bool類型。

以後像上面所介紹的那樣將生成的代碼文件添加到工程裏便可,能夠看到Sourcery爲咱們免去了自定義解析時大量重複的代碼,惟一的缺點就是向模板傳值只能經過註釋的形式,在Xcode添加一個Code Snippet:// sourcery: <#key#> = <#value#>能提供一些幫助,至於Key的名稱就只能在編碼的時候注意別寫錯了。除此以外Sourcery已經完美的解決了我在使用Codable時碰到的問題。

總結

Sourcery本質上至關於一個預處理器,它爲Swift帶來了靈活的元編程特性,你甚至能夠將生成的代碼內嵌到本身的代碼中,它的應用場景遠遠不僅是上面所介紹的這些。程序員的時間是寶貴的,咱們應該將精力集中在真正關鍵的部分,若是你也在使用Swift,不妨來嘗試一下,和那些瑣碎重複的樣板代碼揮手道別😄。

相關文章
相關標籤/搜索