So I essentially need an enum for 4 states. Let's ...
# getting-started
c
So I essentially need an enum for 4 states. Let's say A, B, C, D. they all need a foo property... except for D. In this sort of situation I guess my only sensible option at that point is to create a sealed class sorta like this?
Copy code
sealed class State{
  sealed class WithFoo(foo: String): State {
    sealed class A(): WithFoo(foo = "Aaa")
    sealed class B(): WithFoo(foo = "Bbb")
    sealed class C(): WithFoo(foo = "Ccc")
  }
  sealed class D(): State()
}
p
You can use State directly and move
foo: String
to the subclasses constructors. At the end you will apply class pattern matching in the
when
clause and filter the actual subclass back. Unless you really want to have this base property but then you will have to filter out for D subclass. Or perhaps use a default
foo = null
for D
c
To answer the specific question, I think @Pablichjenkov’s is probably the best. You probably won’t need to directly consider any intermediate classes between
State
and its subclasses, but you’d likely only work with them all in a big
when
block, so don’t try to over-abstract this. The simpler solution will work just fine. But this does beg the question of what exactly you’re trying to model? Without a more concrete example it’s difficult to know whether a sealed class is even appropriate, and whether your situation really benefits from it if there is some state that’s shared among the different states. I’ve got a section in the Ballast state-management library documentation arguing that sealed classes are often touted as a great solution in theory, but in practice can become more cumbersome than they’re worth. A
data class
is often a better choice for modeling the kind of real-world, non-trivial states you’re likely working with.
👆 1
More concretely, as a sealed class, this is better:
Copy code
sealed class State {
    class A(public val foo: String = "Aaa") : State()
    class B(public val foo: String = "Bbb") : State()
    class C(public val foo: String = "Ccc") : State()
    object D : State()
}
because you’re mostly likely going to be using it like this (otherwise, you don’t need the class to be sealed)
Copy code
fun processState(state: State): String {
    return when (state) {
        is State.A -> processWithFoo(state.foo)
        is State.B -> processWithFoo(state.foo)
        is State.C -> processWithFoo(state.foo)
        is State.D -> processWithoutFoo()
    }
}
However, if you’re not processing the class in such a discrete way, and you do want a more shared approach to the
foo
value, you might be better off moving the “type” to an enum as a property of the main state holder data class, like this:
Copy code
enum class StateType { 
    A, B, C, D
}

data class State(
    public val stateType: StateType,
    public val foo: String?,
)
👌🏽 1
c
Thanks for all the info everyone. Concretely I'm actually trying to model my server environments. So basically I have Prod Staging Dev Custom all of them have "urls", but technically the custom URL is dynamic and is loaded from storage and so it doesn't really exist. What I would like to do is
serverEnvironment.url
but I keep having an if statement where
if (ServerType.CUSTOM) getCustomUrl else serverEnvironment.url
and sometimes I forget to write the if statement, so I crash when I'm pointed to a custom url because its just an empty string for the time being. lol but I guess I might make ServerType.Custom's url just be null, and make ServerTypes url property nullable so that it forces me to handle it. But yeah. Trying to think if there's a better way
c
From that description, a sealed class probably is still appropriate, but rather than setting a property of the environment, give the base class an abstract function which returns the URL. Each environment subclass can then override it however is necessary, whether returning a fixed or dynamic value. The rest of the application shouldn’t have to know which environment it is in, just that it needs some value which changes per environment. But it doesn’t care how the value gets loaded either, so this would be a case where a simple
interface
or
abstract class
would suffice, and being sealed isn’t strictly necessary. It does give you some peace of mind knowing that, in a pinch, you can drop down to checking the exact sub-types if you need.
Copy code
// if the custom URL lookup is synchronous/blocking, 
// providing a custom `get()` function works just fine
sealed interface Environment {
    abstract val baseUrl: String

    object Prod : Environment {
        override val baseUrl: String = "<https://prod.example.com>"
    }
    object Staging : Environment {
        override val baseUrl: String = "<https://staging.example.com>"
    }
    object Dev : Environment {
        override val baseUrl: String = "<https://dev.example.com>"
    }
    class Custom(private val extraParameter: String) : Environment {
        override val baseUrl: String
            get() {
                return blockingFetchBaseUrl(extraParameter)
            }
    }
}
Copy code
// if the custom URL comes from making an async 
// network/DB call (or something like that), it's a 
// bit more boilerplate, but still not too bad.
sealed interface Environment {
    abstract suspend fun getBaseUrl(): String

    object Prod : Environment {
        override suspend fun getBaseUrl(): String = "<https://prod.example.com>"
    }
    object Staging : Environment {
        override suspend fun getBaseUrl(): String = "<https://staging.example.com>"
    }
    object Dev : Environment {
        override suspend fun getBaseUrl(): String = "<https://dev.example.com>"
    }
    class Custom(private val extraParameter: String) : Environment {
        override suspend fun getBaseUrl(): String {
            return suspendingFetchBaseUrl(extraParameter)
        }
    }
}
1
c
oooooh. thats a good idea. Let me try that this week...
@Casey Brooks any specific reason to use a sealed interface vs a sealed class?
c
The distinction is the same as an abstract class vs interface. It’s usually better to prefer the interface because it’s simpler, has slightly shorter syntax, and is more flexible. But for the typical use case of sealed class vs sealed interface, there’s usually not much of a difference between the two and they’re fairly interchangeable.
c
Thanks. One last question I encountered during my impl just now. With enums it was easy to do Environment.valueOf() to go from the name of an env to the actual type. I'm basically storing the "name" as another abstract val. Do you know if there's any easy way to go from a "name" to the Type itself? Or is a simple when statement with string equality all I need to do?
I'd like to do something like this (pseudo-ish code) fun getBackendTypeFromName(name: String): BackendType { val subclasses: List<KClass<out BackendType>> = BackendType::class.sealedSubclasses return subclasses.map { it::class.java }.first { it.humanReadableName == name }
I could maybe just do something like return when (name) { Localhost.humanReadableName -> Localhost Prod.humanReadableName -> Prod Staging.humanReadableName -> Staging //...
c
There’s no automatic conversion from a String name to a sealed subclass instance, because the subclasses might be classes that need to be instantiated with values (
Environment.Custom
needs
extraParameter
passed to its constructor). If you know the subclasses are all objects (or OK with this method only returning the object subtypes), this bit of reflection basically does what you need:
Copy code
// only works if all sub-types are objects
fun getEnvironmentByName(name: String) : Environment? {
    return Environment::class.sealedSubclasses
        .firstOrNull { it.simpleName == name }
        ?.objectInstance
}
However, if some of the subtypes are classes instead of objects, then you’ll need some additional logic to construct an instance with the additional parameters. For example, using the
kotlin-reflect
library, you can do this:
Copy code
public fun getEnvironmentByName(name: String): Environment? {
    val environmentType = Environment::class.sealedSubclasses
        .firstOrNull { it.simpleName == name }

    if (environmentType == null) {
        println("environment with name '$name' not found")
        return null
    }

    if (environmentType.objectInstance != null) {
        return environmentType.objectInstance
    }

    val createdInstance = environmentType
        .primaryConstructor
        ?.call("some extra parameter")
    if (createdInstance != null) {
        return createdInstance
    }

    println("Unable to find object or create instance")
    return null
}
c
interesting. okay. still getting used to sealed classes vs enums. i hate that i have basically a pretty darn strict enum, but the custom url throws a wrench into things. lol