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…

相關文章
相關標籤/搜索