Skip to main content

kotlin-patterns

Patrones idiomáticos de Kotlin, buenas prácticas y convenciones para construir aplicaciones Kotlin robustas, eficientes y mantenibles con coroutines, null safety y builders de DSL.

소스 정보

저장소
affaan-m/ECC
최근 소스 활동
2026년 6월 7일 05:26
감지된 SKILL.md 언어
스페인어
스타
276,549
포크
41,243

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
kotlin-patterns
description
Patrones idiomáticos de Kotlin, buenas prácticas y convenciones para construir aplicaciones Kotlin robustas, eficientes y mantenibles con coroutines, null safety y builders de DSL.
origin
ECC
# Patrones de Desarrollo Kotlin Patrones idiomáticos de Kotlin y buenas prácticas para construir aplicaciones robustas, eficientes y mantenibles. ## Cuándo Usar - Escribir nuevo código Kotlin - Revisar código Kotlin - Refactorizar código Kotlin existente - Diseñar módulos o librerías Kotlin - Configurar builds con Gradle Kotlin DSL ## Cómo Funciona Este skill aplica convenciones idiomáticas de Kotlin en siete áreas clave: null safety usando el sistema de tipos y operadores de llamada segura, inmutabilidad mediante `val` y `copy()` en data classes, clases selladas e interfaces para jerarquías de tipos exhaustivas, concurrencia estructurada con coroutines y `Flow`, funciones de extensión para agregar comportamiento sin herencia, builders de DSL type-safe usando `@DslMarker` y receptores lambda, y Gradle Kotlin DSL para configuración de build. ## Ejemplos **Null safety con el operador Elvis:** ```kotlin fun getUserEmail(userId: String): String { val user = userRepository.findById(userId) return user?.email ?: "unknown@example.com" } ``` **Sealed class para resultados exhaustivos:** ```kotlin 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>() } ``` **Concurrencia estructurada con async/await:** ```kotlin 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()) } ``` ## Principios Fundamentales ### 1. Null Safety El sistema de tipos de Kotlin distingue tipos nullable de no-nullable. Aprovecharlo al máximo. ```kotlin // Bien: Usar tipos no-nullable por defecto fun getUser(id: String): User { return userRepository.findById(id) ?: throw UserNotFoundException("User $id not found") } // Bien: Llamadas seguras y operador Elvis fun getUserEmail(userId: String): String { val user = userRepository.findById(userId) return user?.email ?: "unknown@example.com" } // Mal: Desempaquetar forzadamente tipos nullable fun getUserEmail(userId: String): String { val user = userRepository.findById(userId) return user!!.email // Lanza NPE si es null } ``` ### 2. Inmutabilidad por Defecto Preferir `val` sobre `var`, colecciones inmutables sobre mutables. ```kotlin // Bien: Datos inmutables data class User( val id: String, val name: String, val email: String, ) // Bien: Transformar con copy() fun updateEmail(user: User, newEmail: String): User = user.copy(email = newEmail) // Bien: Colecciones inmutables val users: List<User> = listOf(user1, user2) val filtered = users.filter { it.email.isNotBlank() } // Mal: Estado mutable var currentUser: User? = null // Evitar estado global mutable val mutableUsers = mutableListOf<User>() // Evitar a menos que sea realmente necesario ``` ### 3. Cuerpos de Expresión y Funciones de Una Sola Expresión Usar cuerpos de expresión para funciones concisas y legibles. ```kotlin // Bien: Cuerpo de expresión 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('@') } // Bien: When como expresión fun statusMessage(code: Int): String = when (code) { 200 -> "OK" 404 -> "Not Found" 500 -> "Internal Server Error" else -> "Unknown status: $code" } // Mal: Cuerpo de bloque innecesario fun isAdult(age: Int): Boolean { return age >= 18 } ``` ### 4. Data Classes para Objetos de Valor Usar data classes para tipos que principalmente contienen datos. ```kotlin // Bien: Data class con copy, equals, hashCode, toString data class CreateUserRequest( val name: String, val email: String, val role: Role = Role.USER, ) // Bien: Value class para type safety (cero overhead en tiempo de ejecución) @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) ``` ## Clases Selladas e Interfaces ### Modelar Jerarquías Restringidas ```kotlin // Bien: Sealed class para when exhaustivo 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") } ``` ### Sealed Interfaces para Respuestas de API ```kotlin 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 } ``` ## Funciones de Scope ### Cuándo Usar Cada Una ```kotlin // let: Transformar resultado nullable o delimitado val length: Int? = name?.let { it.trim().length } // apply: Configurar un objeto (retorna el objeto) val user = User().apply { name = "Alice" email = "alice@example.com" } // also: Efectos secundarios (retorna el objeto) val user = createUser(request).also { logger.info("Created user: ${it.id}") } // run: Ejecutar un bloque con receptor (retorna resultado) val result = connection.run { prepareStatement(sql) executeQuery() } // with: Forma no-extensión de run val csv = with(StringBuilder()) { appendLine("name,email") users.forEach { appendLine("${it.name},${it.email}") } toString() } ``` ### Anti-Patrones ```kotlin // Mal: Anidar funciones de scope user?.let { u -> u.address?.let { addr -> addr.city?.let { city -> println(city) // Difícil de leer } } } // Bien: Encadenar llamadas seguras en su lugar val city = user?.address?.city city?.let { println(it) } ``` ## Funciones de Extensión ### Agregar Funcionalidad Sin Herencia ```kotlin // Bien: Extensiones específicas del dominio 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() // Bien: Extensiones de colecciones fun <T> List<T>.second(): T = this[1] fun <T> List<T>.secondOrNull(): T? = getOrNull(1) // Bien: Extensiones delimitadas (sin contaminar el namespace global) 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() } } ``` ## Coroutines ### Concurrencia Estructurada ```kotlin // Bien: Concurrencia estructurada con 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(), ) } // Bien: supervisorScope cuando los hijos pueden fallar independientemente 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 para Streams Reactivos ```kotlin // Bien: Flow frío con manejo de errores adecuado 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()) } // Bien: Operadores de 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()) } ``` ### Cancelación y Limpieza ```kotlin // Bien: Respetar la cancelación suspend fun processItems(items: List<Item>) { items.forEach { item -> ensureActive() // Verificar cancelación antes del trabajo costoso processItem(item) } } // Bien: Limpieza con try/finally suspend fun acquireAndProcess() { val resource = acquireResource() try { resource.process() } finally { withContext(NonCancellable) { resource.release() // Siempre liberar, incluso al cancelar } } } ``` ## Delegación ### Delegación de Propiedades ```kotlin // Inicialización diferida val expensiveData: List<User> by lazy { userRepository.findAll() } // Propiedad observable var name: String by Delegates.observable("initial") { _, old, new -> logger.info("Name changed from '$old' to '$new'") } // Propiedades respaldadas por 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)) ``` ### Delegación de Interfaces ```kotlin // Bien: Delegar implementación de interfaz class LoggingUserRepository( private val delegate: UserRepository, private val logger: Logger, ) : UserRepository by delegate { // Solo sobreescribir lo que necesitas agregar logging 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"}") } } } ``` ## Builders de DSL ### Builders Type-Safe ```kotlin // Bien: DSL con @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) // Uso val page = html { head { title("My Page") } body { h1("Welcome") p("Hello, World!") } } ``` ### DSL de Configuración ```kotlin 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) }
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기