Don’t use var in enums
Recently I came across this kind of code.
enum class SomeEnum(var someValue: String) {
TEST1("test 1"),
TEST2("test 2")
}
Notice the usage of var
inside the enum
cases. This is actually a very bad idea. Depending on the usage of the enum
you may never notice in your normal development process that this kind of pattern introduces a subtle bug into your application.
To illustrate what is wrong with this kind of pattern, let’s take a look at how this code behaves in unit tests:
class SomeEnumTest {
@Test
fun test1() {
SomeEnum.TEST1.someValue = "random"
assertEquals("test 2", SomeEnum.TEST2.someValue)
}
@Test
fun test2() {
assertEquals("test 1", SomeEnum.TEST1.someValue)
}
}
If you now run all the tests in the SomeTest
class, despite the fact that the test2
test case does not overwrite the value of SomeEnum.TEST1
case, the test2
test case will fail — maybe. Whether it fails or not depends solely on the order of execution of the test cases. Let’s stick for now with the assumption that the test1
test case runs first and the test2
test case runs second. In this scenario the test2
test case will fail.
This is a problem. test2
was affected by behavior of test1
. However, to be able to test that your code behaves properly, one test case should not affect other test cases! Depending on naming of your test cases or other rules of execution of unit tests you might not even notice, that there’s a problem in your implementation. You might get into a situation where a test case passes despite the fact that it should fail!
But why is the test2
even failing in the first place?
The reason for that is, that enum cases are initialized only once at the start of the application. When overwriting value of a var
inside an enum
case, all the usages of the enum
case are affected. enum
cases effectively act as object
in Kotlin.
In fact, should you try writing similar pattern with objects
, you’d see the exact same behavior:
class SomeObjectTest {
@Test
fun test1() {
SomeObject.someValue = "random"
assertEquals("random", SomeObject.someValue)
}
@Test
fun test2() {
assertEquals("someValue", SomeObject.someValue)
}
}
So is this a problem only about testing?
Absolutely not! Even your runtime code is affected. I have shown you the unit tests only to demonstrate the problem that affects the runtime of your application as well. Specifically, if an enum
or an object
are reused by different parts of the application, overwriting their var
value in one place will have negative aspects on other parts of the application that are using them. This problem lies at the crux of the the infamous singleton pattern/anti-pattern debate.
So how do I avoid this problem?
First of all NEVER use var
inside enum
cases or inside an object
. If you see anybody using a var
inside an enum
or an object
, send them here.
In fact it would be very beneficial to avoid using var
altogether in your code! Immutable code is the safest bet to avoid weird runtime bugs.
Having said that, let’s say that for some obscure reason, you absolutely do have to use var
, you do have to overwrite a value of an enum
case (ask yourself if you really absolutely must use var
). What to do then?
In that case do not use enum
! Fortunately, Kotlin is your friend here, providing you with an alternate solution. sealed class
or a sealed interface
.
sealed class SomeSealedClass(var someValue: String) {
class Test1 : SomeSealedClass("test 1")
class Test2 : SomeSealedClass("test 2")
}
class SomeSealedClassTest {
@Test
fun test1() {
SomeSealedClass.Test1().someValue = "random"
assertEquals("test 2", SomeSealedClass.Test2().someValue)
}
@Test
fun test2() {
assertEquals("test 1", SomeSealedClass.Test1().someValue)
}
}
These test cases do not fail regardless of execution order, because as you can notice, we’re creating a completely new instance of Test1
in both test cases. These are not shared across the execution of the whole application.
Having said that, after reading this article, you should already know, how the following code would behave:
sealed class SomeSealedClass(var someValue: String) {
object Test1 : SomeSealedClass("test 1")
object Test2 : SomeSealedClass("test 2")
}
class SomeSealedClassTest {
@Test
fun test1() {
SomeSealedClass.Test1.someValue = "random"
assertEquals("test 2", SomeSealedClass.Test2.someValue)
}
@Test
fun test2() {
assertEquals("test 1", SomeSealedClass.Test1.someValue)
}
}
Depending on the order of execution, the test2
test case fill of course fail, because now we’re using a var
inside an object
(both Test1
and Test2
are object
s) and the instance is shared across the whole run of the application.
I hope it is now clear, why you should avoid var
s inside enum
or object
s like cancer.