「boilerplate」这个词可以追溯到印刷媒体早期。本地小报需要内容来填满他们报纸的版面,但又通常没有足够的内容,所以很多本地小报通过大型印刷集团来获取稳定的内容,来填充报纸的最后几页。这些内容通常由预制的印板(plate)提供,这些印板长的很像曾经用来制作锅炉(boiler)的卷起来的钢板。因此有了「boiler-plate」这个词语。
经过转喻,这些内容本身被称作「boilerplate」,并且这个概念被拓展到包括标准化和公式化的文本。这些文本出现在合同和套用信函(form letter)里,以及与本周 NSHipster 文章最相关的东西——代码。
并不是所有的代码都那么光鲜亮丽,其实很多工作都是通过底层的 boilerplate 来工作的。
Swift 标准库也是这样,它有诸如符号整数(Int8
, Int16
, Int32
, Int64
)这样的类型家族,其中各个类型的实现除大小之外没有其他不同。
拷贝粘贴可以作为一次性的解决方案(假设你第一次就做的没有错误),但这种方法无法继续维护。每当你想修改这些派生的实现,都会有引入细微不一致的风险,久而久之导致这些实现变得不相同——有一点类似于地球上生命的多样性是因为基因随机突变导致的一样。
从 C++ 模板和 Lisp 宏到 eval
和 C 预处理器指令,不同的语言使用不同的技术来应对这个问题。
Swift 没有宏系统,并且因为标准库本身是使用 Swift 编写的,所以它也不能利用 C++ 元编程的能力。因此,Swift 的维护者们使用一个叫作 gyb.py 的 Python 脚本和一些模板标签来生成代码。
GYB 是「Generate Your Boilerplate」的缩写,参考了另一个 Python 工具 GYP 即「Generate Your Projects」。
GYB 是一个允许你使用 Python 代码来做变量替换和流程控制的轻量模板系统:
%{ <#code#> }
执行一段 Python 代码% <#code#>: ... % end
进行流程控制${ <#code#> }
会被替换为表达式的结果其他输入的文本则不会改变。
Codable.swift.gyb 是 GYB 一个很好的例子。在文件的顶部,基础 Codable
类型被赋值给一个实例变量:
%{
codable_types = ['Bool', 'String', 'Double', 'Float',
'Int', 'Int8', 'Int16', 'Int32', 'Int64',
'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64']
}%
之后,在 SingleValueEncodingContainer
的实现中,这些类型被用来循环生成协议要求的方法声明:
% for type in codable_types:
mutating func encode(_ value: ${type}) throws
% end
执行这个 GYB 模板会得到下面这些声明:
mutating func encode(_ value: Bool) throws
mutating func encode(_ value: String) throws
mutating func encode(_ value: Double) throws
mutating func encode(_ value: Float) throws
mutating func encode(_ value: Int) throws
mutating func encode(_ value: Int8) throws
mutating func encode(_ value: Int16) throws
mutating func encode(_ value: Int32) throws
mutating func encode(_ value: Int64) throws
mutating func encode(_ value: UInt) throws
mutating func encode(_ value: UInt8) throws
mutating func encode(_ value: UInt16) throws
mutating func encode(_ value: UInt32) throws
mutating func encode(_ value: UInt64) throws
这个模式的使用贯穿整个文件,用来给像 encode(_:forKey:)
,decode(_:forKey:)
和 decodeIfPresent(_:forKey:)
这样的方法生成相似、公式化的声明。GYB 减少了几千行 boilerplate 代码:
$ wc -l Codable.swift.gyb
2183 Codable.swift.gyb
$ wc -l Codable.swift
5790 Codable.swift
注意:有效的 GYB 模板不一定能生成有效的 Swift 代码。如果在派生的文件中出现编译错误,可能将很难查明根本原因。
GYB 并不是 Xcode 标准工具链的一部分,所以你并不能在 xcrun
里找到它。你可以下载源码并使用 chmod
命令使 gyb
成为可执行文件(macOS 默认安装的 Python 应该是可以运行 gyb
的):
$ wget https://github.com/apple/swift/raw/master/utils/gyb
$ wget https://github.com/apple/swift/raw/master/utils/gyb.py
$ chmod +x gyb
将这些命令放置到能被你的 Xcode 项目访问到,但与源代码文件不同的地方。比如说项目根目录下一个叫 Vendor
的目录下。
在 Xcode 中,点击 project navigator 中蓝色的项目文件图标,选择项目中正在使用的 target,并去到「Build Phases」面板。在面板顶部,你可以找到一个可以点击的 + 符号,用来增加新的编译阶段。选择「Add New Run Script Phase」,并将下面的代码输入编辑器:
find . -name '*.gyb' | \
while read file; do \
./path/to/gyb --line-directive '' -o "${file%.gyb}" "$file"; \
done
请确保 GYB 编译阶段放置在「Compile Sources」之前。
现在当你编译你的项目时,任何文件扩展名为 .swift.gyb
的文件会被 GYB 执行并产出 .swift
文件,之后与项目中其他的代码一起编译。
和其他工具一样,知道何时使用 GYB 与知道如何使用 GYB 同样重要。下面有一些你可能会想要使用 GYB 的例子。
你是否为集合或者序列中的元素复制粘贴相同的代码?for-in 循环和变量替换可以解决这个问题。
和之前在 Codable
的例子里看到的一样,你可以在 GYB 模板文件的顶部声明一个集合,然后迭代这个集合来生成类型、属性或者方法声明:
%{ abilities = ['strength', 'dexterity', 'constitution',
'intelligence', 'wisdom', 'charisma']
}
class Character {
var name: String
% for ability in abilities:
var ${type}: Int
% end
}
大量的重复代码也常常意味着或许能有更好的解决方法。像协议扩展和泛型这些语言内置的功能可以消灭大量的重复代码,注意是否可以使用这些功能而不是一昧的使用 GYB。
你是否在根据数据源来编写代码?那么尝试将 GYB 加入开发过程吧!
GYB 文件可以导入像 json
,xml
和 csv
这些 Python 包,你几乎可以解析任何你遇到的文件:
%{ import csv }
% with open('path/to/file.csv') as file:
% for row in csv.DictReader(file):
如果你想看看这方面的实践,看看 Currencies.swift.gyb,它给 ISO 4217 标准定义的每个货币生成对应的 Swift 枚举类型。
为了保持编译速度和确定性,应该将数据下载到文件并放入版本控制,而不是在 GYB 文件中执行 HTTP 请求或者数据库查询。
代码生成让代码和相关标准之间保持同步变得简单。只需要更新数据文件再重新运行 GYB 就可以了。
Swift 最近通过编译器合成代码减少了很多 boilerplate 代码, 像 4.0 中的 Encodable
和 Decodable
,4.1 中的 Equatable
和 Hashable
,4.2 中的 CaseIterable
。
与此同时,对于其他代码来说,GYB 是一个非常实用的代码生成工具。
Sourcery 是社区中另一个好用的工具,它让你可以使用 Swift(通过 Stencil)而不是 Python 来编写模板。
「Don’t Repeat Yourself」可能是编程中的美德,但是有些时候你需要重复一些事情来完成工作。当你需要重复的时候,你将会感谢有像 GYB 这样的工具来帮助你。
除非另有声明,本文采用知识共享「署名-非商业性使用 3.0 中国大陆」许可协议授权。