游諭 Swift Dev 🦄

運用泛型實現 Fluent interface Setter

Fluent Interface(FI),又名流式接口,是一種在程式編寫的方式,這篇文章將描述 FI 的意義與如何在 Swift 5.1 實作。

什麼是 Fluent interface

流式接口可以將指令式語言轉化成宣告式語言,使簡易的邏輯判斷消失,進而加強程式碼的可讀性。在 FI 中有許多應用,我分成了 3 種類型 Transformer, Getter, Setter

Transformer

我們在操作集合類型的時候,常常使用 filter、map、reduce,像是:

let input = [1, 2, 3]
let output = input.map(String.init)

相比沒有使用 map 函式,我們可以使用 for in 迴圈來處理:

let input = [1, 2, 3]
var output = [String]()
for value in input {
    output.append(String(value))
}

在 map 函式中,可以看到原先的 input 是一個 let 的變數,明顯可以看到 output 的生成不會改變來源的數值,因此他是一種 Transform 的功能。

Getter

在一些常見的程式語言中常有空的概念(Null),尤其在 Swift 中更是個不能忽略的事情。當我們要使用一個巢狀型別如下:

struct Person {
    var name:String
    var pet:Pet?
    struct Pet {
        var petName:String
        var speakSound: String?
    }
}

我們有一個 Person 型別,這個 Person 有必須的 name 屬性,和一個非必須的 pet 屬性(Pet型別),而這個 Pet 有必須的 petName,和一個非必須的 speakSound。假如我們需要取得所有 person 的 pet 的speakSound,為了避免空的問題,我們可以用 if else 來處理:

// [進階]:為了避免誤解,我省略了 Implicitly Unwrapped Optional

let theMan = Person(name: "Yu", 
                    pet: Pet(petName: "dog", speakSound: "bark"))
                    
if theMan.pet != nil {
    if theMan.pet.speakSound != {
        print(theMan.pet.speakSound) // bark
    }
}

而在 Swift 中,我們可以使用 Optional chaining 來將這個判斷封裝起來:

let theWoman = Person(name: "Swift", 
                      pet: tir)
print(theWoman.pet?.speakSound ?? "...") // ...

在這個例子,我們看到使用 Optional chaining 可以大幅簡化程式碼的複雜情況,讓取得物件 property 的程式可以不需要明顯的邏輯判斷,而是將判斷封裝至 ? 這個運算符號。

Setter

在物件導向設計模式中,有一個 Builder 模式(生成器模式),它可以將複雜對象的建造過程抽象出來(抽象類別),使這個抽象過程的不同實現方法可以構造出不同表現(屬性)的對象。 圖片來自 https://en.wikipedia.org/wiki/Builder_pattern

在 Swift Foundation 中,DateComponents 是一個常見的 Builder:

var dateComponents = DateComponents()
dateComponents.calendar = .current
dateComponents.year = 2020
dateComponents.month = 3
dateComponents.day = 14
dateComponents.hour = 1
dateComponents.minute = 20
dateComponents.second = 59
dateComponents.date // 2020-03-13 17:20:59 +0000

而在我的 FluentInterface 這個 Swift Package 中,我們可以改變這個逐步賦值的方式:

let dateComponents = DateComponents()+
                      .calendar(.current)
                      .year(2020)
                      .month(3)
                      .day(14)
                      .hour(1)
                      .minute(20)
                      .second(59)
                      .unwrappingSubject()
                      .date
dateComponents.date // 2020-03-13 17:20:59 +0000

是不是相對的更直覺一些呢!

FluentInterface 的不好地方

在 2019 年 appcode.com.tw 的 https://www.appcoda.com.tw/fluent-interface/ 文章中,我受到不少啟發,特別分享這篇好文章。 2020 年 3 月時我在 CocoaHeads Taipei 分享了這個 repo,感謝其他與會的開發者提問有關於效能的問題,是的 FI 效率很不好,在連續 1000000 的 Object 生成並 賦予 60 個不同 property 相同數值時,若使用匿名函式(Anonymous Function),單元測試顯示 10 次執行平均時間約為 0.810 秒; 而 FluentInterface 實測同樣條件需要 15.796 秒。因此在使用 FI 的時候,考慮效能在大數量的狀況是必須注意的。

線上的 CocoaHeads Taipei 聚會

文末特別感謝許立恆、陳涵宇等等開發者。

Tagged with: