skip to content

An enum implements an interface and you must dispatch/lookup constants by a runtime value while keeping the interface clean. How do you design this idiomatically in Kotlin?

level: seniorimportance: nice to knowfreq 18%

answer

  1. Behavior in constants, lookup in companion
  2. entries.associateBy { } built once for O(1) lookup
  3. Return the interface type, not the enum
  4. Companion can itself implement a factory interface
  5. Avoid valueOf for non-name keys; return null or throw domain error

basics

~20 s

Keep the per-value behavior in each constant via the interface, and add a companion object with a lookup function (built from entries) to find the right constant from a runtime key. Callers use the interface, not the enum.

solid answer

~50 s

Put the polymorphic behavior in the interface, implemented per-constant. For reverse lookup by some runtime key (a code, char, etc.), add a **companion object** to the enum exposing a function like `fromCode(c: Int): Op?` backed by a precomputed `Map` built from `entries.associateBy { it.code }`. This keeps `O(1)` lookup and avoids scanning. The interface stays free of static concerns; the companion holds the registry. The companion itself can implement a *factory* interface if you want to pass the lookup around. Expose constants to callers as the **interface type** so consumers depend on behavior, not the concrete enum. For closed exhaustive handling prefer a `when (this)`; for open extension keep the per-constant override. Be careful: build the lookup map once (in the companion, lazily or at class init) rather than per call, and return a nullable or throw a domain exception for unknown keys instead of letting `valueOf` throw `IllegalArgumentException` on names.

code

kotlin · 10 lines
kotlin
interface Op { val symbol: Char; fun apply(a: Int, b: Int): Int }
enum class Arith(override val symbol: Char) : Op {
    PLUS('+')  { override fun apply(a: Int, b: Int) = a + b },
    MINUS('-') { override fun apply(a: Int, b: Int) = a - b };
    companion object {
        private val bySymbol = entries.associateBy { it.symbol }
        fun fromSymbol(c: Char): Op? = bySymbol[c]
    }
}
// Arith.fromSymbol('+')?.apply(2,3) -> 5

go deeper

for a junior

Can call a constant's interface method but typically scans or hardcodes lookups.

for a middle

Adds a companion with an associateBy-backed map and returns nullable results.

for a senior

Returns the interface type, builds the map once, and may have the companion implement a factory interface for decoupling.

for a principal

Designs the boundary so consumers never see the enum, choosing lookup/exhaustiveness strategy and error semantics deliberately.

## The pattern: behavior in constants, lookup in the companion You want (a) each constant to carry behavior via the interface and (b) to resolve a constant from a runtime value efficiently — without polluting the interface with static lookup methods. ```kotlin interface Op { val symbol: Char; fun apply(a: Int, b: Int): Int } enum class Arith(override val symbol: Char) : Op { PLUS('+') { override fun apply(a: Int, b: Int) = a + b }, MINUS('-') { override fun apply(a: Int, b: Int) = a - b }; companion object { private val bySymbol = entries.associateBy { it.symbol } // built once fun fromSymbol(c: Char): Op? = bySymbol[c] // O(1), interface-typed } } ``` - Behavior (`apply`) lives in each constant; callers use it via `Op`. - The **companion object** holds the `bySymbol` registry built from `entries.associateBy { ... }`, so lookup is `O(1)` and the map is created **once** at class initialization, not per call. - `fromSymbol` returns the **interface type** (`Op?`), so consumers depend on behavior, not on `Arith`. ## Why a companion, not the interface Lookup is a *static factory* concern, not per-instance behavior, so it doesn't belong on the interface. A companion object is Kotlin's place for 'static' members. If you need to pass the factory around, the **companion itself can implement an interface** (e.g. `interface OpLookup { fun fromSymbol(c: Char): Op? }`) and be referenced as `Arith.Companion` / via the enum name. ## Lookup safety - Prefer a prebuilt `Map` over scanning `entries.firstOrNull { ... }` on every call. - Return nullable (`Op?`) or throw a **domain** exception for unknown keys, rather than relying on `valueOf(name)`, which throws `IllegalArgumentException` and only matches by constant *name*. - For a fixed/closed set you may instead use `when (this)`; it stays exhaustive and needs no `else`. ## Decoupling consumers Expose the constants and lookups as the interface type so the rest of the codebase doesn't import the enum. This is the same dependency-inversion benefit as any interface boundary, applied to a fixed value set. You retain enum perks — `entries`, `EnumMap`/`EnumSet`, exhaustive `when`, singleton identity — while presenting a clean abstraction.

  • Why prebuild the lookup map instead of scanning entries each call?
    associateBy builds the map once at class init for O(1) lookups; scanning entries on every call is O(n) and repeats work needlessly.
  • Can the companion object itself implement an interface?
    Yes — a companion object is a real object and can implement interfaces, letting you pass the enum's factory/lookup around as that interface type.

The constants are specialized workers; the companion object is the front desk with a phone directory that instantly finds the right worker by extension.

saying these in an interview costs you the question

  • Putting static lookup methods on the per-instance interface
  • Scanning entries.firstOrNull on every lookup
  • Using valueOf for arbitrary keys (it only matches constant names and throws)
  • Returning the concrete enum type and leaking it to all callers
  • Rebuilding the lookup map on each call

context