Hello, I'm Munetoshi Ishikawa, a mobile client developer for the LINE messaging app.
This article is the latest installment of our weekly series "Improving code quality". For more information about the Weekly Report, please see the first article.
Bad enumerations drive out good layers
Let's assume that a service has defined the following enumeration for "user account types":
enum class AccountType { FREE, PERSONAL, UNLIMITED }
When reading and writing this value using local storage, databases, or APIs over the network, mechanisms called "converters" or "mappers" are used to convert between language-specific objects and byte sequences defined in interface definition languages or protocols.
For example, in Android application development, you can use a persistence library called Room, which allows easy implementation of converters using a mechanism called TypeConverter
.
However, there are issues with the following use of TypeConverter
. What might those issues be?
class AccountTypeConverter {
@TypeConverter
fun fromStringValue(typeString: String): AccountType = AccountType.valueOf(typeString)
@TypeConverter
fun toStringValue(type: AccountType): String = type.name
}
class AccountTypeConverter {
@TypeConverter
fun fromIntValue(typeInt: Int): AccountType = AccountType.values()[typeInt]
@TypeConverter
fun toIntValue(type: AccountType): Int = type.ordinal
}
Converters as a layer of corruption prevention
The issue with this code is that by using the enumeration properties name
and ordinal
, the converter fails to act as a layer of corruption prevention.
This issue allows changes in external interfaces or protocols to affect the code using the enumeration, or in reverse. If there are changes in the values used in databases or remote APIs, it would necessitate changes in the enumeration definitions, affecting the code using them. On the other hand, there might be a need to rename or reorder the enumerators for internal reasons. However, changes in names or order directly affect the externally used values, restricting freedom in making such changes.
Using ordinal
can cause specific issues. Suppose you want to add a new AccountType
called BUSINESS
. From a pricing and feature perspective, it makes sense to place BUSINESS
between PERSONAL
and UNLIMITED
.
enum class AccountType { FREE, PERSONAL, BUSINESS, UNLIMITED }
However, this change alters the value of UNLIMITED.ordinal
, necessitating updates to the externally used values as well.
Similarly, using name
can cause issues. For example, if you want to rebrand and change the names of the enumerators from FREE
, PERSONAL
, UNLIMITED
to BRONZE
, SILVER
, GOLD
, implementing this change directly would break the values already in use externally.
The frightening aspect of using ordinal
and name
is that even simple refactoring from the perspective of the code using the enumeration can inadvertently introduce bugs. It's not immediately obvious that such changes would affect the externally used values.
Wrapping to prevent corruption
To solve this problem, it's beneficial to separate the declarations of the enumerators from the externally used values. Here's a method to define the externally used values as properties of the enumeration:
enum class AccountType(val dbValue: String) {
FREE("free"),
PERSONAL("personal"),
UNLIMITED("unlimited");
companion object {
val DB_VALUE_TO_TYPE_MAP: Map<String, AccountType> =
values().associateBy(AccountType::dbValue)
}
}
This approach protects the externally used values from changes in the names or order of the AccountType
enumerators.
If it's necessary to hide the dbValue
from the code using AccountType
, define a separate converter class where you can define functions and properties corresponding to dbValue
and DB_VALUE_TO_TYPE_MAP
.
Exception: Eating on the spot prevents corruption
If you use the enumeration only as a "temporary conversion" without converting it to externally used values, using name
and ordinal
is less likely to cause problems. For example, this might apply when saving integers instead of enumerators as an in-memory cache. However, even in such cases, it's better to use the enumerators themselves as much as possible to benefit from type safety.
In summary: In code that converts externally defined values, define internal and external values independently.
Keywords:
value conversion
,externally defined value
,enum