Swift语言中的轻量级API设计

Swift语言自诞生以来,老是或多或少受到人们的非议,新生的编程语言不免有些不够尽善尽美,可是哪一种编程语言是尽善尽美的呢?OC语言算得上是一种古老的面向对象语言了,发展至今,其版本仍处于2.0,可是Apple为了让其看起来强大一点,增长了不少特性,例如Block、instancetype等等,可是其核心的语法变化并不大。编程

截止目前,Swift的版本已经迭代到5.*,整个ABI也已经稳定,每一次迭代更新,老是会带来一些漂亮的设计模式实践,例如在如何设计API方面,给开发者带来了温馨而强大的枚举、扩展和协议等,不只让开发者对于函数的定义有了更清晰的认识,并且对于构建API而言,第一印象每每是轻量的,同时,仍会根据须要逐步显现出更多的功能,以及底层的复杂性。swift

在本篇文章里,将尝试建立一些轻量级的API,以及如何使用API组合的力量使得功能或者系统更增强大等。设计模式

功能和易用性之间的较量

一般,当咱们设计API时,会在数据结构和函数功能的相互交互上,寻找一个相对平衡的方式,最终构建出在功能上知足需求,数据结构尽可能简单的API。可是,让API过于简单,可能它们又不够灵活,没法使功能有不断发展的潜力,然而,太过复杂的设计又不免致使开发工做复杂而无章法,容易形成开发者挫败,逻辑混乱并且API也难以使用,最终可能会致使延期甚至失败。api

例如,一款应用程序的主要功能是对用户选择的图像应用不一样的滤镜效果。每一种滤镜的核心其实都是一组图像变换的组合,不一样的变换组合造成不一样的滤镜效果。假设使用ImageFilter 结构体做为图像滤镜的定义,以下:数组

struct ImageFilter {
    var name: String
    var icon: Icon
    var transforms: [ImageTransform]
}
复制代码

ImageTransform是图像变换的统一入口,由于可能会由多种不一样的变换,所以能够将其定义为一个protocol,而后由实现单独变换操做的各类变换类型所遵循:markdown

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)
    ]
)
复制代码

So far so Good. 可是回头从新审视上述API的实现,能够确定的说,上述实现仅仅是为了功能的实现,在API的易用性方面并无优点,那么该如何进行优化,来保证功能的同时,提升API的灵活性和易用性呢?在上述实现中,每一个图像的变换都是做为单独的类型实现的,所以没有一个能够对全部变换类型一目了然的地方,使用者难以清楚该代码库都包含哪些图像变换的类型。app

为了解决外部使用者没法得知软件库所支持的变换类型,假设使用枚举的方式代替上述方式,来观察哪一种方式更可以体现API的简洁明了以及使用上的清晰易用?编程语言

enum ImageTransform {
    case protrait(_ zoomMultiplier: Double)
    case grayScale(_ brightnessLevel: BrightnessLevel)
    case contrastBoost
}
复制代码

使用枚举的好处既可以提升代码的整洁程度和可读性,也使得API更加的灵活易用,由于在枚举的使用上,开发者能够直接使用点语法构造任意数量的转换,以下:

let dramaticFilter = ImageFilter(
    name: "Dramatic",
    icon: .drama,
    transforms: [
        .protrait(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(_ multiplier: Double) -> Self {
        ImageTransform { image in
            // ...
        }
    }
    
    static func grayScale(_ brightness: BrightnessLevel) -> Self {
        ImageTransform { image in
            // ...
        }
    }
}
复制代码

在 Swift 5.1 中,能够将Self用做静态工厂方法的返回类型。

上面方法的优势在于,咱们回到了将ImageTransform定义为协议时所具备的灵活性和功能性,同时仍保持了与定义为枚举时的调用方式 --- 点语法一致,保证了易用性。

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

点语法自己与枚举无关,可是其能够与任何静态API一块儿使用,这点对于开发者而言很是友好。使用点语法能够将上述的几个滤镜的建立和建模构形成静态属性,使得咱们可以进一步的封装特性等。例如:

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

经过上述改造,一系列复杂的任务 --- 包括图像滤镜和图像转换 -- 封装到一个API中,在使用上,能够像传值给函数同样轻松。

let filtered = image.withFilter(.dramatic)
复制代码

上述一系列的改造能够成为为类型构造语法糖。不只改善了API读取的方式,还改善了API的组织方式,因为全部的转换和滤镜如今只须要进行传单一的值便可,所以在可扩展性方面来讲,可以组织多种方式,不只使得API轻巧灵活,对于使用者来讲也简洁明了。

可变参数与API设计

接下来咱们一块儿看看Swift语言的另外一个特性 --- 可变参数,以及可变参数如何影响API设计中的代码构建的。

假设正在开发一个使用基于形状的绘图来建立其用户界面的应用程序,而且咱们已经使用了与上述相似的基于结构的方法来对每种形状进行建模,并最终将结果绘制到了DrawingContext中:

struct Shape {
    var drawing: (inout DrawingContext) -> Void
}
复制代码

上面使用inout关键字来启用值类型(DrawingContext)的传递。

相似咱们在上面例子中使用静态工厂方法轻松建立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感受更轻巧和方便。

可是,使用可变参数的一个缺点是,预先计算的值数组不能再做为单个参数传递。值得庆幸的是,在这种状况下,能够经过建立一个特殊的组形状(就像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))
复制代码

不过,真正酷的是,上述组API不只使咱们可以构造形状数组,并且还使咱们可以更轻松地将多个形状组合到更高级的组件中。例如,这是咱们如何使用一组组合形状来表示整个图形(例如徽标)的方法:

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的数量,咱们也常常发现如何使它们变得更强大-经过使咱们建立类型的方式更灵活,以及使他们组成。全部这些均可以帮助咱们在简单性与功能之间实现完美的平衡。

原文: Lightweight API design in Swift

连接:www.swiftbysundell.com/articles/li…

相关文章
相关标签/搜索