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,不妨来尝试一下,和那些琐碎重复的样板代码挥手道别😄。

相关文章
相关标签/搜索