Was macht Kotlin-Entwicklungsmuster?
Idiomatische Kotlin-Muster und Best Practices für den Bau robuster, effizienter und wartbarer Anwendungen.
Verwendungshinweise
- Schreiben von neuem Kotlin-Code
- Überprüfen von Kotlin-Code
- Refactoring vorhandenen Kotlin-Codes
- Entwerfen von Kotlin-Modulen oder -Bibliotheken
- Konfigurieren von Gradle Kotlin DSL-Builds
Funktionsweise
Diese Fähigkeit setzt idiomatische Kotlin-Konventionen in sieben Kernbereichen durch: Nullsicherheit mithilfe des Typsystems und sicherer Aufrufoperatoren, Unveränderlichkeit durch val und copy() in Datenklassen, versiegelte Klassen und Interfaces für erschöpfende Typhierarchien, strukturierte Nebenläufigkeit mit Coroutinen und Flow, Erweiterungsfunktionen zum Hinzufügen von Verhalten ohne Vererbung, typsichere DSL-Builder mit @DslMarker und Lambda-Empfängern sowie die Gradle Kotlin DSL für die Build-Konfiguration.
Beispiele
Nullsicherheit mit Elvis-Operator:
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user?.email ?: "[email protected]"
}
Versiegelte Klasse für erschöpfende Ergebnisse:
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>()
}
Strukturierte Nebenläufigkeit mit 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())
}
Kernprinzipien
1. Nullsicherheit
Das Kotlin-Typsystem unterscheidet zwischen nullable und non-nullable Typen. Nutzen Sie es vollständig aus.
// Gut: Standardmäßig Non-Nullable-Typen verwenden
fun getUser(id: String): User {
return userRepository.findById(id)
?: throw UserNotFoundException("User $id not found")
}
// Gut: Sichere Aufrufe und Elvis-Operator
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user?.email ?: "[email protected]"
}
// Schlecht: Erzwungenes Auspacken von Nullable-Typen
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user!!.email // Wirft NPE, falls null
}
2. Unveränderlichkeit als Standard
Bevorzugen Sie val gegenüber var, unveränderliche Kollektionen gegenüber veränderlichen.
// Gut: Unveränderliche Daten
data class User(
val id: String,
val name: String,
val email: String,
)
// Gut: Transformation mit copy()
fun updateEmail(user: User, newEmail: String): User =
user.copy(email = newEmail)
// Gut: Unveränderliche Kollektionen
val users: List<User> = listOf(user1, user2)
val filtered = users.filter { it.email.isNotBlank() }
// Schlecht: Veränderlicher Zustand
var currentUser: User? = null // Vermeiden Sie veränderlichen globalen Zustand
val mutableUsers = mutableListOf<User>() // Nur verwenden, wenn wirklich nötig
3. Ausdruckskörper und Einzelausdruck-Funktionen
Verwenden Sie Ausdruckskörper für prägnante, lesbare Funktionen.
// Gut: Ausdruckskörper
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('@') }
// Gut: When als Ausdruck
fun statusMessage(code: Int): String = when (code) {
200 -> "OK"
404 -> "Not Found"
500 -> "Internal Server Error"
else -> "Unknown status: $code"
}
// Schlecht: Unnötiger Blockkörper
fun isAdult(age: Int): Boolean {
return age >= 18
}
4. Datenklassen für Wertobjekte
Verwenden Sie Datenklassen für Typen, die hauptsächlich Daten halten.
// Gut: Datenklasse mit copy, equals, hashCode, toString
data class CreateUserRequest(
val name: String,
val email: String,
val role: Role = Role.USER,
)
// Gut: Wertklasse für Typsicherheit (kein Overhead zur Laufzeit)
@JvmInline
value class UserId(val value: String) {
init {
require(value.isNotBlank()) { "UserId cannot be blank" }
}
}
@JvmInline
value class Email(val value: String) {
init {
require('@' in value) { "Invalid email: $value" }
}
}
fun getUser(id: UserId): User = userRepository.findById(id)
Versiegelte Klassen und Interfaces
Modellierung eingeschränkter Hierarchien
// Gut: Versiegelte Klasse für erschöpfendes 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("Still loading")
}
Versiegelte Interfaces für API-Antworten
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
}
Bereichsfunktionen (Scope Functions)
Wann welche verwendet werden sollte
// let: Nullable oder bereichsgebundenes Ergebnis transformieren
val length: Int? = name?.let { it.trim().length }
// apply: Ein Objekt konfigurieren (gibt das Objekt zurück)
val user = User().apply {
name = "Alice"
email = "[email protected]"
}
// also: Seiteneffekte (gibt das Objekt zurück)
val user = createUser(request).also { logger.info("Created user: ${it.id}") }
// run: Einen Block mit Empfänger ausführen (gibt Ergebnis zurück)
val result = connection.run {
prepareStatement(sql)
executeQuery()
}
// with: Nicht-Erweiterungsform von run
val csv = with(StringBuilder()) {
appendLine("name,email")
users.forEach { appendLine("${it.name},${it.email}") }
toString()
}
Anti-Patterns
// Schlecht: Verschachteln von Bereichsfunktionen
user?.let { u ->
u.address?.let { addr ->
addr.city?.let { city ->
println(city) // Schwer zu lesen
}
}
}
// Gut: Stattdessen sichere Aufrufe verketten
val city = user?.address?.city
city?.let { println(it) }
Erweiterungsfunktionen
Funktionalität ohne Vererbung hinzufügen
// Gut: Domänenspezifische Erweiterungen
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()
// Gut: Kollektionserweiterungen
fun <T> List<T>.second(): T = this[1]
fun <T> List<T>.secondOrNull(): T? = getOrNull(1)
// Gut: Bereichsgebundene Erweiterungen (verschmutzen nicht den globalen Namensraum)
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() }
}
Coroutinen
Strukturierte Nebenläufigkeit
// Gut: Strukturierte Nebenläufigkeit mit 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(),
)
}
// Gut: supervisorScope, wenn Kinder unabhängig fehlschlagen können
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 für reaktive Streams
// Gut: Kalter Flow mit korrekter Fehlerbehandlung
fun observeUsers(): Flow<List<User>> = flow {
while (currentCoroutineContext().isActive) {
val users = userRepository.findAll()
emit(users)
delay(5.seconds)
}
}.catch { e ->
logger.error("Error observing users", e)
emit(emptyList())
}
// Gut: Flow-Operatoren
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()) }
Abbruch und Bereinigung
// Gut: Abbruch respektieren
suspend fun processItems(items: List<Item>) {
items.forEach { item ->
ensureActive() // Abbruch vor teurer Arbeit prüfen
processItem(item)
}
}
// Gut: Bereinigung mit try/finally
suspend fun acquireAndProcess() {
val resource = acquireResource()
try {
resource.process()
} finally {
withContext(NonCancellable) {
resource.release() // Immer freigeben, auch bei Abbruch
}
}
}
Delegation
Eigenschaftsdelegation (Property Delegation)
// Faule Initialisierung
val expensiveData: List<User> by lazy {
userRepository.findAll()
}
// Beobachtbare Eigenschaft
var name: String by Delegates.observable("initial") { _, old, new ->
logger.info("Name changed from '$old' to '$new'")
}
// Kartenbasierte Eigenschaften
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))
Schnittstellendelegation (Interface Delegation)
// Gut: Schnittstellenimplementierung delegieren
class LoggingUserRepository(
private val delegate: UserRepository,
private val logger: Logger,
) : UserRepository by delegate {
// Nur überschreiben, was zum Hinzufügen von Logging benötigt wird
override suspend fun findById(id: String): User? {
logger.info("Finding user by id: $id")
return delegate.findById(id).also {
logger.info("Found user: ${it?.name ?: "null"}")
}
}
}
DSL-Builder
Typsichere Builder
// Gut: DSL mit @DslMarker
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML {
private val children = mutableListOf<Element>()
fun head(init: Head.() -> Unit) {
children += Head().apply(init)
}
fun body(init: Body.() -> Unit) {
children += Body().apply(init)
}
override fun toString(): String = children.joinToString("\n")
}
fun html(init: HTML.() -> Unit): HTML = HTML().apply(init)
// Verwendung
val page = html {
head { title("My Page") }
body {
h1("Welcome")
p("Hello, World!")
}
}
Konfigurations-DSL
data class ServerConfig(
val host: String = "0.0.0.0",
val port: Int = 8080,
val ssl: SslConfig? = null,
val database: DatabaseConfig? = null,
)
data class SslConfig(val certPath: String, val keyPath: String)
data class DatabaseConfig(val url: String, val maxPoolSize: Int = 10)
class ServerConfigBuilder {
var host: String = "0.0.0.0"
var port: Int = 8080
private var ssl: SslConfig? = null
private var database: DatabaseConfig? = null
fun ssl(certPath: String, keyPath: String) {
ssl = SslConfig(certPath, keyPath)
}
fun database(url: String, maxPoolSize: Int = 10) {
database = DatabaseConfig(url, maxPoolSize)
}
fun build(): ServerConfig = ServerConfig(host, port, ssl, database)
}
fun serverConfig(init: ServerConfigBuilder.() -> Unit): ServerConfig =
ServerConfigBuilder().apply(init).build()
// Verwendung
val config = serverConfig {
host = "0.0.0.0"
port = 443
ssl("/certs/cert.pem", "/certs/key.pem")
database("jdbc:postgresql://localhost:5432/mydb", maxPoolSize = 20)
}
Sequenzen für faule Auswertung
// Gut: Sequenzen für große Kollektionen mit mehreren Operationen verwenden
val result = users.asSequence()
.filter { it.isActive }
.map { it.email }
.filter { it.endsWith("@company.com") }
.take(10)
.toList()
// Gut: Unendliche Sequenzen erzeugen
val fibonacci: Sequence<Long> = sequence {
var a = 0L
var b = 1L
while (true) {
yield(a)
val next = a + b
a = b
b = next
}
}
val first20 = fibonacci.take(20).toList()
Gradle Kotlin DSL
build.gradle.kts Konfiguration
// Nach den neuesten Versionen suchen: https://kotlinlang.org/docs/releases.html
plugins {
kotlin("jvm") version "2.3.10"
kotlin("plugin.serialization") version "2.3.10"
id("io.ktor.plugin") version "3.4.0"
id("org.jetbrains.kotlinx.kover") version "0.9.7"
id("io.gitlab.arturbosch.detekt") version "1.23.8"
}
group = "com.example"
version = "1.0.0"
kotlin {
jvmToolchain(21)
}
dependencies {
// Ktor
implementation("io.ktor:ktor-server-core:3.4.0")
implementation("io.ktor:ktor-server-netty:3.4.0")
implementation("io.ktor:ktor-server-content-negotiation:3.4.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.0")
// Exposed
implementation("org.jetbrains.exposed:exposed-core:1."...)
}