Swift高級分享 - 設計Swift API

每個人都是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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章