Colton Idle
09/14/2023, 3:14 AMsealed 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()
}
Pablichjenkov
09/14/2023, 3:40 AMfoo: 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 DCasey Brooks
09/14/2023, 5:01 AMState
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.Casey Brooks
09/14/2023, 5:18 AMsealed 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)
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:
enum class StateType {
A, B, C, D
}
data class State(
public val stateType: StateType,
public val foo: String?,
)
Colton Idle
09/16/2023, 1:29 AMserverEnvironment.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 wayCasey Brooks
09/16/2023, 2:19 AMinterface
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.
// 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)
}
}
}
// 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)
}
}
}
Colton Idle
09/17/2023, 6:52 PMColton Idle
09/20/2023, 6:28 PMCasey Brooks
09/20/2023, 6:38 PMColton Idle
09/20/2023, 6:41 PMColton Idle
09/20/2023, 6:51 PMColton Idle
09/20/2023, 6:53 PMCasey Brooks
09/20/2023, 6:59 PMEnvironment.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:
// 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:
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
}
Colton Idle
09/20/2023, 7:35 PM