Completing the Refactoring
Test Driven Development can be a great help when refactoring code, guiding you to the best possible outcomes.
In the last post I began to refactor some code using Test Driven Development and having changed the tests first, began to make some choices about how the refactoring was going to be done, including some opinionated design choices. Let’s conclude the refactoring now.
In this series of posts to introduce TDD, I showed how to start by writing a test for a new function that would return the sum of two Roman Numerals. Next we looked at adding tests for existing code, which converted integers to and from the Roman Numeral representation, but that did not already have any test. In part three we decided to refactor the functions into a RomanNumeral
object, but only got as far as refactoring the tests.
Hiding the Internal Representation
A Roman Numeral represents a positive integer using a combination of letters different, and differs from the decimal system because characters have both additive and negative effects depending on position. It feels equally valid to create a RomanNumeral
object using either its string representation or an integer. I decided to use helper functions in a companion object to allow callers to create the object without having to understand how the class internally holds state.
val first = RomanNumeral.fromString("XX")
val second = RomanNumeral.fromInt(20)
assertEquals(first, second)
Either function should return equivalent objects so the above assertion should pass (note that for this to work the equals
function must be overridden in theRomanNumeral
class – see below).
There are three options for storing state of the Roman Numeral. We could store the string representation and calculate the integer value only when needed. A second option would be the opposite, calculating the String value when required. A third way is to store both, doing the calculation at object instantiation. I’m choosing the latter, but I am going to implement in such a way that I can change my mind later – the interface I expose to users should be stable.
I want to hide the internal state of the object so that users don’t rely on how it is stored in memory. The following implementation stores both the integer value and the string representation of the Roman Numeral, but the constructor and member variables are private. There is a function to convert to integer and the toString
function has been overridden to give access to the representation.
class RomanNumeral {
private val value: Int
private val representation: String
private constructor(value: Int, representation: String) {
this.value = value
this.representation = representation
}
fun toInt(): Int = value
override fun toString(): String = representation
The benefit is that should we decide that we got the internal state wrong, we can change the class, including both the way state is stored and those factory functions, and any user code should be unaffected.
Getting the Tests to Compile
At the end of the last post, we had changed the tests to use functions in the companion object to create instances. To get the tests to compile the following placeholders for the these functions can be added.
companion object {
fun fromString(rep: String): RomanNumeral = TODO()
fun fromInt(value: Int): RomanNumeral = TODO()
}
The tests should now compile and run with failures as usual for TDD. Once all the object creation statements have been changed to use the helper functions, it’s time to add the implementation.
Implement the functions
The code in the companion object to create the RomanNumeral
object needs to do a conversion in both directions. I’ve ommited the algorithm used in the code shown below for brevity. If you are interested, please check out this GitHub Gyst which contains the full listing. Here are the functions:
companion object {
private fun toRomanNumeral(value: Int): String = /* see gyst */
fun fromString(rep: String): RomanNumeral {
val value = /* see gyst */
return RomanNumeral(value, rep)
}
fun fromInt(value: Int): RomanNumeral =
RomanNumeral(value, toRomanNumeral(value))
}
The second helper function, fromInt
, makes use of another function in the companion object to convert an Int
to a String
representing the Roman Numeral. It has been made private as external callers should not need to use it directly.
Finish Refactoring
We had three functions to start with, but with this refactoring into a RomanNumeral
class, two of them have changed into companion object functions. The third, to add two objects, still needs to get an implementation. Adding the integer values of the subject and object Roman Numeral and creating a new instance from the result can be done as follows.
inline operator fun plus(another: RomanNumeral): RomanNumeral =
fromInt(toInt() + another.toInt())
Note that it was not possible to use the private value
property in the function. This appears to be due to the way that the inline operator function is applied to the code. value
is a private member of the class so it cannot be referenced in an inline operator function. The property’s accessor function, toInt
, has to be used instead.
Because we would like two Roman Numerals with the same value to be considered equal, we also need to override the equals
and hashCode
functions. Here it is possible to use the value
private member as the code is called inside the class.
override fun equals(other: Any?): Boolean {
return if (other is RomanNumeral) {
value == other.value
} else {
false
}
}
override fun hashCode(): Int {
return value.hashCode()
}
Conclusion
Tests should exercise the classes under test just like they are being used in real code. So the tests, if they are written carefully enough, can describe the code being tested clearly.
The use of English language test function names using Kotlin’s back-tick notation, should make it really easy to understand what each test is checking. I prefer to begin each function name with the words “should test that” or something similar to push my thinking towards a good, descriptive name.
Since the tests act as a description of what the code does, they are a great place to start making changes when refactoring the code. By changing the tests, you can see all use cases being altered before making any hard decisions about the best way to implement new functionality. Decisions can be delayed until almost everything is known.
Then, when it is time to change the actual code, do it in lock-step with further test changes to make sure everything still compiles and runs, even if the tests fail because some parts of the code are not implemented. Watch the red Xs turn into green ticks as you implement one function or class after another. Satisfying.