Tap

Tap 是一个微型库 (仅 11 行代码!),让您可以在实例初始化后对其进行配置,而不会牺牲代码的语义。它的工作方式类似于 Ruby 的 #tap

问题

Swift 的 struct 的人体工程学设计非常出色。想象一下这个结构体

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

您无需额外的工作即可获得一个初始化器 Person(name:age:),如果您将 let 变为 vars 并提供默认值,您甚至可以获得更多初始化器。想象一下这个 Person 结构体:

struct Person {
	var name: String = ""
	var age: Int = 0
}

然后,除了 Person(name:age:) 之外,您还将获得 Person()Person(name:)Person(age:),而无需编写任何额外的代码。当您想要一些临时的实例时,这种自动合成功能非常出色。

如果您想向结构体添加另一个带有默认值的字段,比如 phoneNumber

struct Person {
	var name: String = ""
	var age: Int = 0
	var phoneNumber: String = ""
}

那么程序的其余部分(还记得我们以前称应用为“程序”吗?)将继续工作而无需修改。好的代码是可塑的——易于适应新的需求。

可悲的是,当您想将一个 struct 从一个模块引入到另一个模块时,所有这些都会失效。(这种情况经常发生在我身上,因为我是 The Composable Architecture 的粉丝,并且我喜欢将我的应用程序拆分为独立的基于功能的包。)

在这种情况下,您需要在您想要使用的 struct 中声明一个 public 初始化器。

struct Person {
	init(name: String = "", age: Int = 0) {
		self.name = name
		self.age = age
	}

	var name: String
	var age: Int
}

代码现在增加了一倍的长度!更糟糕的是,假设我们添加一个电话号码

public struct Person {
	public init(name: String = "", age: Int = 0, phoneNumber: String = "") {
		self.name = name
		self.age = age
		self.phoneNumber = phoneNumber
	}

	public var name: String
	public var age: Int
	public var phoneNumber: String
}

为了不破坏 Person 的现有客户端,我们不得不修改三行不同的代码,而我们上面只做了一行更改。

如果您使用无参数的 init 并在之后配置实例,人体工程学设计会大大改善。

public struct Person {
	public init() {}

	public var name: String = ""
	public var age: Int = 0
}

var john = Person()
john.name = "John"
john.age = 41

现在,添加新字段将产生更可控的连锁反应。

但是,我认为现在的代码更弱了。我们将初始化与配置分离,因此它不如以前那样清晰地揭示意图。此外,它在语义上可能是错误的:现在 john 强制成为 var,而不管我们之后是否要改变它。

解决方案

Tap 通过为您提供一个协议 Tappable 来解决这个问题,它允许您这样做

public struct Person: Tappable {
	public init() {}

	public var name: String = ""
	public var age: Int = 0
}

let john = Person().tap { john in
	john.name = "John"
	john.age = 41
}

现在,您的结构体可以轻松地跨模块更改,并且您的代码就像您使用合成的初始化器一样清晰。

请注意,Tap 还以另一种方式改进了人体工程学设计:初始化器需要特定的参数顺序,而配置块则没有这种约束。

如果您的结构体符合 DefaultConstructible (由我们提供),您甚至可以使用 .tap 作为静态函数,从而更加简洁。我发现这在 The Composable Architecture 中派生结构体时特别有用

public struct PersonState: Equatable, Tappable, DefaultConstructible {
	public init() {}

	public var name: String = ""
	public var age: Int = 0
}

struct AppState: Equatable {
	public var name: String = ""
	public var age: Int = 0

	var personState: PersonState {
		.tap { state in
			state.name = name
			state.age = age
		}
	}
}