Swift高级分享 - 设计Swift API

15 篇文章 0 订阅

每个人都是API设计师。虽然很容易将API视为仅与打包代码(如SDK或框架)相关的内容,但事实证明,所有应用程序开发人员几乎每天都会设计API。每次我们定义非私有属性或函数时,实际上我们都在设计API。

但是,设计出色的 API起初可能非常棘手。我们不仅必须在易用性和提供足够的功能之间取得平衡,我们还需要考虑到这样一个事实,即不同的人将在我们的API领域中拥有不同程度的知识 - 并且还涉及到一定的品味。好。

本周,让我们来看看在Swift中设计各种API时要记住的一些技巧和技巧 - 以及我们如何创建既易于使用又功能强大的API。

同时小编这里有些书籍和面试资料哦(点击下载

上下文和呼叫站点

真正优秀的API的一个关键特性是它提供了恰当的上下文,使其感觉直观和自然。添加太多的上下文,API开始感到*“狡猾”*和冗长,并且由于上下文太少,它变得令人困惑和模棱两可。

例如,假设我们正在构建某种形式的购物应用程序,并且我们正在为我们的一个关键模型 - 购物车设计API。我们首先创建一种通过向其添加产品来改变购物车的方法,如下所示:

struct ShoppingCart {
    mutating func add(product: Product) {
        ...
    }
}

乍一看,上述API似乎确实在简单性和清晰度之间取得了很好的平衡。如果我们只看一下这个定义就会很清楚:“添加产品”

但是,在设计API时,我们不应该查看我们的属性和方法的定义 - 我们应该在调用站点查看它们将如何使用,它们描绘了一幅略有不同的图片:

let product = loadProduct()
cart.add(product: product)

以上不是任何灾难product,因为在那里有外部参数标签感觉有点不必要,因为我们已经清楚的是,我们添加的内容实际上是一个产品 - 给定使用的模型。因此,让我们继续通过在其前面添加下划线来删除该标签:

struct ShoppingCart {
    mutating func add(_ product: Product) {
        ...
    }
}

它可能看起来像一个挑剔的细节,但上述变化确实使我们的呼叫网站更好阅读 - 就像一个正确的英语句子甚至 - “购物车:添加产品”:

let product = loadProduct()
cart.add(product)

另一方面,如果我们正在处理的类型不能使上下文清晰,那么删除外部标签会使事情变得非常混乱。以此方法为例,这使我们能够计算将购物车中的所有产品运送到给定地址的总价格:

extension ShoppingCart {
    func calculateTotalPrice(_ address: Address) -> Price {
        ...
    }
}

再说一次,通过查看上面的方法定义,我们可以发现该地址最有可能用于计算运费,但在阅读呼叫网站时,这肯定不明确:

let price = cart.calculateTotalPrice(user.address)

上面几乎看起来像程序员错误 - 就像错误的数据被传递给错误的方法。这是一个主要迹象,表明我们还没有设计足够清晰的API。让我们通过添加一个外部参数标签来解决这个问题,该标签清楚地说明了我们将使用的地址:

extension ShoppingCart {
    func calculateTotalPrice(shippingTo address: Address) -> Price {
        ...
    }
}

现在给我们以下呼叫网站:

let price = cart.calculateTotalPrice(shippingTo: user.address)

好多了!我们再次通过将其作为英语句子(添加了一些次要的*“胶水”)来验证我们的API的清晰度:“购物车:计算运送到用户地址的总价格。”*。

嵌套类型和重载

另一个在我们的*“API设计器工具箱”中非常有用的工具是嵌套类型。就像我们在“使用嵌套类型的命名空间Swift代码”中*看到的那样,构建相关类型的层次结构可能是提供其他上下文的好方法。

假设我们正在为我们的购物应用添加一项新功能,这使得供应商能够定义可以作为一个单元出售的产品。只是添加一个被调用的顶级模型Bundle可能无法提供足够的上下文,一眼就能理解我们正在谈论的是产品包- 特别是考虑到我们与基金会的Bundle类型相冲突(这不会给我们带来影响)实际的编译错误,但在清晰度方面仍然不是很好)。

让我们将我们的Bundle类型嵌入其中Product- 为我们提供额外的上下文以使我们的API清晰:

extension Product {
    struct Bundle {
        var name: String
        var products: [Product]
    }
}

具有明确命名类型的一大好处是它使我们能够使用方法重载来定义类似的API,同时仍然保持清晰。例如,要将产品包添加到购物车,我们可以从以前重载我们的addAPI - 同样为我们提供相同的好的调用站点:

extension ShoppingCart {
    mutating func add(_ bundle: Product.Bundle) {
        bundle.products.forEach { add($0) }
    }
}

任何使用我们ShoppingCartAPI的人现在只需要知道关键动词add- 无论我们添加的是产品,捆绑还是其他任何东西。这两者都使代码非常优雅,同时也使我们的API学习曲线不那么陡峭。

强打字

在API设计方面,命名很重要,但可以说更重要的是涉及的实际类型。充分利用Swift强大而强大的类型系统可以使我们的API更直观,更不容易出错。

假设我们希望添加到我们的下一个功能ShoppingCart是支持折扣和促销的优惠券代码。由于用户将使用文本字段输入实际的优惠券代码,因此最初的想法可能是String从该文本字段中取出并直接将其传递到我们的购物车中 - 如下所示:

extension ShoppingCart {
    mutating func apply(couponCode code: String) {
        ...
    }
}

虽然上述方法非常方便,但我们的API可能会意外地与不兼容的输入一起使用。因为code只是一个简单的String,任何字符串都可以传递给它 - 并且因为字符串在所有程序中都非常普遍,所以很可能很难合并,或者只是误解,可能会导致类似这样的事情:

cart.apply(couponCode: user.name)

上面显然是错误的,但编译器不会警告我们,因为我们将有效字符串传递给接受字符串的方法。为了解决这个问题,并使我们的API更加健壮,让我们引入一个专用Coupon类型 - 它将包含基于字符串的代码作为属性。

这样做也可以让我们简化我们的apply方法,因为现在涉及的类型使上下文清晰(就像add之前的API一样):

struct Coupon {
    let code: String
}

extension ShoppingCart {
    mutating func apply(_ coupon: Coupon) {
        ...
    }
}

如果我们再次看一下调用网站,有趣的是它读取的方式与之前完全相同(“购物车:应用优惠券代码”),但现在有了更加类型安全的设置:

cart.apply(Coupon(code: "spring-sale"))

虽然直接使用原始值(如字符串,整数等)在我们处理实际文本和数字时是完全合适的,但对于更具体的用法,引入专用类型的额外*“仪式”*通常是值得的。

可扩展的API

可以真正建立或破坏API的另一个方面是它根据不同的用例进行扩展的程度。理想情况下,最常见的用例应该非常简单,而通过平滑添加更多参数或自定义选项,应该可以实现更高级的用例。

例如,假设我们正在构建一个ImageTransformer,它允许我们将各种变换应用于UIImage。目前,我们的API看起来像这样:

struct ImageTransformer {
    func transform(_ image: UIImage,
                   scale: CGVector,
                   angle: Measurement<UnitAngle>,
                   tintColor: UIColor?) -> UIImage {
        ...
    }
}

我们再次使用上面的强类型,通过使用内置Measurement类型来表示角度,而不是直接传递数值。

如果我们想要同时缩放,旋转和着色图像,上述工作非常有用 - 但很有可能在很多地方我们只想执行一个或两个特定的变换。为了实现这一点,不必总是将*“虚拟数据”*传递给我们不感兴趣的变换 - 让我们的API可扩展,方法是为所有参数添加默认值image

我们还将利用这个机会为所有变换添加外部参数标签,以便无论提供多少参数,我们的调用站点都可以很好地读取:

struct ImageTransformer {
    func transform(
        _ image: UIImage,
        scaleBy scale: CGVector = .zero,
        rotateBy angle: Measurement<UnitAngle> = .zero,
        tintWith color: UIColor? = nil
    ) -> UIImage {
        ...
    }
}

// To enable us to simply use '.zero' to create a
// Measurement instance above, we'll add this extension:
extension Measurement where UnitType: Dimension {
    static var zero: Measurement {
        return Measurement(value: 0, unit: .baseUnit())
    }
}

有了上述变化,我们现在已经获得了很多关于如何使用API​​的灵活性,并且所有各种排列都为我们提供了清晰的呼叫站点 - 有足够的上下文来查看正在发生的事情:

// Rotate an image
let angle = Measurement<UnitAngle>(value: 180, unit: .degrees)
transformer.transform(image, rotateBy: angle)

// Scale and tint an image
let scale = CGVector(dx: 0.5, dy: 1.2)
transformer.transform(image, scaleBy: scale, tintWith: .blue)

// Apply all supported transforms to an image
transformer.transform(image,
    scaleBy: scale,
    rotateBy: angle,
    tintWith: .blue
)

上述方法唯一真正的缺点是,由于现在可以省略所有非图像参数,因此可以transform仅使用图像调用我们的API而不进行变换 - 有效地再次返回相同的图像 - 但这是一种权衡。在这种情况下最有可能值得做。

便利包装

使API可扩展的另一种方法是在便利API中包装一些更高级的方法,这些方法执行给定情况下所需的所有底层定制。

举个例子,假设我们在我们的应用程序中展示了很多模态对话框,并且我们编写了一个扩展,UIViewController以便更容易设置DialogViewController显示给定对话框的实例:

extension UIViewController {
    func presentDialog(ofKind kind: DialogKind,
                       title: String,
                       text: String,
                       actions: [DialogAction]) {
        let dialog = DialogViewController()
        ...
        present(dialog, animated: true)
    }
}

上面已经是一个方便的API本身,但我们仍然可以使它更容易用于我们的一些最常见的用例。

假设我们使用上述API在许多不同的地方显示确认对话框,这样做需要我们将DialogQuestion模型中的数据转换为对上述presentDialog方法的调用。为了封装该转换逻辑,并提供另一个便利性,让我们创建一个特定于呈现确认对话框的包装方法:

extension UIViewController {
    func presentConfirmation(for question: DialogQuestion) {
        presentDialog(
            ofKind: .confirmation,
            title: question.title,
            text: question.explanation,
            actions: [
                question.actions.cancel,
                question.actions.confirm
            ]
        )
    }
}

我们上面的API套件现在可以很好地从最简单的用例(呈现确认对话框),到更高级的(呈现任何类型的对话框),完全可自定义(DialogViewController直接创建实例)。无论在任何特定情况下我们需要什么级别的控制,我们现在都可以使用我们专门为此量身定制的API。

结论

什么使一个非常好的API很可能永远不会是一个精确的科学,因为不同的情况保证不同的解决方案,每个开发人员有自己的首选方式设计和使用各种AP​​I。

虽然正式的Swift API指南以及本文中的文章中提到的技巧提供了一个坚实的起点 - 但这一切都归结为我们为每个API提出以下问题:“我是否已尽我所能这个API易于使用,并且尽可能清晰明了,“。不管你喜不喜欢,我们都是API设计师。

问题,意见或反馈?请通过加我们的交流群 点击此处进交流群 ,来一起交流或者发布您的问题,意见或反馈。

谢谢阅读~点个赞再走呗!?

原文地址 https://www.swiftbysundell.com/posts/designing-swift-apis

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值