如何用Sourcery增强基于Swift枚举的状态

我真的很喜欢Swift中具有关联值的枚举,这是我设计状态的主要工具。 为什么枚举有如此强大而美丽的选择? 主要是因为它们使我可以对类型系统中的数据保持强大的不变性。

例如,如果我有一个屏幕来显示从服务器加载的一些数据,则可以用下一个模型来表示它:

  struct MyScreenModel { 
let isLoaded:布尔
let isEmpty:布尔
让数据:SomeInfo?
让错误:错误?
}

然后,我可以编写单元测试来检查该模型的正确性,并添加带有预期组合标志和可选参数的文档。

另一方面,我可以用一个枚举来表示它:

 列举MyScreenModel { 
空的情况
装箱
案例数据(SomeInfo)
大小写失败(错误)
}

现在,我的类型反映了我在数据上拥有的所有不变量。 这意味着编译器将能够对其进行检查,并且我可以删除负责验证这些不变量的单元测试。

优点清晰明了,缺点呢? 我们将为这种担保支付什么? 让我们比较一下使用这些类型中的每种类型有多么容易:

  func viewWillLayoutSubviews(){ 
self.loadingIndicator.isHidden = self.model.isLoading ==否
...
}

与我们以前的版本相比:

  func viewWillLayoutSubviews(){ 
self.loadingIndicator.isHidden = {
警卫队.loading = self.model else {返回true}
返回假
}()
...
}

显然,正在发生坏事。 我们失去了引用模型特定部分的能力,现在我们不得不仔细检查该模型中究竟存储了什么。

编辑使情况变得更糟。 让我们将SomeInfo结构声明为可变的,看看它如何影响我们的代码:

  struct SomeInfo { 
变量名称:字符串
}

首先,以天真,不安全的方式:

  self.model.info?.name =“新名称” 

并且以安全的枚举方式:

  self.model = { 
后卫var case .info(info)= self.model else {return self.model}
info.name =“新名称”
返回.info(info)
}()

显然,这是错误的。 我们能否实现基于结构的方法的可用性和基于枚举的方法的安全性?

什么是棱镜? 一般而言,棱镜是能够将某些光束分解为细节,使细节可见且清晰的对象。

就我们的问题领域而言,棱镜是什么?

如果我们的数据结构的枚举版本是实心光束,那么我们的结构就是“幽灵”。 因此,棱镜是将枚举转化为结构的一种方法。 有几种方法可以实现它,我将展示基于扩展的方法:

 扩展MyScreenModel { 
var isLoading:布尔{
警卫队.loading = self else {return false}
返回真
}

var isEmpty:布尔{
警卫队.empty =自我else {return false}
返回真
}
var数据:SomeInfo? {
守护让案例.data(someInfo)= self else {return nil}
返回someInfo
}
var错误:错误? {
守护让案例.error(error)= self else {return nil}
返回错误
}
}

现在,我们可以回到基于结构的变体的简短语法,并从基于枚举的选项中获得编译器保证。 双赢!

唯一的缺点:编写这些扩展很无聊。

Sourcery是一个独立的工具,可让您使用名为Stencil的模板语言连接有关代码的某些信息。 换句话说,这是由您自己的代码驱动的很酷的代码生成方法。 如果您不熟悉Stencil的语法,请不要担心。 我还指的是我的直觉和谷歌,而不是一些知识和理解。

那我想得到什么呢? 我想扩展所有枚举。 让我们做到:

  {类型中的枚举的%枚举,其中enum.cases.all.count> 0而不是enum.accessLevel =='private'%} ... 
{%endfor%}

这段代码非常好,我不需要编写任何注释。 基本上是不言而喻的。 我们想要每个枚举什么? 延期!

  {{enum.accessLevel}}扩展名{{enum.name}} { 
...
}

下一步是什么? 对于每种情况,我们都需要生成一个属性。

  {%,代表枚举的情况下,%s} 
...
{%endfor%}

我们想产生什么? 如果我们的案例包含一些关联的值,我们需要可选的访问器,否则为布尔值。

  {如果case.hasAssociatedValue%} 
{%呼叫associatedValueVar案例%}
{%else%}
{%呼叫simpleVar case%}
{% 万一 %}

我们发现了一些很酷的功能,例如调用语法!
让我们看一下简单的案例实现:

  {%macro simpleVar case%} 
公共变量是{{case.name | upperFirst}}:布尔{
得到{
警卫案。{{case.name}} =自我else {return false}
返回真
}
设置{
保护newValue else {
fatalError(“禁止设置假值”)
}
自我=。{{case.name}}
}
}
{%endmacro%}

对于getter,我们只检查self并返回true或false。 设置器允许我们像这样切换枚举:

  var model = MyScreenModel.emptymodel.isLoading = true 
// 是相同的
模型= .loading

将false设置为没有任何意义,因此我必须添加运行时检查来捕获它。
关联类型的案例呢?

  {%macro relatedValueVar case%} 
公共变量{{case.name}}:{%call caseType case%} {
得到{
警卫队让。{{case.name}}({{case.name}})=自我{
返回零
}
返回{{case.name}}
}
设置{
警卫让newValue = newValue else {
fatalError(“禁止设置零值”)
}
自我=。{{case.name}}({%call caseSet case%})
}
}
{%endmacro%}

我会说很简单。 起初它很棘手,但是经过3分钟的研究,它变得非常简单。 请注意,caseType和caseSet宏不在此处范围内,但是它们大多用于规范化对类型名称的访问。

您可以在公共生产代码库中轻松找到模板的全文。

这样我们就有了模板。 如何实际生成代码?

在Sourcery README文件中有很多方法可以执行此操作,但是我想向您展示我们为自己找到的方法。

步骤1.添加Xcode构建规则以支持*.stencil文件。

转到项目->构建规则->“ +”
并使其如下所示:

 设置-eif! 哪个来源> / dev / null; 然后 
回声“错误:缺少Sourcery。使brew安装sourcery。”
1号出口
科幻
模板= $ 1
输出= $ 2
sourcery --sources源--templates“ $ {templates}” --output“ $ {output}”

其中sources / stencil.sh是围绕Sourcery工具本身的简单包装。

作为此规则的结果,每个模具文件将被处理为一些快速文件,随后将对其进行编译。 生成的文件将存储到派生源dir中,并且永远不会出现在git或代码审查中。 它会在每个版本中进行更新,并且始终是相关的。

换句话说,它就是有效的。

第2步。将模具模板移至编译阶段

步骤3.构建您的项目。

有时Xcode中的某些内容会消失,并且此设置不会为您带来更新的版本。 如果出现此类问题,请像往常一样清理原始数据。

我希望我的简要指南对您中的某些人有所帮助。🙂如果您想了解更多信息并查看使用此技术的实际项目,则可以查看此处托管的生产代码。

谢谢阅读。