Once you copy the calculation for loan amounts above $2000, the test passes:

Here's how the code looks like once you remove all Magic Numbers:

The code that shows the result after you apply the same steps of the previous post, including all the duplication.

Right now that seems like a mess. The code has a lot of duplication and hard-coded values everywhere. However, this is the kind of messy code that was driven by tests. Therefore, it contains many patterns that can lead to insightful discoveries.

To uncover those patterns, you need to refactor. You need to apply small changes to the code without altering its behavior. The way you see that you're not altering the behavior of the program is when you apply the changes for a module/class/function — like saving, — and the behavior of the program doesn't change.

That’s the reason why it’s so critical to start with Tests-First. If you don’t write Tests-First, it's harder to ensure that you're testing the right things and that the behavior of the system won’t change when you refactor. In the same way, without practicing Test-Driven, it's hard to understand if you’re increasing or decreasing the level of transformation according to the Transformation Priority Premise.

You know you're refactoring when you change the code, and the following remains true: the level of transformation of the code doesn't decrease, the tests stay green, and future tests which follow the same pattern would also stay green.

At this point, the code has duplication for each conditional. An effective way to remove that duplication is to create a function with arguments for the values that change.

However, it's hard to know how that new function should look like beforehand. If you want to increase the chances for the tests to stay green all the time and keep the changes small, you can start with pure functions that are very specific to their purpose. You can modify them to be more generic later.

That said, create a new function for the calculation of interest rates when the loan amount is higher than $2000. It's a good idea to keep the function closer to the code you're extracting so that you can see in which position the arguments should be.

After that, it's a good idea to lift the function that calculates the interest to a scope outside the primary function "interest to pay for." Although this violates the Strictness Principle, which states you should keep variables only in the scope that's using them, it also allows you to verify that the function doesn't access any external variables, the "side-effects." If the function you create has access to external variables, it's hard to change it. If the tests don't break after moving it, that means the function has no side-effects.

After you make sure that the tests pass, lift the function outside the scope and replace the logic everywhere else.

When you refactor code to a new function, verify if it doesn’t have side-effects.

If you do the same thing for each one of the other calculations, you'll end up with a code that looks like this:

The code for the conditions after you create one function for each calculation.

You still have duplication, but it looks better than before. There’s one function to handle $2000, one function to handle $5000 and another function to handle $10000.

The code that shows the implementation of the functions to calculate each range.

When you refactor, and there's duplication, it's essential to keep the functions as similar as you can to each other. As humans, we are pattern recognition creatures. If you have code that looks the same, it's much easier to understand the problem and discover meaningful patterns.

Notice that the first function to calculate loan amounts above $2000 is missing one argument to have the same number of arguments as the other functions. You can fix that.

Also, the internal variables and arguments for all the functions have different names. Let's make them the same.

You can see now that all the functions accept the same things:

The loan amount.

The amount that represents the "end of the range."

The amount that represents the "interest per dollar."

The amount that represents the "previous interest per dollar."

The “loan amount” is a fixed value. It’s the input that only changes in the context of the primary function “interest to pay for.” The value for the "loan amount" won't change throughout the execution of each calculation.

The other arguments are different:

The code calls the functions with a different value for the arguments “end of the range,” “interest per dollar” and “previous interest per dollar” depending on which calculation is running. The functions to calculate the interest for each range uses Connascence of Position for its arguments, instead of Connascence of Name. That's a Bad Code Smell.

To fix the Bad Code Smell and discover why the code calls the functions with the different arguments, you can apply the DRY approach. Create one Object Literal representing the arguments that change, then reuse them. You can start with the range of calculations for loan amounts above $2000:

Then, as a second step, uplift the Object Literal outside the function. Given this is an interface breaking change, you need to update all the external function calls inside the other conditionals for the tests to remain green.

If you do the same thing for the other ranges and delete the duplication completely you'll end up with a piece of code that exposes a new pattern:

The code after you delete all the functions to calculate interest and replace with a generic one.

You can see the commits which lead to that result.

Now that you refactored the code, you can see that there's only one return, which is the "interest amount," but the code duplicates it inside every condition.

Let's remove that duplication:

Now take a careful look at the code:

The code that shows the first two conditions. There's a duplication in the lines 3 and 6.

You can see the condition for a loan amount greater than $5000 repeats the calculation for a loan amount greater than $2000. The reason it repeats is that the first condition only runs if the loan amount is less than $5001.

You can dump the right-hand side conditional of the first condition. If you do, you fix the duplication:

You can do the same thing for the rest of the code:

Here's the result:

The code that shows each condition handling one calculation.

The code above clearly shows how the algorithm calculates the interest if a “loan amount” is greater than $2000, $5000, or $10000.

Now here's the mind-blowing moment:

When you refactor the code to make each component similar to each other and remove Bad Code Smells, not just the code becomes painless to maintain, but you'll also better understand the patterns of the problem you are trying to solve. This way, you know you are generalizing in the right direction without speculation or over-engineering.

In Test-Driven Development, you only write the code you need. Nothing else.

Another interesting thing you can see is that there's a decoupling between the ranges and the code that runs the calculation on them. You can extract the ranges into a JSON configuration file. If you do that, Jack can modify the behavior of the code by modifying the config file. He doesn't need to pay a developer every time he wants to modify behavior that follows the same pattern.

If you don't want to extract the ranges into a configuration file, you can still refactor the code to emphasize the decoupling. When you emphasize decoupling, you also help to increase the legibility of the code, regardless if you move the data to a configuration file or not.

Test-Driven Development allows you to understand the problem and create more value.

That's it! Here's the final code:

The final code after you apply all the refactoring from this post.

You won't perceive most of the benefits of TDD for problems inside the “obvious” domain. In "obvious" domains, the cause and effect are very clear, and the situation is stable.

You'll perceive most of the benefits of TDD for problems that the Cynefin Framework characterizes as “complicated.” In "complicated" domains, the cause and effect relationship is not apparent and requires more analysis. Test-Driven Development is a great tool to help you with that analysis because it allows you to ask the right questions.

An example of an "obvious" domain is to send an HTTP POST request to a website with some simple data and no authentication. You can use curl and run in the command line.

An example of a "complicated" domain is to create a Content Management System. In a CMS, you need to ask the right questions in every step of the way to get the knowledge about how the domain works.

You perceive most of the benefits of TDD when you work in "complicated" problems instead of "obvious" ones.

If you've been reading this since the first post, now you should now be able to understand every detail of the "Jack, The Moneylender" problem. That means you can continue refactoring the code as much as you want and be confident you never introduce bugs unintentionally.

Test-Driven Development and refactoring may sound like a tedious process. However, as with any skill, you get better over time. With practice, your velocity increases. Next time you get a similar problem, you may discover the patterns earlier and finish all this in a fraction of the time.

Jack didn't merely choose anybody to solve his problem.

He chose a professional programmer.