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

前段時間發(fā)現(xiàn)了一個十分強大的工具:Sourcery诱篷,它很好的解決了我在Swift開發(fā)中遇到的一些問題,在中文社區(qū)中sourcery似乎并不是很有名雳灵,所以這里特地寫一篇文章來作介紹棕所。本文大致分為三個部分:

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

什么是元編程

很多人可能對元編程(meta-programming)這個概念比較陌生,當然有一部分是因為翻譯的問題悯辙,這個“元”字看起來實在是云里霧里琳省。如果用一句話來解釋,所謂元編程就是用代碼來生成代碼躲撰。

這句話可以從兩個層面上來理解:

  • 在運行時通過反射之類的技術(shù)來動態(tài)修改程序自身的結(jié)構(gòu)岛啸。比如說我們都非常熟悉的Objective-C Runtime。
  • 通過DSL來生成特定的代碼茴肥,這通常發(fā)生在編譯期預(yù)處理階段。

OC有著十分強大的Runtime特性荡灾,在運行時可以查看和修改一個對象的所有成員瓤狐,所以有了Mantle之類JSON轉(zhuǎn)Model的庫;甚至可以在運行時添加批幌、刪除础锐、替換一個類型中的方法,當然也可以動態(tài)的添加類型荧缘,所以有了AspectsAOP皆警。這些應(yīng)用都可以歸納為元編程的范疇,因為它們的功能都是通過在運行時修改程序本身來實現(xiàn)的截粗,這一特性為我們節(jié)省了很多重復(fù)的樣板代碼信姓。

而Swift是一門靜態(tài)強類型語言,沒有OC這樣強大的運行時特性绸罗,雖然Swift也可以接入OC Runtime意推,但是那很容易讓你的代碼變成“用Swift寫的OC”,而且對運行時的修改容易讓程序變得難以理解珊蟀。既然這樣菊值,再來看看Swift自身的反射機制,Swift提供了一個名為Mirror的類型用來在運行時檢查對象的屬性,但是一方面Mirror只能查看不能修改腻窒,另一方面它的性能很差昵宇,文檔中也建議僅在Debug的時候使用。

所以說第一條路子在Swift中是走不通了儿子,只能從另一個方面來尋找答案瓦哎,所幸的是已經(jīng)有了一套成熟的解決方案,那就是下面要介紹的Sourcery典徊。

Sourcery

簡單來說Sourcery是一個Swift代碼的生成器杭煎,它能夠根據(jù)我們預(yù)先定義好的模板來自動生成Swift代碼。

基本使用

定義模板

以官方的Demo為例卒落,比如說你有一個自定義的類型:

struct Person {
    var name: String
    var age: Int
}

想要為這個類型實現(xiàn)Equatable協(xié)議羡铲,必須在==方法中依次比較每一個屬性的相等性:

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類型,如果要為它們都實現(xiàn)Equatable儡毕,會帶來大量重復(fù)的工作也切。而且如果你在一個類型中添加了新的屬性的話,必須同步修改它的Equatable實現(xiàn)腰湾,否則可能會出現(xiàn)難以預(yù)料的Bug雷恃。

Sourcery可以將我們從這些繁瑣的樣板代碼中解放出來,首先我們需要為所有的Equatable實現(xiàn)定義一個統(tǒng)一的模板费坊,這部分是通過一門名為Stencil的語言來編寫的倒槐。Stencil是一門專門為Swift設(shè)計的模板語言,語法十分簡單附井,對于上面代碼可以定義這樣的模板(模板的編寫推薦使用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 %}

代碼中出現(xiàn)的AutoEquatable是預(yù)先定義在我們自己代碼中的一個協(xié)議讨越,只是一個作為標記用的空協(xié)議:

protocol AutoEquatable { }

它的作用是讓我們能夠在模板中找到需要的類型,只需將自定義的Person類型聲明為實現(xiàn)AutoEquatable永毅,之后在模板中就可以通過types.implementing.AutoEquatable找到目標類型把跨,然后通過type.storedVariables來遍歷類型中的所有儲存屬性生成對應(yīng)的比較代碼。

代碼生成

定義了模板之后就可以通過這個模板來生成代碼了沼死,首先在系統(tǒng)中安裝Sourcery:brew install sourcery着逐。之后運行下面的指令:

sourcery \
   --sources ./YourProject \
   --templates ./YourTemplates \
   --output ./YourProject/AutoGenerated.swift

其中--source指定了工程的根目錄,--templates指定存放模板文件的目錄意蛀,--output將生成的代碼輸出到指定路徑耸别,除了命令行也可以通過一個.sourcery.yml文件來定制參數(shù),這里就不再展開介紹了县钥。

之后就能在工程的路徑下看到一個名為AutoGenerated.swift的代碼文件太雨,它包含了這樣的內(nèi)容:

// 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之前,否則新生成的代碼無法參與編譯锥咸。在這之后只要我們的類型實現(xiàn)了AutoEquatable狭瞎,無論是添加還是刪除屬性,每次Build代碼就會自動更新搏予,免去了手動修改的困擾熊锭。

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

原理

從上面的例子中可以看出來雪侥,Sourcery之所以如此強大碗殷,關(guān)鍵在于模板解析時能夠獲取我們代碼中的所有類型信息,這使我們在編寫模板的時候獲得了極大的自由度速缨。Sourcery使用了兩個關(guān)鍵的技術(shù)來實現(xiàn)這一切:Stencil和SourceKitten锌妻。

Stencil

在之前的介紹中也提到了,Stencil是一門用Swift實現(xiàn)的專門為Swift設(shè)計的模板語言旬牲,它的語法十分簡單仿粹,只解析下面這三種語法模式:

  • {{ ... }}:變量語法,將中間的部分作為變量(或變量的表達式)來解析原茅,解析后的值會作為結(jié)果插入到模板中的相應(yīng)位置上吭历。
  • {% ... %}:標簽語法(Tag),標簽用來表示一些具有特殊功能的語法擂橘,比如用來實現(xiàn)判斷的if和循環(huán)的for晌区。
  • {# ... #}:注釋語法,不會出現(xiàn)在解析后的結(jié)果中通贞。

除此之外還有一個名為Filter的概念朗若,它的語法是這樣的:{{ "stencil"|uppercase }}。符號|左邊是輸入的變量滑频,右邊就是一個Filter,這里輸出了字符串的大寫形式唤冈。Filter本質(zhì)上是一個輸入和輸出都是Any的方法峡迷,比如說上面的uppercase在源碼中對應(yīng)是這樣的:

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

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

同樣模板解析時可以訪問的變量也是在運行時注入到Stencil環(huán)境中的。Stencil有著十分強大的擴展性你虹,github上有一個這樣的庫StencilSwiftKit绘搞,為Stencil擴展了許多更加便捷的語法。

SourceKit

Xcode對Swift和OC的處理有一點不同的地方傅物,OC的編譯器是在Xcode進程中執(zhí)行的夯辖,而Swift的編譯器是在一個獨立的進程中進行的,所涉及到的一系列編譯工具的集合稱為SourceKit董饰,編譯的結(jié)果通過XPC與Xcode進行通信蒿褂。

這樣一來就有機會對編譯中間的結(jié)果做一些分析圆米,SourceKitten就是這樣一個開源庫,它與SourceKit進行交互并將代碼的語法結(jié)構(gòu)轉(zhuǎn)換成JSON的形式返回啄栓。利用SourceKitten娄帖,Sourcery可以獲取代碼中所有類型的相關(guān)信息,并將它們作為變量注入到了Stencil的上下文環(huán)境中昙楚,所以我們才能在模板中用{{ types }}這樣的方式遍歷代碼中的所有類型近速。

在Codable中的實踐

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

Codable是Swift4引入的對JSON解析的原生支持堪旧,與ObjectMapper之類的第三方庫相比削葱,它可以自動地解析Model中的屬性,如果你的數(shù)據(jù)模型和JSON結(jié)構(gòu)完全一致的話淳梦,使用起來將會非常簡單析砸。

然而現(xiàn)實往往并不是這么美好,很多時候需要對解析做一些自定義谭跨,這樣一來操作將會變得十分繁瑣干厚,要自定義KeyPath首先得為類型定義一個實現(xiàn)了CodingKey的枚舉,這個枚舉中要包含所有的屬性字段螃宙,即使這個屬性不需要自定義蛮瞄;而如果要做更加復(fù)雜的自定義的話還得自己實現(xiàn)init(from decoder: Decoder)encode(to encoder: Encoder)方法,并為所有的屬性實現(xiàn)decode和encode操作谆扎。

顯然這些代碼具有很高的重復(fù)性挂捅,非常適合使用Sourcery來自動生成:

AutoCodable

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

protocol AutoCodable: Codable { }

在模板中找到所有實現(xiàn)了AutoCodable的類型,并在擴展中為它們自動加上一個包含了所有屬性名的枚舉:

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

Sourcery提供了一個名為annotation的機制堂湖,可以在代碼中以注釋的形式向模板提供一些必要的數(shù)據(jù)闲先,只需要在某個變量或是類型的定義前加上一行這樣的注釋:

// sourcery: key = "value"
var something: Int

Sourcery會將這種格式的注釋解析出來,以key-value的方式添加到模板中該變量所對應(yīng)的annotations屬性上无蜂,通過這種方式可以在代碼中為模板解析提供一些自定義的數(shù)據(jù)伺糠。

使用

讓你的自定義類型實現(xiàn)AutoCodable

struct Person: AutoCodable {
    var myName: String
}

AutoCodable實現(xiàn)了以下功能:

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

    // sourcery: key = "my_name"
    var myName: String
    
  • 設(shè)置屬性默認值:
    AutoCodable允許你為屬性提供默認值,當JSON中的該字段解析失敗時該屬性會被設(shè)置為默認值斥季,而不是拋出錯誤训桶,有了默認值之后該屬性不再需要定義成可選類型:

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

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

    Codable在解析JSON的時候?qū)τ陬愋褪怯袊栏褚蟮暮ㄇ悖绻粋€屬性的類型是Bool舵揭,在JSON中對應(yīng)的字段值是Int類型的話會拋出一個類型錯誤(不像OC中的Mantle會自動轉(zhuǎn)換)。
    雖然Codable的這個做法無可厚非躁锡,然而在我們的實際項目中已經(jīng)有大量的后臺接口數(shù)據(jù)使用1和0來表示true和false了午绳。所以在這里AutoCodable針對Bool類型做了處理,支持將Int類型的值解析成Bool類型映之。

之后像上面所介紹的那樣將生成的代碼文件添加到工程里即可拦焚,可以看到Sourcery為我們免去了自定義解析時大量重復(fù)的代碼蜡坊,唯一的缺點就是向模板傳值只能通過注釋的形式,在Xcode添加一個Code Snippet:// sourcery: <#key#> = <#value#>能提供一些幫助耕漱,至于Key的名稱就只能在編碼的時候注意別寫錯了算色。除此之外Sourcery已經(jīng)完美的解決了我在使用Codable時碰到的問題。

總結(jié)

Sourcery本質(zhì)上相當于一個預(yù)處理器螟够,它為Swift帶來了靈活的元編程特性灾梦,你甚至可以將生成的代碼內(nèi)嵌到自己的代碼中,它的應(yīng)用場景遠遠不只是上面所介紹的這些妓笙。程序員的時間是寶貴的若河,我們應(yīng)該將精力集中在真正關(guān)鍵的部分,如果你也在使用Swift寞宫,不妨來嘗試一下萧福,和那些瑣碎重復(fù)的樣板代碼揮手作別??。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辈赋,一起剝皮案震驚了整個濱河市鲫忍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钥屈,老刑警劉巖悟民,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異篷就,居然都是意外死亡射亏,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門竭业,熙熙樓的掌柜王于貴愁眉苦臉地迎上來智润,“玉大人,你說我怎么就攤上這事未辆】弑粒” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵咐柜,是天一觀的道長兼蜈。 經(jīng)常有香客問我,道長炕桨,這世上最難降的妖魔是什么饭尝? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任肯腕,我火速辦了婚禮献宫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘实撒。我一直安慰自己姊途,他們只是感情好涉瘾,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著捷兰,像睡著了一般立叛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贡茅,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天秘蛇,我揣著相機與錄音,去河邊找鬼顶考。 笑死赁还,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的驹沿。 我是一名探鬼主播艘策,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼渊季!你這毒婦竟也來了朋蔫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤却汉,失蹤者是張志新(化名)和其女友劉穎驯妄,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體病涨,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡富玷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了既穆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赎懦。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖幻工,靈堂內(nèi)的尸體忽然破棺而出励两,到底是詐尸還是另有隱情,我是刑警寧澤囊颅,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布当悔,位于F島的核電站,受9級特大地震影響踢代,放射性物質(zhì)發(fā)生泄漏盲憎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一胳挎、第九天 我趴在偏房一處隱蔽的房頂上張望饼疙。 院中可真熱鬧,春花似錦慕爬、人聲如沸窑眯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽磅甩。三九已至炊林,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間卷要,已是汗流浹背渣聚。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留僧叉,地道東北人饵逐。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像彪标,于是被迫代替她去往敵國和親倍权。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容