For the first time since I moved to Nuremberg I made it to the local monthly Softwerkskammer meeting yesterday. Softwerkskammer is a german community format focusing on software crafting and it’s a great way for software developers to improve their skills.
This month’s topic was The Transformation Priority Premise (TPP), an approach to writing code coined by Robert C. Martin aka. Uncle Bob. It tries to address the problem that – when doing Test-Driven-Development (TDD) – sometimes you get stuck in the TDD loop because you have to substantially change the existing implementation to make new tests pass.
TDD?! TPP?! WTF!
Let’s have a quick look at what Test-Driven-Development and the Transformation Priority Premise are about. During the normal TDD cycle, there’s three phases:
- Write a failing test
- Make the test pass by implementing new functionality
- Refactor the code
During Phase 3 you apply refactorings to your implementation in order to get it into a more cohesive state. Refactorings don’t change behaviour, they only change the structure of existing code. In contrast, changes in Phase 2 introduce new behaviour. Uncle Bob calls these changes Transformations and they are in some way a counterpart to refactorings.
TPP states the premise, that transformations applied during the second phase of TDD have different complexity and should therefore be prioritized. In order to make a test pass, higher priority transformations should always be preferred. In fact, when thinking about which test to write next you should choose one that you can make green with a higher priority transformation.
So what are those transformations? In his original article, Robert C. Martin gives a list of possibilities:
- ({} –> nil): no code at all -> code that employs nil
- (nil -> constant): nil -> code that employs a constant value
- (constant -> constant+): a simple constant to a more complex constant
- (constant -> scalar): replacing a constant with a variable or an argument
- (statement -> statements): adding more unconditional statements.
- (unconditional -> if): splitting the execution path
- (scalar -> array): replace a scalar value with an array
- (array -> container): replace an array with a container
- (statement -> recursion): replace a statement with a recursion
- (if -> while): replace an if statement with a while loop
- (expression -> function): replacing an expression with a function or algorithm
- (variable -> assignment): replacing the value of a variable.
Transformations on top of that list have higher priority than those at the bottom. Now, not all of these transformations might make sense in any given context, for example there’s programming languages that don’t allow reassignment of variable values. What’s important however is the idea, that different transformations have different complexity and complex solutions will be more difficult to generify later. Micah Martin has, in a different article, come up with a slightly simplified version of these transformations:
- constant: a value
- scalar: a local binding, or variable
- invocation: calling a function/method
- conditional: if/switch/case/cond
- while loop: applies to
for
loops as well - assignment: replacing the value of a variable
You get the idea: TPP is not a fixed set of rules that you can just apply while doing TDD. You’ll need to reflect and see how it applies to your context.
Working with TPP
After an introductory talk about the topic, which was based on David Völkel’s previous presentation at the Munich Softwerkskammer meeting, we split up into pairs. By solving the Roman Numerals Kata using TDD we were to find out how TPP can help us. My team chose Java to implement it in order to not get distracted by language unfamiliarities. Our approach was to look for easy to pass tests and then implement them.
We started out with the simple case 1 -> I
by initially returning the constant “I” :
@Test
public void one() {
assertEquals("I", RomanNumbers.convert(1));
}
public String convert(String arabicNumber) {
return "I";
}
For the next scenario 2 -> II
we just repeated the the symbol “I” as often as the input parameter stated. This the also neatly managed the 3 -> III
case.
@Test
public void two() {
assertEquals("II", RomanNumbers.convert(2));
}
public void three() {
assertEquals("III", RomanNumbers.convert(3));
}
public String convert(String arabicNumber) {
return StringUtils.repeat("I", arabicNumber);
}
Looking for the next simple test we decided to go with 5 -> V
, since for us 4 -> IV
seemed a more complex matter. We added a conditional and another constant for that.
@Test
public void five() {
assertEquals("V", RomanNumbers.convert(5));
}
public String convert(String arabicNumber) {
if (arabicnumber == 5) {
return "V";
}
return StringUtils.repeat("I", arabicNumber);
}
A similar case was 10 -> X
, so we solved that accordingly. Conditionals are fairly expensive in terms of complexity, so we looked for a way to get rid of the repetition. This led us to introducing a symbol mapping that we could iterate over.
@Test
public void ten() {
assertEquals("X", RomanNumbers.convert(10));
}
static List>; romanSymbols = new ArrayList<>();
static {
romanSymbols.add(new AbstractMap.SimpleEntry<>(5, "V"));
romanSymbols.add(new AbstractMap.SimpleEntry<>(10, "X"));
}
public String convert(String arabicNumber) {
for (Map.Entry entry : romanSymbols) {
if (arabicNumber == entry.getKey()) {
return entry.getValue();
}
}
return StringUtils.repeat("I", arabicNumber);
}
Moving on, we decided to try implementing 6 -> VI
. By making use of a recursive invokation, we were able to easily come up with behaviour that also worked for 7 -> VII
. And at the same time we got rid of the default case for remaining “I” at the end by adding 1 -> I
to the mappings as well.
@Test
public void six() {
assertEquals("VI", RomanNumbers.convert(6));
}
public String convert(String arabicNumber) {
for (Map.Entry entry : romanSymbols) {
if (arabicNumber >= entry.getKey()) {
return entry.getValue()
+ convert(arabicNumber - entry.getKey());
}
}
return "";
}
At this point we wanted to tackle the reverse combination cases like 4 -> IV
or 9 -> IX
. After some considerations, we saw that we could simply add them to our mappings as symbols of their own right. That way all the rest of the implementation continued to work and we basically just added more bindings. This was totally in line with TPP.
In order to get more tests passing, at this point we would just add new symbols to the mapping. We still saw a pattern in those mapping that we would have liked to address to minimize our mappings further, but unfortunatly time was up. If you want to look at our complete solution you can view it on Bitbucket.
Conclusion
In the meeting’s retrospective, there were some solutions that were conceptually similar to ours, some other’s where quite different though. We had the impression, that thinking about transformation priorities guided us naturally to the solution that we ended up with. In fact, we had a few light-bulb moments when we saw our algorithm evolve. If you practice TDD – and you should! – I definitely recommend having a closer look at the Transformation Priority Premise.
I find that doing TDD and applying the TPP almost makes you feel stupid sometimes for implementing what you know are silly solutions, when you think you can see a more intelligent general solution already. It feels counterintuitive as a human to reduce your thinking to something that more closely resembles a dumb robot working in a loop. It strikes me that all we’re doing here is writing declarative rules and then applying a constraint-solving algorithm to find a solution, with as little thought as possible. The programmer is reduced to an algorithm. However, I recognize the benefits of this approach, as our temptation to leap to solutions can hurt us too. But I think the long-term implications of TDD + TPP are that programmers will only write tests and the code will be generated by algorithms that solve these tests. Non-declarative programming is set to become as obsolete as writing in assembly.