Keep things simple

The Power of Simplicity

Measuring complexity

LoC ("lines of code") are not the number of lines in your Kotlin module, or the number of lines in a file — even though some automatic software tool might try to tell you so.

When counting lines of code in the scope of measuring how complex a certain function, apply these principles:

Complexity is measured in lines of code that require mental processing.

Count a line if it adds cognitive load (something a developer has to think about when reading the code), for example:

  • Actual executable statements
  • Logic that influences flow (e.g., conditions, loops, function calls)

Ignore these when counting complexity LOC:

  • Empty lines: Used for readability, not logic.
  • Comments: don’t add execution complexity.
  • Closing brackets when they don’t add complexity
  • Opening Braces if used for Structure: If a function or class declaration has { on a new line, it’s just formatting.
  • Import Statements: They are necessary but don’t require processing once written by a developer reading code.
  • Package Declarations: Present in every file — don’t contribute to logic.
  • Annotations: Metadata for compiler/frameworks, but don’t affect code logic and thus complexity directly.
  • Logging Statements (log.debug(...)): Logging is important but doesn’t contribute to program logic.
  • Redundant statements (which should be removed at the next possibility either way), e.g.:
    • Explicit return Statements in Single-Expression Functions: If a function can be expressed as a single expression, the explicit return is redundant (and thus does not directly impact code complexity).

For example:

// 13 physical lines
package my.project

import java.util.*

val x = y.f()

// Check if x abc
if (x.abc) {
    return false
}

// continue if otherwise
doThing(x)
// meaningful LOC: 3
val x = y.f()
if (x.abc) return false
doThing(x)

Examples of common pitfalls

Keep it simple and cognitive load minimal

// Simple ✅
fun isAdult(age: Int) = age >= 18
// Complex ❌
class AgeValidator {
    companion object {
        private const val ADULT_AGE = 18
        fun validateAdultAge(ageToValidate: Int): Boolean {
            return performAgeCheck(ageToValidate)
        }
        private fun performAgeCheck(age: Int) = age >= ADULT_AGE
    }
}

Over-engineering (most common pitfall possibly?)

// Simple ✅
fun getActiveUsers() = users.filter { it.isActive }
// Over-engineered ❌
class UserFilterStrategy {
    interface FilterCriteria {
        fun matches(user: User): Boolean
    }
    
    class ActiveUserFilter : FilterCriteria {
        override fun matches(user: User) = user.isActive
    }
    
    fun filterUsers(users: List<User>, criteria: FilterCriteria) =
        users.filter { criteria.matches(it) }
}

Premature optimization (that's usually not actually faster)

// Simple ✅
val userNames = users.map { it.name }
// Premature optimization ❌
val userNames = ArrayList<String>(users.size).apply {
    users.forEach { user ->
        add(user.name)
    }
}