“给它加上一个 ID”和“使用更多的函数式编程”是如今解决任何编程问题的常用方案。这个包不会帮助你实现后者,但它应该会让你在 Swift 环境中更愉快、更安全地使用前者。
这个包允许 Swift 开发者以极低的成本将每个 ID 类型转换为不同的 Swift 类型,同时不会妨碍代码可读性和可测试性,也不会给后续开发增加任何摩擦。 Javascript 开发者可能会到处扔字符串,但这也不能怪他们,毕竟他们的编程语言不能让他们轻松地做更多的事情。
这应该可以在任何可以使用 Swift 5.9 的地方正常工作。 目前它配置为可以在 Xcode 15 支持的任何平台上工作。 如果你需要支持任何 Apple 平台的早期版本,你可以使用该包的 1.0.0 版本,因为宏支持是强制使用较新工具集和最低 OS 版本部署的唯一依赖项。
最简单的采用方式是直接使用 #StronglyTypedID
宏。 它的两个基本参数是 ID 类型的名称和它的底层类型。
该宏只声明一个符合 StronglyTypedID
协议及其 rawValue
存储属性的值类型。
可选地,你可以使用宏的 adopts
可变参数声明符合其他协议。 如果需要,这允许设置 ID 类型的层次结构。
例如,如果我们想使用值模型类型来管理我们的 Clown 对象(正如现在的流行做法一样),并使用 UUID
值来标识它们,我们可以声明如下:
struct Clown: Identifiable {
#StronglyTypedID("ID", backing: UUID)
var id: ID
var name: String
var noseColor: CGColor
var shoeSize: Double
var musicalInstrument: MusicalInstrument
/* ... */
}
没有什么能阻止我们对引用类型进行类似的声明,但对于 protocol
类型,我们需要在外部声明它们,因为 Swift 协议不允许内部类型。 因此,如果我们最终得到了 Clown 对象的抽象外观,我们会得到以下结果:
#StronglyTypedID("ClownID", backing: UUID)
protocol Clown: Identifiable {
var id: ClownID { get }
var name: String { get set }
var noseColor: CGColor { get set }
/* ... */
func honk(times: Int)
/* ... */
}
当然,如果我们成为了功能蔓延的受害者,并开始构建一个全面的马戏团 HR 解决方案,我们可能最终希望确保我们的 Clown 对象,尽管他们做过的一切,都像马戏团的其他工作人员一样受到同等对待。 因此,你只需要将通用协议添加到 ID 类型,以便在通用功能中使用它们。
protocol Performer {
var salary: Decimal
[...]
}
protocol PerformerID: StronglyTypedID {}
struct Clown: Identifiable, Performer {
#StronglyTypedID("ID", backing: UUID, adopts: PerformerID)
[...]
}
struct Acrobat: Identifiable {
#StronglyTypedID("ID", backing: UUID, adopts: PerformerID)
[...]
}
protocol Payroll {
func pay(performer: PerformerID, period: TimeInterval) -> Decimal
[...]
}
StronglyTypedID
包括 Codable
的默认实现,它避免了对内容进行键值编码。 这在处理外部数据(例如典型的 JSON 后端回复)时非常方便,因为这些数据通常只有一个字符串字段来表示数据的 ID。
继续我们的小丑示例,我们正在进入小丑即服务 (clowns-as-a-service) 领域,我们的后端正在向我们发送以下可用小丑:
[
{
"id": "CD6AB6A4-3FE8-4B47-913B-B5F5D65DD6B2",
"name": "Pagliacci",
"noseColor": "0xff0000",
"shoeSize": 18
},
{
"id": "1DAFCD43-C8C8-40D9-B519-749B15B3A94F",
"name": "Bozo",
"noseColor": "0xffff00",
"shoeSize": 10
}
]
你需要做什么才能将这些纯字符串解码为我们的 UUID ID 呢?什么都不需要。 你可能需要确保后端人员始终在其中发送 v4 UUID,因为这是 Foundation UUID
类型支持的。 你可能还需要处理那些十六进制颜色,但这只是因为这是一个糟糕的例子。
再说一次,你需要做什么才能编码你的基于 UUID 的强类型 ID,使其成为后端可以处理的东西呢?什么都不需要。 它们会将自己编码为 UUID 的字符串形式,然后你就可以开始了。
在编写测试时,你不太可能关心你的唯一 ID 是否真的唯一。 你可以控制它们,并且可能希望确保它们易于在测试的执行和验证过程中进行跟踪。
这主要适用于 ID 原始值本身没有固有验证的类型(换句话说,具有指定格式的 UUID 和其他形式的全局唯一 ID 仍然需要小心处理)。
对于这些类型的 ID,允许使用字符串常量来构建 ID 可以使设置测试数据变得更加简单。 假设我们的 Clown
对象正在使用基于 String
的强类型 ID,并且我们正在为我们的 ClownCar
编写一些单元测试:
final class ClownCarTests: XCTestCase {
func testSeventeenClowns() throws {
let seventeen = 17
let clownCar = ClownCar()
for index in 1 ... seventeen {
clownCar.insert(Clown(
id: "Clown-id-\(index)",
name: "Clown #\(index)",
/* ... */
))
}
// Test that the clowns survive.
XCTAssertNoThrow(try clownCar.jumpOverPiranhaPool())
// Test that the clowns have been shaken but not stirred:
for index in 1 ... seventeen {
XCTAssertEqual(clownCar.clowns[index].id, "Clown-id-\(index)")
}
}
}
如果你尝试构建它,它将不会成功。 但是,如果添加以下行,它将可以正常构建,并且你将能够自动化对小丑的计算机残酷行为:
extension Clown.ID: ExpressibleByStringInterpolation {}
我们默认不将此包含在基于 String
的 ID 中的主要原因是,我们不知道哪些 ID 会支持它(仅仅因为强类型 ID 是基于 String
的,并不意味着它会支持任何随机的 String
作为原始值),并且通常你不希望在实际应用程序的代码中意外发生通过字符串常量进行的初始化——如果需要,你仍然可以使用 init(rawValue:)
来创建它们——。