Kotlin Code Smell 26 - if-else/when statements
Code Evolution: Conquering Complexity with Patterns
Table of contents
First programming lesson: Control structures. Senior developer lesson: avoid them.
Problems
- Too many decisions together
- Coupling
- Duplicated code
- Violation of Open/Closed Principle.
- A new condition should not change the main algorithm.
- Nulls
Solutions
- Polymorphism
- Create hierarchies/compose objects following the Open closed principle.
- Use State pattern to model transitions.
- Use Strategy Pattern/Method Object to choose for branches.
Examples
- Discrete Values
- State transition
- Algorithm choice.
Sample Code
Wrong
fun convertToMp3(source: Path, mimeType: String) =
when(mimeType) {
"audio/mpeg" -> convertMpegToMp3(source)
"audio/wav" -> convertWavToMp3(source)
"audio/ogg" -> convertOggToMp3(source)
// Lots of new if-else cases
else -> throw IllegalArgumentException("Unknown mime type")
}
fun convertMpegToMp3(source: Path) = println("Convert from mpeg")
fun convertWavToMp3(source: Path) = println("Convert from wav")
fun convertOggToMp3(source: Path) = println("Convert from ogg")
Right
val registeredConverters = mapOf(
"audio/mpeg" to ::convertMpegToMp3,
"audio/wav" to ::convertWavToMp3,
"audio/ogg" to ::convertOggToMp3,
// Lots of other converters
)
fun convertToMp3(source: Path, mimeType: String) =
registeredConverters[mimeType]
?.let { converter -> converter(source) }
?: throw IllegalArgumentException("No converter found")
fun convertMpegToMp3(source: Path) = println("Convert from mpeg")
fun convertWavToMp3(source: Path) = println("Convert from wav")
fun convertOggToMp3(source: Path) = println("Convert from ogg")
After Further Refactoring (Using Objects)
interface Mp3Converter {
fun convertToMp3(source: Path)
}
class ConvertMpegToMp3 : Mp3Converter {
override fun convertToMp3(source: Path) =
println("Convert from mpeg")
}
class ConvertWavToMp3 : Mp3Converter {
override fun convertToMp3(source: Path) =
println("Convert from wav")
}
class ConvertOggToMp3 : Mp3Converter {
override fun convertToMp3(source: Path) =
println("Convert from ogg")
}
// Many more converters
val registeredConverters = mapOf(
"audio/mpeg" to ConvertMpegToMp3(),
"audio/wav" to ConvertWavToMp3(),
"audio/ogg" to ConvertOggToMp3(),
// Lots of other converters
)
fun convertToMp3(source: Path, mimeType: String) =
registeredConverters[mimeType]
?.convertToMp3(source)
?: throw IllegalArgumentException("No converter found")
Conclusion
Excessive use of if-else/when statements in Kotlin can lead to code smells, making the code difficult to maintain and understand. To avoid these issues, it is essential to utilize design patterns such as polymorphism, state patterns, and strategy patterns.
You can almost always replace the if-else/when statement with a map implementation.