val uri =URI("...")val res =loadResource(uri)val lines = res!!read()// bad!val lines = res?.read()?:throwIAE("$uri invalid")// more reasonable
Stick to nullable types only
public Optional<Goody>findGoodyForAmount(amount:Double)val goody =findGoodyForAmount(100)if(goody.isPresent()) goody.get()...else...// badval goody =findGoodyForAmount(100).orElse(null)if(goody !=null) goody ...else...// good uses null consistently
Use nullability where applicable but don't overuse it.
dataclassOrder(val id: Int?=null,val items: List<LineItem>?=null,val state: OrderState?=null,val goody: Goody?=null)// too much!dataclassOrder(val id: Int?=null,val items: List<LineItem>=emptyList()),val state: OrderState = UNPAID,val goody: Goody?=null)// some types made more sense as not-null values
Avoid using nullable types in Collections
val items: List<LineItem?>=emptyList()val items: List<LineItem>?=null,val items: Lilst<LineItem?>?=null// all terribadval items: List<LineItem>=emptyList()// that's what this is for
Use lateinit var for late initialization rather than nullability
Use value classes for domain specific types instead of common types.
value classEmail(val value: String)
value classPassword(val value: String)funlogin(email: Email, pwd: Password)// no performance impact! type erased in bytecode
// badval fis =FileInputStream("path")val text =try{val sb =StringBuilder()var line: String?while(fis.readLine().apply{line =this}!=null){
sb.append(line).append(System.lineSeparator())}
sb.toString()}finally{try{ fis.close()}catch(ex:Throwable){}}// good, via extension functionsval text =FileInputStream("path").use{ it.reader().readText()}
Extend third party classes
// badfuntoDateString(dr: LocalDateTime)= dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))// good, works with code completion!fun LocalDateTime.toDateString()=this.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
This is better because it results in more concise, deterministic, more easily testable and clearly scoped code that is easy to reason about compared to the imperative style.
if/else is an expression returning a result.
// imperative stylevar result: String
if(number %2==0)
result ="EVEN"else
result ="ODD"// expression style, betterval result =if(number %2==0)"EVEN"else"ODD"
when is an expression too, returning a result.
// imperative stylevar hi: String
when(lang){"NL"-> hi ="Goede dag""FR"-> hi ="Bonjour"else-> hi ="Good day"}// expression style, betterval hi =when(lang){"NL"->"Goede dag""FR"->"Bonjour"else->"Good day"}
try/catch also.
// imperative stylevar text: String
try{
text =File("path").readText()}catch(ex: Exception){
text =""}// expression style, betterval text =try{File("path").readText()}catch(ex: IOException){""}
Most functional collections return a result, so the return keyword is rarely needed!
You are actually programming at a higher-level of abstraction, since you're manipulating the collection directly instead of considering each of its elements. e.g. it's obvious in the second example that we're filtering, instead of needing to read the implementation to figure it out.
For readability, write multiple chained functions from top-down instead of left-right.
Use let/run to manipulate the context object and return a different type.
// old wayval file =File("/path")
file.setReadOnly(true)val created = file.createNewFile()// new wayval created =File("/path").run{setReadOnly(true)createNewFile()// last expression so result from this function is returned}
Use also to execute a side-effect.
// old wayif(amount <=0){val msg ="Payment amount is < 0"
LOGGER.warn(msg)throwIAE(msg)}else...// new wayrequire(amount >0){"Payment amount is < 0".also(LOGGER::warn)}
// No coroutines// Using Mono from Sprint React and many combinators (flatMap)// Standard constructs like if/else cannot be used// Business intent cannot be derived from this code@PutMapping("/users")@ResponseBodyfunupsertUser(@RequestBody user: User): Mono<User>=userByEmail(user.email).switchIfEmpty{verifyEmail(user.email).flatMap{ valid ->if(valid) Mono.just(user)else Mono.error(ResponseStatusException(BAD_REQUEST,"Bad Email"))}}.flatMap{ toUpsert ->save(toUpsert)}// Coroutines clean this up// Common language constructs can be used// Reads like synchronous code@PutMapping("/users")@ResponseBodyfunupsertUser(@RequestBody user: User): User =userByEmail(user.email).awaitSingle()?:if(verifyEmail(user.email)) user elsethrowResponseStatusException(BAD_REQUEST,"Bad Email")).let{
toUpsert ->save(toUpsert)}
Project Loom will (eventually) result in support for coroutines running on the JVM. This will greatly simplify running coroutines.