CODEX

Roman numerals as an example of TDD refactoring in Java

Alonso Del Arte
Feb 26 · 10 min read
Photo by Fabrizio Verrecchia on Unsplash

The test-driven development (TDD) cycle consists of fail (red), pass (green) and refactor (blue). However, refactoring tends to get short shrift in most tutorials, it’s barely mentioned.

Or, when it’s mentioned, it feels unrealistic, especially if the intended program is of little practical value. Roman numeral arithmetic doesn’t have much practical value, but I do think it nevertheless provides a realistic example of one way that refactoring might arise in a real world project.

The problem is that sometimes, especially early on in the process, there’s no need to refactor anything. You ask yourself or your teammate whether there’s any opportunity for refactoring after you get a test to pass, but it becomes a meaningless mantra, and you just barrel on to the next test.

Take for example a Boolean function. You write a stub that always returns false. Then you write a test that expects the function to return true. The test fails, so you change the function to always return true. There’s no opportunity for refactoring there.

Maybe after failing and passing the next test an opportunity for refactoring will present itself. It can also happen that ideas for refactoring don’t occur to you until much later, especially under the pressure of a deadline when you’re not really inclined to question what’s working properly.

In this tutorial, we’re going to make a Java class that represents numbers in Roman numerals, and performs arithmetic on them (addition, subtraction, multiplication and division).

The arithmetic operations will actually take place in binary, and the conversion to Roman numerals will only occur in toString().

Here’s a skeleton for the class we’ll be working on:

public class RomanNumeralsNumber
implements Comparable<RomanNumeralsNumber> {

// TODO: Write getter or add @Getter annotation
private final short value;
// TODO: Override toString(), equals(), hashCode() // STUB TO FAIL THE FIRST TEST
@Override
public int compareTo(RomanNumeralsNumber other) {
return 0; // Short.compare(this.value, other.value);
}
public RomanNumeralsNumber(int n) {
// TODO: Add validation: no 0, no negative numbers
// TODO: Determine maximum n and add validation
this.value = (short) n;
}
}

As you know, there’s no Roman numeral for zero. There are no negative numbers with Roman numerals either.

As for what should be the maximum positive n that this class can accept, 3999 (corresponding to MMMCMXCIX) seems like a good choice, but you can change it if you want.

If you decide to go beyond MMMCMXCIX, whether with overlines or with symbols like ↈ (100000) and ↁ (5000), or some other way, note that short only goes up to 32767. In that case, just change short to int, or maybe long. I think BigInteger would be overkill.

Maybe the constructor’s parameter validation should be our top priority. What we work on first will have consequences for our TDD process, but that doesn’t necessarily mean the choice is right or wrong.

I say we work on overriding toString() first. We write a test that expects new RomansNumeralNumber(1) to return “I” for toString(). The test fails because that function instead returns something like “RomanNumeralsNumber@2b7”.

So we override toString() so it always returns “I”. There’s no opportunity for refactoring there, so we move on to the next test.

I don’t expect you to write 3998 more tests. Write seventeen more tests: eight to cover II to IX, then nine to cover X to XC. Once those are passing, write a test that loops through XI to XIX, XXI to XXIX, …, XCI to XCIX.

The expected String will be a concatenation, e.g., forty.toString() + seven.toString() for XLVII, for example. Repeat this process for C to CM and M to MMM until you have covered I to MMMCMXCIX. All these tests should run in less than a second on any decent computer.

At the end you might have something like this:

    @Override
public String toString() {
String unprocessed = Short.toString(this.value);
int currLen = unprocessed.length();
String numeral;
String processed = "";
char digitGlyph;
short multiplier = 1;
int digit;
while (currLen > 0) {
numeral = "";
digitGlyph = unprocessed.charAt(--currLen);
digit = digitGlyph - 48;
numeral = numeral + process(digit, multiplier);
processed = numeral + processed;
unprocessed = unprocessed.substring(0, currLen);
multiplier *= 10;
}
return processed;
}

This needs a private static function, process():

    private static String process(int digit, int power) {
switch (digit * power) {
case 0: return "";
case 1: return "I";
case 2: return "II";
case 3: return "III";
case 4: return "IV";
// etc.
case 9: return "IX";
case 10: return "X";
case 20: return "XX";
case 30: return "XXX";
case 40: return "XL";
// etc.
case 3000: return "MMM";
default: return "Unknown Digit Power Combination + ";
}
}

There’s certainly an opportunity for refactoring here, but it’s not obvious to me. I don’t want to have that many Cases in a Switch statement, but no simple straightforward solution occurs to me at the moment.

This is inelegant, but it’s not obviously inefficient. The computer can crunch all of these numbers a lot faster than I can convert a single arbitrary number to or from Roman numerals.

Sometimes it’s okay to leave things to code review. Let me know in the comments if you come up with a better way to convert an integer primitive to Roman numerals.

For now, let’s move on to arithmetic: we want to be able to add, subtract, multiply and divide RomanNumeralsNumber instances. Or at least we do in this hypothetical scenario.

I reiterate that there’s little real world need for this. But I think it will soon lead us to an example of refactoring that hopefully feels much more realistic and satisfying than other examples.

Write stubs for plus(), minus(), times() and divides(). Since we haven’t written any constructor validation yet, we can use something like “return new RomanNumeralsNumber(-1)” in our stubs without any problem.

So, for example, if our first test for plus() is I plus I, the test will expect II, but it will fail because the actual result is Unknown Digit Power Combination + I.

Though I doubt that you like the game of writing several tests just to come up with something that is so painfully obvious that it needs no refactoring. As in writing a test for I plus I, then a test for I plus V, etc.

So I want you to import java.util.Random into the test class. Then we have the plus() test come up with two random numbers between I and MCMXCIX (we don’t want an overflow at this point).

Then the test fails, expecting something like MCI plus VIII equals MCIX but getting Unknown Digit Power Combination + I instead. To get it to pass, we change plus() to something like this:

    public RomanNumeralsNumber plus(RomanNumeralsNumber addend) {
return new RomanNumeralsNumber(this.value + addend.value);
}

We do need to write one more test for plus(): if this and addend add up to 4000 or more, plus() should throw ArithmeticException, just like Math.addExact() does when the result is less than Integer.MIN_VALUE or Long.MIN_VALUE, or greater than Integer.MAX_VALUE or Integer.MAX_VALUE.

In this case, though, we needn’t worry about the result being less than I, or at least we won’t once we put in the constructor parameter validation. We only need to provide for the greater than MMMCMXCIX case.

So write the test, using assertThrows() if you’re using JUnit 5. I leave it up to you whether to use MM plus MM or some other single case thought up in advance, or use the pseudorandom number generator to come up with two numbers, each greater than MM but not greater than MMMCMXCIX.

The test should fail, as the plus() function gives a result such as Unknown Digit Power Combination + DCCLVI instead of throwing an exception for MMDCV plus MMMCLVI.

To get the test to pass, we rewrite plus() something like this:

    public RomanNumeralsNumber plus(RomanNumeralsNumber addend) {
if (this.value + addend.value > 3999) {
throw new ArithmeticException("The sum "
+ (this.value + addend.value)
+ " exceeds 3999");
}

return new RomanNumeralsNumber(this.value + addend.value);
}

I wrote my test to require a non-null exception message to pass, and I think you should, too. But you might decide that you don’t need the exception message to mention this.value + addend.value.

Either way, there are two opportunities for refactoring here, one more obvious than the other. The obvious opportunity is that this.value + addend.value is calculated twice (depending on branching) and mentioned thrice.

This has hardly any measurable impact on performance, but we should still change plus() to put this.value + addend.value into a local variable. Go ahead, make that change and verify the test still passes.

Next up, minus(). Write a test that calls minus() with a minuend of at least II and a subtrahend of at least I but less than the minuend. For example, DCC minus CIII. Check the test fails, rewrite minus() to pass the test.

The worry here is that zero could arise (if the minuend and subtrahend are equal) or the result could be a negative number (if the subtrahend is greater than the minuend).

So, write the the test for either zero or a negative number, that it should throw ArithmeticException, check it fails, rewrite minus() to pass.

    public RomanNumeralsNumber 
minus(RomanNumeralsNumber subtrahend) {
int projected = this.value - subtrahend.value;
if (projected < 1) {
String excMsg = "The number " + projected
+ " is outside the range of this class";
throw new ArithmeticException(excMsg);
}
return new RomanNumeralsNumber(projected);
}

Do you like that variable name, “projected”? If you don’t, change it. It’s much easier to make that change if you’re using an integrated development environment (IDE) like NetBeans.

Here we have eliminated one refactoring opportunity in advance. But I suggested there was another. If you haven’t figured out what it is yet, you will soon, when we move on to multiplication.

Since 3999 = 3 × 31 × 43, to express 3999 as a product of two integers, we have two choices: 43 × 93 or 31 × 129. Or you might prefer to limit both multiplicands to the range I to LXIII, so that the largest product to come up in the “main” test would be MMMCMLXIX.

As with addition, we’re not worried about zero or negative numbers like we were with subtraction. So, to pass the overflow test, we need only check that the product does not exceed 3999. This should do it:

    public RomanNumeralsNumber 
times(RomanNumeralsNumber multiplicand) {
int projected = this.value * multiplicand.value;
if (projected > 3999) {
throw new ArithmeticException("The product " + projected
+ " exceeds 3999");
}
return new RomanNumeralsNumber(projected);
}

Aside from the different parameter name, the different operation for projected and the different exception message, times() is exactly the same as plus(). Duplicated lines mean refactoring opportunity.

Your IDE probably has an inspection for “duplicated code.” But even if you had written the exact same exception message, your IDE’s threshold for flagging duplicated lines is probably set too high.

The duplication in divides() of minus() is much subtler. For something like XX divided by X, divides() should give II as the result. But X divided by XX is 0.5, which, by integer arithmetic, becomes just 0. And, as already stipulated, RomanNumeralsNumber should not represent 0.

But we haven’t yet written anything that forbids 0 or negative integers for the constructor parameter n. Let’s take care of that now. If n is outside the prescribed range, the constructor should throw an exception.

My first choice of exception for invalid constructor parameters is almost always IllegalArgumentException. However, in this case, I think we should go with ArithmeticException.

Write the appropriate tests, see them fail, then amend the constructor to make the tests pass:

    public RomanNumeralsNumber(int n) {
if (n < 1 || n > 3999) {
String excMsg = "The number " + n
+ " is outside the range 1 to 3999";
throw new ArithmeticException(excMsg);
}

this.value = (short) n;
}

All the tests should be passing at this point, in not even half a second.

And with this choice of ArithmeticException rather than IllegalArgumentException, we can greatly simplify the arithmetic functions:

    public RomanNumeralsNumber plus(RomanNumeralsNumber addend) {
return new RomanNumeralsNumber(this.value + addend.value);
}
public RomanNumeralsNumber
minus(RomanNumeralsNumber subtrahend) {
return new RomanNumeralsNumber(this.value
- subtrahend.value);
}
public RomanNumeralsNumber
times(RomanNumeralsNumber multiplicand) {
return new RomanNumeralsNumber(this.value
* multiplicand.value);
}
public RomanNumeralsNumber divides(RomanNumeralsNumber divisor)
{
return new RomanNumeralsNumber(this.value / divisor.value);
}

This saves us twenty-four lines, and all the tests are still passing (if you’re not sure just run them). The first time I ran them after this round of refactoring, they all passed in less than a quarter of a second.

There’s no performance penalty that we should care about from overflow operations causing exceptions in the constructor rather than directly in the pertinent arithmetic function.

It bothers me a little that the exception messages are less specific this way. But a quick look at a sample stack trace should ease any concerns. For example, suppose that we try MMXVIII + MMXIV or MMXVI × II. For the former, the exception message and stack trace would look something like this:

And for the latter, the exception and stack trace would be almost example the same, but with “RomanNumeralsNumber.times” instead of “RomanNumeralsNumber.plus,” and the appropriate line number.

If we had started with the constructor validation test, or at least done it before the arithmetic function tests, we probably wouldn’t have written all those arithmetic overflow tests.

Let’s now suppose that you decide to expand the range of numbers that RomanNumeralsNumber can represent, from 3999 to 399999 (as, for example, ↈↈↈↂↈↀↂⅭↀⅩⅭⅠⅩ).

After taking care of the necessary new toString() tests and adjusting the constructor validation tests, we’re going to have to adjust all the arithmetic overflow tests.

But the class under test will only need to be changed in toString() (and the private helper function, if applicable) and in its constructor.

In my opinion, Don’t Repeat Yourself (DRY) should be applied much more strictly in Source Packages than in Test Packages. But in neither case should it be taken to a ridiculous extreme.

An occasional criticism of TDD is that it often leads to test classes of at least twice as many lines as the classes they test. That’s not at all a bad thing.

With better test coverage, you can refactor with greater confidence that the tests will catch any problems that are unwittingly introduced in the refactoring process.

It might be annoying to have to change several tests in order to cope with a program requirement that changed. But wouldn’t you rather deal with that minor nuisance than with having to fix a program that broke while a customer was using it?

The RomanNumeralsNumber class is unlikely to be essential to a customer in a real world project. Even so, I like the apparent redundancy of having several arithmetic overflow tests.

More generally, classes in Source Packages should be refactored to avoid duplicating parameter validation in multiple places. Conversely, test classes should include tests to cover every foreseeable case where a change to the tested class could cause a validation problem.

I hope that this example with Roman numerals, though of limited practical value, does give you a good idea of one kind of opportunity for refactoring in real world projects.

CodeX

Everything connected with Code & Tech!

Alonso Del Arte

Written by

is a composer and photographer from Detroit, Michigan. He has been working on a Java program to display certain mathematical diagrams.

CodeX

CodeX

Everything connected with Code & Tech!

Alonso Del Arte

Written by

is a composer and photographer from Detroit, Michigan. He has been working on a Java program to display certain mathematical diagrams.

CodeX

CodeX

Everything connected with Code & Tech!

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store