kotlin-patterns

安装量: 35
排名: #19762

安装

npx skills add https://github.com/affaan-m/everything-claude-code --skill kotlin-patterns
Kotlin Development Patterns
Idiomatic Kotlin patterns and best practices for building robust, efficient, and maintainable applications.
When to Use
Writing new Kotlin code
Reviewing Kotlin code
Refactoring existing Kotlin code
Designing Kotlin modules or libraries
Configuring Gradle Kotlin DSL builds
How It Works
This skill enforces idiomatic Kotlin conventions across seven key areas: null safety using the type system and safe-call operators, immutability via
val
and
copy()
on data classes, sealed classes and interfaces for exhaustive type hierarchies, structured concurrency with coroutines and
Flow
, extension functions for adding behaviour without inheritance, type-safe DSL builders using
@DslMarker
and lambda receivers, and Gradle Kotlin DSL for build configuration.
Examples
Null safety with Elvis operator:
fun
getUserEmail
(
userId
:
String
)
:
String
{
val
user
=
userRepository
.
findById
(
userId
)
return
user
?
.
email
?:
"unknown@example.com"
}
Sealed class for exhaustive results:
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
>
(
)
}
Structured concurrency with 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
(
)
)
}
Core Principles
1. Null Safety
Kotlin's type system distinguishes nullable and non-nullable types. Leverage it fully.
// Good: Use non-nullable types by default
fun
getUser
(
id
:
String
)
:
User
{
return
userRepository
.
findById
(
id
)
?:
throw
UserNotFoundException
(
"User
$
id
not found"
)
}
// Good: Safe calls and Elvis operator
fun
getUserEmail
(
userId
:
String
)
:
String
{
val
user
=
userRepository
.
findById
(
userId
)
return
user
?
.
email
?:
"unknown@example.com"
}
// Bad: Force-unwrapping nullable types
fun
getUserEmail
(
userId
:
String
)
:
String
{
val
user
=
userRepository
.
findById
(
userId
)
return
user
!!
.
email
// Throws NPE if null
}
2. Immutability by Default
Prefer
val
over
var
, immutable collections over mutable ones.
// Good: Immutable data
data
class
User
(
val
id
:
String
,
val
name
:
String
,
val
email
:
String
,
)
// Good: Transform with copy()
fun
updateEmail
(
user
:
User
,
newEmail
:
String
)
:
User
=
user
.
copy
(
email
=
newEmail
)
// Good: Immutable collections
val
users
:
List
<
User
>
=
listOf
(
user1
,
user2
)
val
filtered
=
users
.
filter
{
it
.
email
.
isNotBlank
(
)
}
// Bad: Mutable state
var
currentUser
:
User
?
=
null
// Avoid mutable global state
val
mutableUsers
=
mutableListOf
<
User
>
(
)
// Avoid unless truly needed
3. Expression Bodies and Single-Expression Functions
Use expression bodies for concise, readable functions.
// Good: Expression body
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
(
'@'
)
}
// Good: When as expression
fun
statusMessage
(
code
:
Int
)
:
String
=
when
(
code
)
{
200
->
"OK"
404
->
"Not Found"
500
->
"Internal Server Error"
else
->
"Unknown status:
$
code
"
}
// Bad: Unnecessary block body
fun
isAdult
(
age
:
Int
)
:
Boolean
{
return
age
>=
18
}
4. Data Classes for Value Objects
Use data classes for types that primarily hold data.
// Good: Data class with copy, equals, hashCode, toString
data
class
CreateUserRequest
(
val
name
:
String
,
val
email
:
String
,
val
role
:
Role
=
Role
.
USER
,
)
// Good: Value class for type safety (zero overhead at runtime)
@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
)
Sealed Classes and Interfaces
Modeling Restricted Hierarchies
// Good: Sealed class for exhaustive 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"
)
}
Sealed Interfaces for API Responses
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
}
Scope Functions
When to Use Each
// let: Transform nullable or scoped result
val
length
:
Int
?
=
name
?
.
let
{
it
.
trim
(
)
.
length
}
// apply: Configure an object (returns the object)
val
user
=
User
(
)
.
apply
{
name
=
"Alice"
email
=
"alice@example.com"
}
// also: Side effects (returns the object)
val
user
=
createUser
(
request
)
.
also
{
logger
.
info
(
"Created user:
${
it
.
id
}
"
)
}
// run: Execute a block with receiver (returns result)
val
result
=
connection
.
run
{
prepareStatement
(
sql
)
executeQuery
(
)
}
// with: Non-extension form of run
val
csv
=
with
(
StringBuilder
(
)
)
{
appendLine
(
"name,email"
)
users
.
forEach
{
appendLine
(
"
${
it
.
name
}
,
${
it
.
email
}
"
)
}
toString
(
)
}
Anti-Patterns
// Bad: Nesting scope functions
user
?
.
let
{
u
->
u
.
address
?
.
let
{
addr
->
addr
.
city
?
.
let
{
city
->
println
(
city
)
// Hard to read
}
}
}
// Good: Chain safe calls instead
val
city
=
user
?
.
address
?
.
city
city
?
.
let
{
println
(
it
)
}
Extension Functions
Adding Functionality Without Inheritance
// Good: Domain-specific extensions
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
(
)
// Good: Collection extensions
fun
<
T
>
List
<
T
>
.
second
(
)
:
T
=
this
[
1
]
fun
<
T
>
List
<
T
>
.
secondOrNull
(
)
:
T
?
=
getOrNull
(
1
)
// Good: Scoped extensions (not polluting global namespace)
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
Structured Concurrency
// Good: Structured concurrency with 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
(
)
,
)
}
// Good: supervisorScope when children can fail independently
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 for Reactive Streams
// Good: Cold flow with proper error handling
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
(
)
)
}
// Good: Flow operators
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
(
)
)
}
Cancellation and Cleanup
// Good: Respect cancellation
suspend
fun
processItems
(
items
:
List
<
Item
>
)
{
items
.
forEach
{
item
->
ensureActive
(
)
// Check cancellation before expensive work
processItem
(
item
)
}
}
// Good: Cleanup with try/finally
suspend
fun
acquireAndProcess
(
)
{
val
resource
=
acquireResource
(
)
try
{
resource
.
process
(
)
}
finally
{
withContext
(
NonCancellable
)
{
resource
.
release
(
)
// Always release, even on cancellation
}
}
}
Delegation
Property Delegation
// Lazy initialization
val
expensiveData
:
List
<
User
>
by
lazy
{
userRepository
.
findAll
(
)
}
// Observable property
var
name
:
String
by
Delegates
.
observable
(
"initial"
)
{
_
,
old
,
new
->
logger
.
info
(
"Name changed from '
$
old
' to '
$
new
'"
)
}
// Map-backed properties
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
)
)
Interface Delegation
// Good: Delegate interface implementation
class
LoggingUserRepository
(
private
val
delegate
:
UserRepository
,
private
val
logger
:
Logger
,
)
:
UserRepository
by
delegate
{
// Only override what you need to add logging to
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 Builders
Type-Safe Builders
// Good: DSL with @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
)
// Usage
val
page
=
html
{
head
{
title
(
"My Page"
)
}
body
{
h1
(
"Welcome"
)
p
(
"Hello, World!"
)
}
}
Configuration 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
(
)
// Usage
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
)
}
Sequences for Lazy Evaluation
// Good: Use sequences for large collections with multiple operations
val
result
=
users
.
asSequence
(
)
.
filter
{
it
.
isActive
}
.
map
{
it
.
email
}
.
filter
{
it
.
endsWith
(
"@company.com"
)
}
.
take
(
10
)
.
toList
(
)
// Good: Generate infinite sequences
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 Configuration
// Check for latest versions: 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.0.0"
)
implementation
(
"org.jetbrains.exposed:exposed-dao:1.0.0"
)
implementation
(
"org.jetbrains.exposed:exposed-jdbc:1.0.0"
)
implementation
(
"org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0"
)
// Koin
implementation
(
"io.insert-koin:koin-ktor:4.2.0"
)
// Coroutines
implementation
(
"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"
)
// Testing
testImplementation
(
"io.kotest:kotest-runner-junit5:6.1.4"
)
testImplementation
(
"io.kotest:kotest-assertions-core:6.1.4"
)
testImplementation
(
"io.kotest:kotest-property:6.1.4"
)
testImplementation
(
"io.mockk:mockk:1.14.9"
)
testImplementation
(
"io.ktor:ktor-server-test-host:3.4.0"
)
testImplementation
(
"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2"
)
}
tasks
.
withType
<
Test
>
{
useJUnitPlatform
(
)
}
detekt
{
config
.
setFrom
(
files
(
"config/detekt/detekt.yml"
)
)
buildUponDefaultConfig
=
true
}
Error Handling Patterns
Result Type for Domain Operations
// Good: Use Kotlin's Result or a custom sealed class
suspend
fun
createUser
(
request
:
CreateUserRequest
)
:
Result
<
User
>
=
runCatching
{
require
(
request
.
name
.
isNotBlank
(
)
)
{
"Name cannot be blank"
}
require
(
'@'
in
request
.
email
)
{
"Invalid email format"
}
val
user
=
User
(
id
=
UserId
(
UUID
.
randomUUID
(
)
.
toString
(
)
)
,
name
=
request
.
name
,
email
=
Email
(
request
.
email
)
,
)
userRepository
.
save
(
user
)
user
}
// Good: Chain results
val
displayName
=
createUser
(
request
)
.
map
{
it
.
name
}
.
getOrElse
{
"Unknown"
}
require, check, error
// Good: Preconditions with clear messages
fun
withdraw
(
account
:
Account
,
amount
:
Money
)
:
Account
{
require
(
amount
.
value
>
0
)
{
"Amount must be positive:
$
amount
"
}
check
(
account
.
balance
>=
amount
)
{
"Insufficient balance:
${
account
.
balance
}
<
$
amount
"
}
return
account
.
copy
(
balance
=
account
.
balance
-
amount
)
}
Collection Operations
Idiomatic Collection Processing
// Good: Chained operations
val
activeAdminEmails
:
List
<
String
>
=
users
.
filter
{
it
.
role
==
Role
.
ADMIN
&&
it
.
isActive
}
.
sortedBy
{
it
.
name
}
.
map
{
it
.
email
}
// Good: Grouping and aggregation
val
usersByRole
:
Map
<
Role
,
List
<
User
>
>
=
users
.
groupBy
{
it
.
role
}
val
oldestByRole
:
Map
<
Role
,
User
?
>
=
users
.
groupBy
{
it
.
role
}
.
mapValues
{
(
_
,
users
)
->
users
.
minByOrNull
{
it
.
createdAt
}
}
// Good: Associate for map creation
val
usersById
:
Map
<
UserId
,
User
>
=
users
.
associateBy
{
it
.
id
}
// Good: Partition for splitting
val
(
active
,
inactive
)
=
users
.
partition
{
it
.
isActive
}
Quick Reference: Kotlin Idioms
Idiom
Description
val
over
var
Prefer immutable variables
data class
For value objects with equals/hashCode/copy
sealed class/interface
For restricted type hierarchies
value class
For type-safe wrappers with zero overhead
Expression
when
Exhaustive pattern matching
Safe call
?.
Null-safe member access
Elvis
?:
Default value for nullables
let
/
apply
/
also
/
run
/
with
Scope functions for clean code
Extension functions
Add behavior without inheritance
copy()
Immutable updates on data classes
require
/
check
Precondition assertions
Coroutine
async
/
await
Structured concurrent execution
Flow
Cold reactive streams
sequence
Lazy evaluation
Delegation
by
Reuse implementation without inheritance
Anti-Patterns to Avoid
// Bad: Force-unwrapping nullable types
val
name
=
user
!!
.
name
// Bad: Platform type leakage from Java
fun
getLength
(
s
:
String
)
=
s
.
length
// Safe
fun
getLength
(
s
:
String
?
)
=
s
?
.
length
?:
0
// Handle nulls from Java
// Bad: Mutable data classes
data
class
MutableUser
(
var
name
:
String
,
var
email
:
String
)
// Bad: Using exceptions for control flow
try
{
val
user
=
findUser
(
id
)
}
catch
(
e
:
NotFoundException
)
{
// Don't use exceptions for expected cases
}
// Good: Use nullable return or Result
val
user
:
User
?
=
findUserOrNull
(
id
)
// Bad: Ignoring coroutine scope
GlobalScope
.
launch
{
/ Avoid GlobalScope /
}
// Good: Use structured concurrency
coroutineScope
{
launch
{
/ Properly scoped /
}
}
// Bad: Deeply nested scope functions
user
?
.
let
{
u
->
u
.
address
?
.
let
{
a
->
a
.
city
?
.
let
{
c
->
process
(
c
)
}
}
}
// Good: Direct null-safe chain
user
?
.
address
?
.
city
?
.
let
{
process
(
it
)
}
Remember
Kotlin code should be concise but readable. Leverage the type system for safety, prefer immutability, and use coroutines for concurrency. When in doubt, let the compiler help you.
返回排行榜