Give shared behavior a clear owner
An object declaration defines a singleton. A companion object associates factories or utilities with a class name. Neither makes mutable state automatically safe for concurrent access.
class LessonId private constructor(val value: String) {
companion object {
fun parse(raw: String): LessonId? =
raw.trim().takeIf { it.isNotEmpty() }?.let { LessonId(it) }
}
}
object Labels {
const val DEFAULT_TOPIC = "Kotlin"
}
LessonId.parse is a named creation policy that can reject bad input without exposing a public constructor. On JVM, a companion is an object; Java-facing static methods may require interoperability annotations such as @JvmStatic when that calling convention is desired.
Global objects holding mutable caches create hidden dependencies between tests and features. Prefer injecting stateful services where ownership and lifetime matter.
Exercise
Add a factory that validates a restricted ID character set. Test whitespace, empty values, and punctuation. Keep shared constants separate from mutable session data.
Check: the factory should enforce the same rules regardless of which caller invokes it.