經過這二天的 KSP 介紹,不知道大家對這門技術的感覺如何呢?
今天筆者想要透過一些實用的簡單範例,讓大家可以更理解 KSP 可以運用在哪裡!
不曉得對 Design pattern 熟悉的讀者對於 Builder pattern 的想法是怎樣,筆者是覺得在 Kotlin 使用上有點贅、加上 Kotlin 的 class 有 default value 可以使用,實際使用的機會變得蠻少的,但如果你是因為寫起來有點贅的原因,或許這時候就可以使用 KSP 來幫忙了!
data class MyObj(
val value1: String,
val value2: Int? = 0,
)
假設我們有以上這樣的物件,相對應的 Builder class 應該如下定義:
public class MyObjBuilder {
public var value1: String? = null
public var value2: Int? = null
public fun build(): MyObj {
val value1 = value1 ?: throw RuntimeException("value1 is not set yet!")
return MyObj(value1, value2)
}
}
而大家想想看這個 MyObjBuilder
的程式碼,是不是全部都是依據 MyObj
所決定的呢?
有沒有可能我們在 MyObj
上面加個 annotation 就讓 MyObjBuilder
自動產生呢?
@KBuilder
data class MyObj(
val value1: String,
val value2: Int? = 0,
)
答案當然是可以的,透過簡單的 parser 就可以做到,如果想了解更多可以參考 Github 上的原始碼範例:
Factory pattern 也是一個蠻意思的 Design pattern,我們定義了一個統一的入口點,依據 input 的 type 來建立相對應的實體回傳。
一個簡單的範例如下:
object AnimalFactory {
fun createAnimal(type: String): Animal {
when (type) {
"cat" -> return Cat()
"dog" -> return Dog()
}
throw RuntimeException("not support type")
}
}
第一眼看起來蠻不錯的,但之後如果要新增、移除、修改的話,我們就要一直不斷地調整 AnimalFactory
這個 class,而且用 String
感覺很容易打錯,如果要提出來又多了一系列的物件必須維護,而且整個 Factory 的核心其實不在於這個 Factory class 上,而在於 Animal
、Dog
跟 Cat
,是他們之間的關係讓這個 AnimalFactory
的結構變成必然,既然是必然的結果那我們有沒有可能讓 KSP 幫我們建立整個 AnimalFactory
呢?
這邊比 KBuilder 稍微複雜點,整個關係圖上有二個角色。一個是 base class、一個是 real implementation,我們使用 @AutoFactory
跟 @AutoElement
分別幫它們標註如下:
@AutoFactory
interface Animal {...}
@AutoElement
class Dog : Animal {...}
@AutoElement
class Cat: Animal {...}
接下來就可以寫個 Processor 來解析這些資訊,判斷他們真的有繼承關係之後,把它們加在同個 Factory 下就可以了,而 type 的部分也可以用這些 class name 自動生成,範例 factory 如下:
public enum class AnimalType { CAT, DOG }
public fun AnimalFactory(key: AnimalType): Animal = when (key) {
AnimalType.CAT -> Cat()
AnimalType.DOG -> Dog()
}
如果想了解更多可以參考 Github 上的原始碼範例:
這邊補充一個可能有用的小知識,筆者一開始開發 KFactory 的時候命名為 Kactory,後來才發現這個在某些語系讀音近似於大便的意思,所以後來才改成 KFactory XD。
還記得我們在介紹 Ktor 的時候曾經有說之後會補一個怎麼讓 Ktor 的使用跟 Retorfit 一樣精簡嗎?
以下是一個 Retrofit 的使用範例:
interface GithubService {
@GET("/users/{userName}")
suspend fun getUser(@Path userName: String): User
@GET("/users/{name}/repos")
suspend fun getRepos(@Path("name") userName: String, @Query("sort") sort: String?): List<Repo>
}
而具體的 class 會由 Retrofit 在 runtime 提供,但 Ktor 並沒有這層的功能,所以我們必須自己串 function 上的參數到 api 的 path 上,範例如下:
suspend fun getUser(@Path userName: String): User {
return client.get("https://api.github.com/users/$userName").body()
}
雖然並不是很複雜,但就像我們前面舉的例子一樣,這些其實都是有固定的 pattern,由工程師手動來維護其實沒什麼效率,生命應該浪費在更有意義的事情上,所以我們也可以使用 KSP 來自動產生相關程式碼,自動產生的範例如下:
public fun HttpClient.createGithubService(): GithubService = GithubServiceImpl(this)
private class GithubServiceImpl(
private val client: HttpClient,
) : GithubService {
public override suspend fun getUser(userName: String): User {
val _builder = StringBuilder("/users/$userName")
val _result: User = client.`get`(_builder.toString()).body()
return _result
}
public override suspend fun getRepos(userName: String, sort: String?): List<Repo> {
val _builder = StringBuilder("/users/$userName/repos")
if (sort != null) {
_builder.append("?sort=$sort")
}
val _result: List<Repo> = client.`get`(_builder.toString()).body()
return _result
}
}
當然這個範例比上述二個更複雜了些,不過相信大家可以慢慢體會到 KSP 可以運用的場域了吧!
如果想了解更多可以參考 Github 上的原始碼範例:
好,希望大家能不僅止於了解 KSP,有一天也能透過 KSP 寫出自己的小專案囉!