Kotlin-Entwicklungsmuster
Idiomatische Kotlin-Muster und Best Practices für den Bau robuster, effizienter und wartbarer Anwendungen.
Wann zu verwenden
- Schreiben von neuem Kotlin-Code
- Überprüfen von Kotlin-Code
- Refactoring von vorhandenem Kotlin-Code
- Entwerfen von Kotlin-Modulen oder -Bibliotheken
- Konfigurieren von Gradle-Kotlin-DSL-Builds
So funktioniert es
Dieses Skill setzt idiomatische Kotlin-Konventionen in sieben Schlüsselbereichen durch: Null-Sicherheit unter Verwendung des Typsystems und sicherer Aufrufoperatoren, Unveränderlichkeit mittels val und copy() bei Datenklassen, versiegelte Klassen und Schnittstellen für erschöpfende Typhierarchien, strukturierte Nebenläufigkeit mit Coroutinen und Flow, Erweiterungsfunktionen zum Hinzufügen von Verhalten ohne Vererbung, typsichere DSL-Builder unter Verwendung von @DslMarker und Lambda-Empfängern sowie Gradle Kotlin DSL für die Build-Konfiguration.
Beispiele
Null-Sicherheit 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. Null-Sicherheit
Das Typsystem von Kotlin unterscheidet zwischen nullable- und non-nullable-Typen. Nutzen Sie es voll aus.
// Gut: Standardmäßig non-nullable-Typen verwenden
fun getUser(id: String): User {
return userRepository.findById(id)
?: throw UserNotFoundException("Benutzer $id nicht gefunden")
}
// Gut: Sichere Aufrufe und Elvis-Operator
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user?.email ?: "[email protected]"
}
// Schlecht: Forciertes Entpacken von nullable-Typen
fun getUserEmail(userId: String): String {
val user = userRepository.findById(userId)
return user!!.email // Wirft NPE wenn null
}
2. Unveränderlichkeit als Standard
Bevorzugen Sie val gegenüber var, unveränderliche Sammlungen gegenüber veränderlichen.
// Gut: Unveränderliche Daten
data class User(
val id: String,
val name: String,
val email: String,
)
// Gut: Transformieren mit copy()
fun updateEmail(user: User, newEmail: String): User =
user.copy(email = newEmail)
// Gut: Unveränderliche Sammlungen
val users: List<User> = listOf(user1, user2)
val filtered = users.filter { it.email.isNotBlank() }
// Schlecht: Veränderlicher Zustand
var currentUser: User? = null // Veränderlichen globalen Zustand vermeiden
val mutableUsers = mutableListOf<User>() // Nur wenn wirklich nötig verwenden
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 -> "Nicht gefunden"
500 -> "Interner Serverfehler"
else -> "Unbekannter 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 (keine Laufzeit-Overhead)
@JvmInline
value class UserId(val value: String) {
init {
require(value.isNotBlank()) { "UserId darf nicht leer sein" }
}
}
@JvmInline
value class Email(val value: String) {
init {
require('@' in value) { "Ungültige E-Mail: $value" }
}
}
fun getUser(id: UserId): User = userRepository.findById(id)
Versiegelte Klassen und Schnittstellen
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("Wird noch geladen")
}
Versiegelte Schnittstellen 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
Wann welche verwenden
// let: Nullable oder Bereichsergebnis 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("Benutzer erstellt: ${it.id}") }
// run: Einen Block mit Empfänger ausführen (gibt Ergebnis zurück)
val result = connection.run {
prepareStatement(sql)
executeQuery()
}
// with: Nicht-Extension-Form 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: Sammlungserweiterungen
fun <T> List<T>.second(): T = this[1]
fun <T> List<T>.secondOrNull(): T? = getOrNull(1)
// Gut: Bereichsbegrenzte 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("Fehler beim Beobachten von Benutzern", 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 beachten
suspend fun processItems(items: List<Item>) {
items.forEach { item ->
ensureActive() // Vor teurer Arbeit Abbruch 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
// Faule Initialisierung
val expensiveData: List<User> by lazy {
userRepository.findAll()
}
// Beobachtbare Eigenschaft
var name: String by Delegates.observable("initial") { _, old, new ->
logger.info("Name geändert von '$old' zu '$new'")
}
// Map-gestützte 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
// Gut: Schnittstellenimplementierung delegieren
class LoggingUserRepository(
private val delegate: UserRepository,
private val logger: Logger,
) : UserRepository by delegate {
// Nur das überschreiben, was zum Hinzufügen von Logging benötigt wird
override suspend fun findById(id: String): User? {
logger.info("Suche Benutzer nach ID: $id")
return delegate.findById(id).also {
logger.info("Benutzer gefunden: ${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("Meine Seite") }
body {
h1("Willkommen")
p("Hallo, Welt!")
}
}
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 Sammlungen 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
// Auf neueste Versionen prüfen: 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.