Kotlin 開發模式 是做什麼的?
符合慣用法的 Kotlin 模式與最佳實踐,用於打造穩健、高效且易於維護的應用程式。
使用時機
- 編寫新的 Kotlin 程式碼
- 審查 Kotlin 程式碼
- 重構現有的 Kotlin 程式碼
- 設計 Kotlin 模組或函式庫
- 配置 Gradle Kotlin DSL 建置
運作方式
此技能在七個關鍵領域強制執行符合慣用法的 Kotlin 慣例:使用型別系統與安全呼叫運算子的空值安全;透過 val 與資料類別的 copy() 實現的不變性;用於窮舉型別階層的密封類別與介面;使用協程與 Flow 實現的結構化並行;用於在不使用繼承的情況下新增行為的擴充函式;使用 @DslMarker 與 lambda 接收器實現的型別安全 DSL 建構器;以及用於建置配置的 Gradle Kotlin DSL。
範例
使用 Elvis 運算子的空值安全:
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user?.email ?: "[email protected]"
}
用於窮舉結果的密封類別:
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Failure(val error: AppError) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
使用 async/await 的結構化並行:
suspend fun fetchUserWithPosts(userId: String): UserProfile =
coroutineScope {
val user = async { userService.getUser(userId) }
val posts = async { postService.getUserPosts(userId) }
UserProfile(user = user.await(), posts = posts.await())
}
核心原則
1. 空值安全
Kotlin 的型別系統區分了可空與不可空型別。充分利用它。
// 良好:預設使用不可空型別
fun getUser(id: String): User {
return userRepository.findById(id)
?: throw UserNotFoundException("User $id not found")
}
// 良好:安全呼叫與 Elvis 運算子
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user?.email ?: "[email protected]"
}
// 不良:強制解開可空型別
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user!!.email // 若為 null 則拋出 NPE
}
2. 預設不變性
偏好使用 val 而非 var,使用不可變集合而非可變集合。
// 良好:不可變資料
data class User(
val id: String,
val name: String,
val email: String,
)
// 良好:使用 copy() 進行轉換
fun updateEmail(user: User, newEmail: String): User =
user.copy(email = newEmail)
// 良好:不可變集合
val users: List<User> = listOf(user1, user2)
val filtered = users.filter { it.email.isNotBlank() }
// 不良:可變狀態
var currentUser: User? = null // 避免可變的全域狀態
val mutableUsers = mutableListOf<User>() // 除非真的需要,否則避免
3. 表達式主體與單表達式函式
使用表達式主體來撰寫簡潔、可讀的函式。
// 良好:表達式主體
fun isAdult(age: Int): Boolean = age >= 18
fun formatFullName(first: String, last: String): String =
"$first $last".trim()
fun User.displayName(): String =
name.ifBlank { email.substringBefore('@') }
// 良好:將 when 作為表達式
fun statusMessage(code: Int): String = when (code) {
200 -> "OK"
404 -> "Not Found"
500 -> "Internal Server Error"
else -> "Unknown status: $code"
}
// 不良:不必要的區塊主體
fun isAdult(age: Int): Boolean {
return age >= 18
}
4. 用於值物件的資料類別
對於主要保存資料的型別,使用資料類別。
// 良好:附帶 copy、equals、hashCode、toString 的資料類別
data class CreateUserRequest(
val name: String,
val email: String,
val role: Role = Role.USER,
)
// 良好:用於型別安全的值類別(執行時期零開銷)
@JvmInline
value class UserId(val value: String) {
init {
require(value.isNotBlank()) { "UserId 不能為空白" }
}
}
@JvmInline
value class Email(val value: String) {
init {
require('@' in value) { "無效的電子郵件:$value" }
}
}
fun getUser(id: UserId): User = userRepository.findById(id)
密封類別與介面
對受限階層建模
// 良好:用於窮舉 when 的密封類別
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Failure(val error: AppError) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
fun <T> Result<T>.getOrNull(): T? = when (this) {
is Result.Success -> data
is Result.Failure -> null
is Result.Loading -> null
}
fun <T> Result<T>.getOrThrow(): T = when (this) {
is Result.Success -> data
is Result.Failure -> throw error.toException()
is Result.Loading -> throw IllegalStateException("仍在載入中")
}
用於 API 回應的密封介面
sealed interface ApiError {
val message: String
data class NotFound(override val message: String) : ApiError
data class Unauthorized(override val message: String) : ApiError
data class Validation(
override val message: String,
val field: String,
) : ApiError
data class Internal(
override val message: String,
val cause: Throwable? = null,
) : ApiError
}
fun ApiError.toStatusCode(): Int = when (this) {
is ApiError.NotFound -> 404
is ApiError.Unauthorized -> 401
is ApiError.Validation -> 422
is ApiError.Internal -> 500
}
作用域函式
何時使用哪一個
// let:轉換可空值或作用域結果
val length: Int? = name?.let { it.trim().length }
// apply:配置物件(回傳該物件)
val user = User().apply {
name = "Alice"
email = "[email protected]"
}
// also:副作用(回傳該物件)
val user = createUser(request).also { logger.info("已建立使用者:${it.id}") }
// run:以接收器執行區塊(回傳結果)
val result = connection.run {
prepareStatement(sql)
executeQuery()
}
// with:run 的非擴充形式
val csv = with(StringBuilder()) {
appendLine("name,email")
users.forEach { appendLine("${it.name},${it.email}") }
toString()
}
反模式
// 不良:巢狀作用域函式
user?.let { u ->
u.address?.let { addr ->
addr.city?.let { city ->
println(city) // 難以閱讀
}
}
}
// 良好:改為鏈式安全呼叫
val city = user?.address?.city
city?.let { println(it) }
擴充函式
在不使用繼承的情況下新增功能
// 良好:領域特定的擴充
fun String.toSlug(): String =
lowercase()
.replace(Regex("[^a-z0-9\\s-]"), "")
.replace(Regex("\\s+"), "-")
.trim('-')
fun Instant.toLocalDate(zone: ZoneId = ZoneId.systemDefault()): LocalDate =
atZone(zone).toLocalDate()
// 良好:集合擴充
fun <T> List<T>.second(): T = this[1]
fun <T> List<T>.secondOrNull(): T? = getOrNull(1)
// 良好:作用域擴充(不污染全域命名空間)
class UserService {
private fun User.isActive(): Boolean =
status == Status.ACTIVE && lastLogin.isAfter(Instant.now().minus(30, ChronoUnit.DAYS))
fun getActiveUsers(): List<User> = userRepository.findAll().filter { it.isActive() }
}
協程
結構化並行
// 良好:使用 coroutineScope 的結構化並行
suspend fun fetchUserWithPosts(userId: String): UserProfile =
coroutineScope {
val userDeferred = async { userService.getUser(userId) }
val postsDeferred = async { postService.getUserPosts(userId) }
UserProfile(
user = userDeferred.await(),
posts = postsDeferred.await(),
)
}
// 良好:當子任務可以獨立失敗時使用 supervisorScope
suspend fun fetchDashboard(userId: String): Dashboard =
supervisorScope {
val user = async { userService.getUser(userId) }
val notifications = async { notificationService.getRecent(userId) }
val recommendations = async { recommendationService.getFor(userId) }
Dashboard(
user = user.await(),
notifications = try {
notifications.await()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
emptyList()
},
recommendations = try {
recommendations.await()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
emptyList()
},
)
}
用於反應式串流的 Flow
// 良好:具有適當錯誤處理的冷 flow
fun observeUsers(): Flow<List<User>> = flow {
while (currentCoroutineContext().isActive) {
val users = userRepository.findAll()
emit(users)
delay(5.seconds)
}
}.catch { e ->
logger.error("觀察使用者時出錯", e)
emit(emptyList())
}
// 良好:Flow 運算子
fun searchUsers(query: Flow<String>): Flow<List<User>> =
query
.debounce(300.milliseconds)
.distinctUntilChanged()
.filter { it.length >= 2 }
.mapLatest { q -> userRepository.search(q) }
.catch { emit(emptyList()) }
取消與清理
// 良好:尊重取消
suspend fun processItems(items: List<Item>) {
items.forEach { item ->
ensureActive() // 在執行昂貴工作前檢查取消
processItem(item)
}
}
// 良好:使用 try/finally 進行清理
suspend fun acquireAndProcess() {
val resource = acquireResource()
try {
resource.process()
} finally {
withContext(NonCancellable) {
resource.release() // 即使取消也始終釋放
}
}
}
委派
屬性委派
// 惰性初始化
val expensiveData: List<User> by lazy {
userRepository.findAll()
}
// 可觀察屬性
var name: String by Delegates.observable("initial") { _, old, new ->
logger.info("名稱從 '$old' 變更為 '$new'")
}
// 基於 Map 的屬性
class Config(private val map: Map<String, Any?>) {
val host: String by map
val port: Int by map
val debug: Boolean by map
}
val config = Config(mapOf("host" to "localhost", "port" to 8080, "debug" to true))
介面委派
// 良好:委派介面實作
class LoggingUserRepository(
private val delegate: UserRepository,
private val logger: Logger,
) : UserRepository by delegate {
// 僅覆蓋需要新增日誌記錄的部分
override suspend fun findById(id: String): User? {
logger.info(\