Swift的最强大功能之一就是在设计API方面给我们提供了极大的灵活性。这种灵活性不仅使我们能够定义易于理解和使用的函数和类型,还使我们能够创建给人以非常轻量级为第一印象的API,同时在需要的时候仍可以逐步暴露更多功能和复杂性。
本周,让我们看一下使这些轻量级API得以创建的一些核心语言功能,以及我们如何使用它们来通过组合的力量使功能或系统更加强大。
简书 - API
Swift:轻量级API的设计(二)
功能和易用性的平衡
通常,当我们设计各种类型和功能如何相互交互时,我们必须在功能和易用性之间找到某种形式的平衡。使事情变得过于简单,它们可能不够灵活,无法使我们的功能不断发展——但是,另一方面,过于复杂通常会导致沮丧,误解并最终导致错误。
举例来说,假设我们正在开发一个应用程序,该应用程序使我们的用户可以对图像应用各种滤镜——例如,能够从其相机胶卷或图库中编辑照片。每个滤镜由一组图像变换组成,并使用ImageFilter
结构定义,如下所示:
struct ImageFilter {
var name: String
var icon: Icon
var transforms: [ImageTransform]
}
关于ImageTransform
API,当前已将其建模为协议,然后由我们遵循并单独实现各种类型的转换操作:
typealias Image = UIImage
protocol ImageTransform {
func apply(to image: Image) throws -> Image
}
struct PortraitImageTransform: ImageTransform {
var zoomMultiplier: Double
func apply(to image: Image) throws -> Image {
return image
}
}
struct ContrastBoostImageTransform {
func apply(to image: Image) throws -> Image {
return image
}
}
struct GrayScaleImageTransform: ImageTransform {
var brightnessLevel: BrightnessLevel
func apply(to image: Image) throws -> Image {
return image
}
}
enum Icon {
case drama
}
enum BrightnessLevel {
case light
case dark
}
上述方法的一个核心优势是,由于每个变换都是作为自己的类型实现的,因此我们可以自由地让每个类型定义自己的属性和参数集——例如,如何使GrayScaleImageTransform
接受BrightnessLevel
来使图片变成灰度。
然后,我们可以根据需要组合任意数量的上述类型,以形成每个滤镜——例如,通过一系列转换使图像具有某种“戏剧性”外观的滤镜:
代码语言:javascript复制let dramaticFilter = ImageFilter(
name: "Dramatic",
icon: .drama,
transforms: [
PortraitImageTransform(zoomMultiplier: 2.1),
ContrastBoostImageTransform(),
GrayScaleImageTransform(brightnessLevel: .dark)
]
)
到目前为止,一切都很好。但是,如果我们仔细研究上述API,可以肯定地说,我们的选择是为了提高功能和灵活性,而不是为了易于使用。由于每个转换都是作为单独的类型实现的,因此,由于没有一个可以立即发现所有转换的地方,因此使用者无法立即清楚我们的代码库包含哪种转换。
与之相比,如果我们选择使用枚举代替协议,则将为我们提供所有可能选项的清晰概述:
代码语言:javascript复制enum ImageTransform {
case portrait(zoomMultiplier: Double)
case grayScale(BrightnessLevel)
case contrastBoost
}
使用枚举还可以产生非常漂亮且可读性强的调用,这使我们的API更加轻巧易用,因为我们可以使用点语法**dot-syntax
**来转换所有的调用,如下所示:
let dramaticFilter = ImageFilter(
name: "Dramatic",
icon: .drama,
transforms: [
.portrait(zoomMultiplier: 2.1),
.contrastBoost,
.grayScale(.dark)
]
)
但是,尽管Swift枚举在许多情况下都是一种出色的工具,但在此处它真的不是一个好的选择。
由于每个转换都需要执行截然不同的图像操作,因此在这种情况下使用枚举将迫使我们编写一个庞大的switch
语句来处理这些操作中的每一项——这很可能会成为噩梦。
Light as an enum, capable as a struct (这句怎么翻译,轻如枚举(enum
),强如结构(struct
)?)
值得庆幸的是,还有第三种选择——可以让我们两全其美。与其使用协议(protocol
)或枚举(enum
),不如使用结构(struct
),而该struct
又包含一个封装了给定转换各种操作的闭包:
struct ImageTransform {
let closure: (Image) throws -> Image
func apply(to image: Image) throws -> Image {
try closure(image)
}
}
请注意,不再需要
apply(to:)
方法,但我们仍在添加该方法以保持向后兼容性,并使调用的可读性更好。
完成上述操作后,我们现在可以使用静态工厂方法和属性来创建我们的转换——每个转换仍可以单独定义并具有自己的一组参数:
代码语言:javascript复制extension ImageTransform {
static var contrastBoost: Self {
ImageTransform { image in
image
}
}
static func portrait(withZoomMultipler multiplier: Double) -> Self {
ImageTransform { image in
image
}
}
static func grayScale(withBrightness brightness: BrightnessLevel) -> Self {
ImageTransform { image in
image
}
}
}
现在,函数、闭包单表达式函数将会隐式返回。可以将
Self
用作静态工厂方法的返回类型,Swift 5.1中的Self关键字 。
上面方法的优点在于,我们回到了将ImageTransform
定义为协议时所具有的灵活性和强大功能,同时仍然能够使用与使用枚举时大致相同的点语法:
let dramaticFilter = ImageFilter(
name: "Dramatic",
icon: .drama,
transforms: [
.portrait(withZoomMultipler: 2.1),
.contrastBoost,
.grayScale(withBrightness: .dark)
]
)
点语法与枚举无关,而是可以代替任何类型的静态API,它的功能非常强大——甚至可以通过将上述过滤器创建建模为计算的静态属性,使我们进一步封装东西好:
代码语言:javascript复制extension ImageFilter {
static var dramatic: Self {
ImageFilter(
name: "Dramatic",
icon: .drama,
transforms: [
.portrait(withZoomMultipler: 2.1),
.contrastBoost,
.grayScale(withBrightness: .dark)
]
)
}
}
经过以上所有操作的结果是,我们现在可以执行一系列非常复杂的任务——应用图像过滤器和转换——并将它们封装到一个API中,从表面上看,它像将值传递给函数一样轻巧:
代码语言:javascript复制let filtered = image.withFilter(.dramatic)
尽管可以轻松地将上述更改视为仅添加“语法糖(syntactic sugar)”,但我们不仅改善了API读取的方式,还改善了其组成的方式。由于所有的转换和过滤器现在都只是值,因此可以将它们以多种方式组合在一起——不仅使它们更轻巧,而且也更加灵活。
文章来自 John Sundell的Lightweight API design in Swift,简单翻译了上半部分,剩下的部分Swift:轻量级API的设计(二)
注:文中部分代码有做补充和修改,使得可以正常编译
附:
withFilter()
方法实现
extension Image {
func withFilter(_ imageFilter: ImageFilter) -> Image? {
var image = self
for trans in imageFilter.transforms {
do {
try image = trans.closure(self)
return image
} catch let error {
print(error)
return nil
}
}
return image
}
func withTransform(_ imageTransform: ImageTransform) -> Image? {
return try? imageTransform.closure(self)
}
}
补充了withTransform()
方法,提供一个可以自由组合ImageTransform
的方法,也提供一种API开发思路,可以提供预置的组合方法,也提供完全自定义的方法,使用示例:
let image = UIImage(named: "filter")!
//1、文中出现的使用预置滤镜方法
let filtered = image.withFilter(dramaticFilter)
//2、自定义滤镜方法一
let dramaticFilter = ImageFilter(
name: "Dramatic",
icon: .drama,
transforms: [
.portrait(withZoomMultiplier: 2.1),
.contrastBoost,
.grayScale(withBrightness: .dark),
]
)
let filtered = image.withFilter(dramaticFilter)
//2、自定义滤镜方法二
let filtered = image
.withTransform(.portrait(withZoomMultiplier: 2.1))?
.withTransform(.contrastBoost)?
.withTransform(.grayScale(withBrightness: .dark))