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?
answer
- Behavior in constants, lookup in companion
- entries.associateBy { } built once for O(1) lookup
- Return the interface type, not the enum
- Companion can itself implement a factory interface
- Avoid valueOf for non-name keys; return null or throw domain error
basics
~20 sKeep 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 sPut 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 linesinterface 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) -> 5go deeper
Can call a constant's interface method but typically scans or hardcodes lookups.
Adds a companion with an associateBy-backed map and returns nullable results.
Returns the interface type, builds the map once, and may have the companion implement a factory interface for decoupling.
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