首页 今日热点 好文分享 生活资讯 科技资讯

Swift中的轻量级API设计

时间:2019-11-27 21:34 来源:网络 整理:紫沐兜 浏览:484

Swift的最强大功能之一就是在设计API方面给我们提供了多少灵活性。这种灵活性不仅使我们能够定义易于理解和使用的函数和类型,还使我们能够创建给人以非常轻量级第一印象的API,同时在需要时仍会逐步公开更多功能和复杂性。本周,让我们看一下使这些轻量级API得以创建的一些核心语言功能,以及我们如何使用它们通过组合的力量使功能或系统更加强大。

功率与易用性之间的权衡通常,当我们设计各种类型和功能如何相互交互时,我们必须在功能和易用性之间找到某种形式的平衡。使事情变得过于简单,它们可能不够灵活,无法使我们的功能不断发展-但是,另一方面,过于复杂通常会导致挫败感,误解并最终导致错误。

例如,假设我们正在开发一个应用程序,该应用程序使我们的用户可以对图像应用各种滤镜-例如,能够从其相机胶卷或图库中编辑照片。每个滤镜由图像变换数组组成,并使用一个ImageFilter结构定义,如下所示:

struct ImageFilter {    var name: String
    var icon: Icon
    var transforms: [ImageTransform]}

当涉及到ImageTransformAPI时,它目前被建模为协议,然后由实现我们各自的转换操作的各种类型所遵循:

protocol ImageTransform {    func apply(to image: Image) throws -> Image}struct PortraitImageTransform: ImageTransform {    var zoomMultiplier: Double
    func apply(to image: Image) throws -> Image {
        ...
    }}struct GrayScaleImageTransform: ImageTransform {    var brightnessLevel: BrightnessLevel
    func apply(to image: Image) throws -> Image {
        ...
    }}

上述方法的一个核心优势是,由于每个变换都是作为自己的类型实现的,因此我们可以自由地让每个类型定义自己的属性和参数集-例如将图像转换为灰度时如何GrayScaleImageTransform接受BrightnessLevel

然后,我们可以根据需要组合任意数量的上述类型,以形成每个滤镜-例如,通过一系列转换使图像具有某种“戏剧性”外观的滤镜

let dramaticFilter = ImageFilter(
    name: "Dramatic",
    icon: .drama,
    transforms: [
        PortraitImageTransform(zoomMultiplier: 2.1),        ContrastBoostImageTransform(),        GrayScaleImageTransform(brightnessLevel: .dark)
    ])

到目前为止,一切都很好。但是,如果我们仔细研究上述API,可以肯定地说,我们选择进行优化是为了提高功能和灵活性,而不是为了易于使用。由于每个转换都是作为一个单独的类型实现的,因此尚无法立即弄清我们的代码库包含哪种转换,因为没有一个单一的位置可以立即发现它们。

与之相比,如果我们选择使用枚举代替对我们的转换进行建模,则将为我们提供所有可能选项的清晰概述:

enum ImageTransform {    case portrait(zoomMultiplier: Double)    case grayScale(BrightnessLevel)    case contrastBoost}

使用枚举还可以产生非常漂亮且可读性强的调用站点-使我们的API感觉更加轻巧和易于使用,因为我们已经能够使用点语法构造任意数量的转换,如下所示:

let dramaticFilter = ImageFilter(
    name: "Dramatic",
    icon: .drama,
    transforms: [
        .portrait(zoomMultiplier: 2.1),
        .contrastBoost,
        .grayScale(.dark)
    ])

但是,尽管Swift枚举在许多不同情况下都是一种出色的工具,但实际上并不是其中之一。

由于每个转换都需要执行截然不同的图像操作,因此在这种情况下使用枚举将迫使我们编写一个庞大的switch语句来处理这些操作中的每一个-这很可能会成为噩梦。

以光为枚举,以结构为准

值得庆幸的是,还有第三个选择-这给了我们两全其美的选择。与其使用协议或枚举,不如使用结构,而该结构又包含一个封装了给定转换的各种操作的闭包:

struct ImageTransform {    let closure: (Image) throws -> Image
    func apply(to image: Image) throws -> Image {        try closure(image)
    }}

请注意,apply(to:)不再需要方法,但是我们仍然添加方法是为了向后兼容,并且使我们的呼叫站点更好看。

完成上述操作后,我们现在可以使用静态工厂方法和属性来创建我们的转换-每个转换仍可以单独定义并具有自己的一组参数:

extension ImageTransform {    static var contrastBoost: Self {        ImageTransform { image in
            ...
        }
    }
    static func portrait(withZoomMultipler multiplier: Double) -> Self {        ImageTransform { image in
            ...
        }
    }
    static func grayScale(withBrightness brightness: BrightnessLevel) -> Self {        ImageTransform { image in
            ...
        }
    }}

Self现在可以用作返回类型的静态工厂方法是一体的5.1引入了小但显著的改善

上面方法的优点在于,我们回到了定义ImageTransform为协议所具有的灵活性和强大功能,同时仍然能够使用与使用枚举时差不多的点句法:

let dramaticFilter = ImageFilter(
    name: "Dramatic",
    icon: .drama,
    transforms: [
        .portrait(withZoomMultipler: 2.1),
        .contrastBoost,
        .grayScale(withBrightness: .dark)
    ])

点语法与枚举无关,而是可以与任何类型的静态API一起使用的事实,其功能非常强大-甚至可以通过将上述过滤器创建建模为计算的静态属性,使我们进一步封装东西好:

extension ImageFilter {    static var dramatic: Self {        ImageFilter(
            name: "Dramatic",
            icon: .drama,
            transforms: [
                .portrait(withZoomMultipler: 2.1),
                .contrastBoost,
                .grayScale(withBrightness: .dark)
            ]
        )
    }}

以上所有结果的结果是,我们现在可以执行一系列非常复杂的任务-应用图像过滤器和转换-并将它们封装到一个API中,从表面上看,它像将值传递给函数一样轻巧:

let filtered = image.withFilter(.dramatic)

尽管仅将“语法糖”纯粹添加而忽略了上述更改,但我们不仅改善了API读取的方式,还改善了其组成部分的方式。由于所有的转换和过滤器现在都只是值,因此可以将它们以多种方式组合在一起-不仅使它们更轻巧,而且也更加灵活。

可变参数和进一步的构成

接下来,让我们看一下另一个非常有趣的语言功能-可变参数-以及它们可以解锁的API设计选择。

现在,我们说我们正在开发一个使用基于形状的绘图来创建其用户界面的一部分的应用程序,并且我们已经使用了与上述类似的基于结构的方法来建模每种形状的绘制方式变成DrawingContext

struct Shape {    var drawing: (inout DrawingContext) -> Void}

在上方,我们使用inout关键字启用值类型(DrawingContext)的传递,就好像它是引用一样。有关该关键字的更多信息以及一般的值语义,请查看“在Swift中使用值语义”

就像我们ImageTransform以前如何使用静态工厂方法轻松创建值一样,我们现在也能够将每个形状的绘图代码封装在完全独立的方法中,如下所示:

extension Shape {    func square(at point: Point, sideLength: Double) -> Self {        Shape { context in
            let origin = point.movedBy(
                x: -sideLength / 2,
                y: -sideLength / 2
            )
            context.move(to: origin)
            context.drawLine(to: origin.movedBy(x: sideLength))
            context.drawLine(to: origin.movedBy(x: sideLength, y: sideLength))
            context.drawLine(to: origin.movedBy(y: sideLength))
            context.drawLine(to: origin)
        }
    }}

由于每个形状都只是简单地建模为一个值,因此绘制它们的数组变得非常容易-我们要做的就是创建一个的实例DrawingContext,然后将其传递到每个形状的闭包中以构建最终图像:

func draw(_ shapes: [Shape]) -> Image {    var context = DrawingContext()
    
    shapes.forEach { shape in
        context.move(to: .zero)
        shape.drawing(&context)
    }
    
    return context.makeImage()}

调用上面的函数看起来也很优雅,因为我们再次能够使用点语法来大大减少执行工作所需的语法量:

let image = draw([
    .circle(at: point, radius: 10),
    .square(at: point, sideLength: 5)])

但是,让我们看看是否可以使用可变参数将事情更进一步。尽管不是Swift独有的功能,但结合Swift真正灵活的参数命名功能后,使用可变参数可以产生一些非常有趣的结果。

当参数被标记为可变参数时(通过将...后缀添加到其类型中),我们基本上可以将任意数量的值传递给该参数-编译器将自动为我们将这些值组织到一个数组中,如下所示:

func draw(_ shapes: Shape...) -> Image {
    ...
    // Within our function, 'shapes' is still an array:
    shapes.forEach { ... }}

完成上述更改后,我们现在可以从对draw函数的调用中删除所有数组文字,并使它们看起来像这样:

let image = draw(.circle(at: point, radius: 10),
                 .square(at: point, sideLength: 5))

这看起来似乎不是一个很大的变化,但是特别是在设计旨在用于创建更多更高级别值(例如我们的draw函数)的更低级别的API时,使用可变参数可以使这类API感觉良好更轻巧方便。

但是,使用可变参数的一个缺点是,预先计算的值数组不能再作为单个参数传递。值得庆幸的是,在这种情况下,可以通过创建一个特殊的group形状很容易地解决该问题,就像draw函数本身一样,可以在一系列基础形状上进行迭代并绘制它们:

extension Shape {    static func group(_ shapes: [Shape]) -> Self {        Shape { context in
            shapes.forEach { shape in
                context.move(to: .zero)
                shape.drawing(&context)
            }
        }
    }}

完成上述操作后,我们现在可以再次轻松地将一组预先计算的Shape值传递给我们的draw函数,如下所示:

let shapes: [Shape] = loadShapes()let image = draw(.group(shapes))

不过,真正酷的是,上述groupAPI 不仅使我们能够构造形状的数组-还使我们能够更轻松地将多个形状组合成更高级的组件。例如,以下是我们使用一组组合形状来表示整个图形(例如徽标)的方法:

extension Shape {    static func logo(withSize size: Size) -> Self {
        .group([
            .rectangle(at: size.centerPoint, size: size),
            .text("The Drawing Company", fittingInto: size),
            ...
        ])
    }}

由于上述徽标Shape与其他徽标一样,因此可以draw通过使用与之前使用的相同的优雅点语法方法进行一次调用来轻松绘制该徽标

let logo = draw(.logo(withSize: size))

有趣的是,尽管我们最初的目标可能是使我们的API更轻量级,但这样做也使它的可组合性和灵活性也更高。

结论

我们向“ API设计器的工具箱”中添加的工具越多,我们越有可能设计出能够在功能,灵活性和易用性之间达到适当平衡的API。使API尽可能轻便可能不是我们的最终目标,但是通过尽可能减少API的数量,我们还经常发现如何使它们变得更强大-通过使我们创建类型的方式更灵活,以及使他们组成。所有这些都可以帮助我们在简单性与功能之间实现完美的平衡。

顶: 0 踩: 0