[Day 27] 繼續談 Functional Programming,怎麼拆分邏輯

昨天我們談到 Ktor 可以用 Functional Programming 的方式來拆分邏輯,並拆分了一個 route 到其他檔案。

今天我們來繼續看看怎麼做拆分。

Functional Programming 的特色

Function as first class citizen

Functional Programming 的一個特色,是 function 是可以作為參數隨意傳遞的。

我們以前面的一段程式舉例:

get("/") {
    val a = async { client.get<String>("http://127.0.0.1:8080/a") }
    val b = async { client.get<String>("http://127.0.0.1:8080/b") }
    val c = async { client.get<String>("http://127.0.0.1:8080/c") }
    val result = a.await() + b.await() + c.await()
    client.close()
    call.respondText(result, contentType = ContentType.Text.Plain)
}

其中的 val result = a.await() + b.await() + c.await(),如果我們想要更換合併的方式而不更改 get("/") 裡面的內容,那我們可以把這段抽成函式:

suspend fun combineResult(a: Deferred<String>, b: Deferred<String>, c: Deferred<String>) =
        a.await() + b.await() + c.await()

private suspend fun PipelineContext<Unit, ApplicationCall>.root(
        client: HttpClient, combineStrategy: suspend (Deferred<String>, Deferred<String>, Deferred<String>) -> String
) {
    val a = async { client.get<String>("http://127.0.0.1:8080/a") }
    val b = async { client.get<String>("http://127.0.0.1:8080/b") }
    val c = async { client.get<String>("http://127.0.0.1:8080/c") }
    val result = combineStrategy(a, b, c)
    client.close()
    call.respondText(result, contentType = ContentType.Text.Plain)
}

get("/") {
    root(client, ::combineResult)
}

這樣,當我想替換邏輯時,我只要寫新的 combineStrategy 就好:

suspend fun combineResult(a: Deferred<String>, b: Deferred<String>, c: Deferred<String>) =
        a.await() + b.await() + c.await()

suspend fun combineNewResult(a: Deferred<String>, b: Deferred<String>, c: Deferred<String>) =
        b.await() + c.await() + a.await()

private suspend fun PipelineContext<Unit, ApplicationCall>.root(
        client: HttpClient, combineStrategy: suspend (Deferred<String>, Deferred<String>, Deferred<String>) -> String
) {
    val a = async { client.get<String>("http://127.0.0.1:8080/a") }
    val b = async { client.get<String>("http://127.0.0.1:8080/b") }
    val c = async { client.get<String>("http://127.0.0.1:8080/c") }
    val result = combineStrategy(a, b, c)
    client.close()
    call.respondText(result, contentType = ContentType.Text.Plain)
}

get("/") {
    root(client, ::combineNewResult)
}

root() 裡面的邏輯不需要改動。

有的讀者可能有點印象:這跟物件導向裡面的 Strategy Pattern 不是解決了一樣的問題嗎?沒有錯!這裡正是利用了 Function as first class citizen 的特點,在沒有實作 Strategy Pattern 的情況下,就解決了這個問題。