iT邦幫忙

2022 iThome 鐵人賽

DAY 27
0
Mobile Development

Kotlin 全面啟動 系列 第 27

[Kotlin 全面啟動] KSP III

  • 分享至 

  • xImage
  •  

經過這二天的 KSP 介紹,不知道大家對這門技術的感覺如何呢?

今天筆者想要透過一些實用的簡單範例,讓大家可以更理解 KSP 可以運用在哪裡!

KBuilder

不曉得對 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 上的原始碼範例:

KFactory

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 上,而在於 AnimalDogCat,是他們之間的關係讓這個 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。

Ktorfit

還記得我們在介紹 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 寫出自己的小專案囉!


上一篇
[Kotlin 全面啟動] KSP II
下一篇
[Kotlin 全面啟動] Koin
系列文
Kotlin 全面啟動 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言