Swift:轻量级API的设计(一)

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来使图片变成灰度。

然后,我们可以根据需要组合任意数量的上述类型,以形成每个滤镜——例如,通过一系列转换使图像具有某种“戏剧性”外观的滤镜:
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更加轻巧易用,因为我们可以使用点语法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:)方法,但我们仍在添加该方法以保持向后兼容性,并使调用的可读性更好。

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

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,它的功能非常强大——甚至可以通过将上述过滤器创建建模为计算的静态属性,使我们进一步封装东西好:

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)

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

文章来自 John SundellLightweight 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))

赏我一个赞吧~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容