What is this code doing?

Using refactoring techniques to help understand a piece of code, and also make it easier for others to understand.

PS: To learn more about Clean Coding practices and applying these techniques on a real life codebase, checkout our courses on Clean Code and TDD. Our courses are 100% hands-on, very interactive, and not more than 5 hours long.

Consider the following code. This is an actual code written for a programming contest. The author is one of the smartest software developers I know, and someone I really respect.

Most likely, the code is correct. However, by looking at the code it is quite hard to understand what it is doing.

package com.example.cleancode;

import java.util.Scanner;

public class Main {

static int gcd(int a, int b) {
if (b == 0)
return a;
return gcd(b, a % b);
}


public static void main(String[] args) {
int t, a, b, r;
Scanner in = new Scanner(System.in);
t = in.nextInt();
while (t-- > 0) {
a = in.nextInt();
b = in.nextInt();

//using MACRO and determining GCD of p,q
int k=gcd((a>b)?a:b,(a<b)?a:b);

// bring the numbers to their smallest possible forms
a/=k;
b/=k;

int[] A = new int[b+5];
int[] B = new int[b+5];
int x=0,ans=-1;

int flag=1;
a=a%b;
while(true)
{
if(a==0)
{
flag=0;
break;
}
a*=10;
while(a<b)
{
a*=10;
x++;
}
r=a%b;
if(A[r]!=0)
{
ans=x-A[r];
break;
}
A[r]=x;
x++;
a=r;
}
if(flag==0)
System.out.println("-1");
else
System.out.println(ans);

}
}
}

Imagine if you had functions like these in a new codebase on a team you just joined, and you were trying to figure out where to make changes. You would be lost.

I don’t know what the code above does. However I am confident I can figure that out by applying clean code principles.

In this blog, let’s apply some refactoring techniques to see how we can use them to understand the code ourselves, and make it easier for others to work on the codebase.

Ensuring Correctness

When I get such a code, the easiest way to understand it better is to refactor. You can start by changing variable names to more meaningful ones, and then move on to bigger changes.

Before doing any refactoring, I need tests to ensure I’m not breaking anything. Since we have a main class here, it’s not easy to write a unit test. However, I can manually run the code for random different inputs, and save the outputs in a file. Till it becomes possible to write tests, I can manually run the program and keep comparing the output with the original output to make sure I’ve not broken anything.

I can makeout that the program is accepting t testcases, each of which requires 2 numbers as input, and prints 1 number as output. Here are the input and output of the random test cases I came up with -

The input -

11
11 19
10 15
10 11
8 213
5 7
19 33
5 2
20 6
20 5
5 20
30 19

The output -

18
1
2
35
6
2
-1
1
-1
-1
18

Separating I/O and Logic

I can see that for every test case we read 2 numbers a and b, and then output either -1 or ans. If I can extract a function that takes a and be as input and returns the output, I can separate it from the actual input and output code.

package com.example.cleancode;

import java.util.Scanner;

public class Main {

static int gcd(int a, int b) {
if (b == 0)
return a;
return gcd(b, a % b);
}


public static void main(String[] args) {
int t, a, b, r;
Scanner in = new Scanner(System.in);
t = in.nextInt();
while (t-- > 0) {
a = in.nextInt();
b = in.nextInt();

System.out.println(calculateAnswerForTestCase(a, b));

}
}

private static int calculateAnswerForTestCase(int a, int b) {
int r;//using MACRO and determining GCD of p,q
int k=gcd((a>b)?a:b,(a<b)?a:b);

// bring the numbers to their smallest possible forms
a/=k;
b/=k;

int[] A = new int[b+5];
int[] B = new int[b+5];
int x=0,ans=-1;

int flag=1;
a=a%b;
while(true)
{
if(a==0)
{
flag=0;
break;
}
a*=10;
while(a<b)
{
a*=10;
x++;
}
r=a%b;
if(A[r]!=0)
{
ans=x-A[r];
break;
}
A[r]=x;
x++;
a=r;
}
if(flag==0) ans = -1;
return ans;
}
}

I can now manually run the code again to ensure it is still working as before.

Writing Tests

Since I have a separate function, I can now write tests that will test the same scenarios that I was manually testing earlier.

These tests are not tests that are written with an expected behaviour in mind. These tests only ensure that the output is what it was before. Thus, if the existing code has 7 bugs, it will continue to have those same 7 bugs. But these tests will ensure that I don’t introduce an additional 8th bug.

These tests are called Characterisation Tests, since they pin the character of the system to a certain state.

Here is my Characterisation Test.

package com.example.cleancode;

import org.junit.Test;

import static com.example.cleancode.Main.*;
import static org.junit.Assert.*;

public class MainTest {

@Test
public void shouldCalculateAnswerForMultipleTestCases() {
assertEquals(18, calculateAnswerForTestCase(11,19));
assertEquals(1, calculateAnswerForTestCase(10,15));
assertEquals(2, calculateAnswerForTestCase(10,11));
assertEquals(35, calculateAnswerForTestCase(8,213));
assertEquals(6, calculateAnswerForTestCase(5,7));
assertEquals(2, calculateAnswerForTestCase(19,33));
assertEquals(-1, calculateAnswerForTestCase(5,2));
assertEquals(1, calculateAnswerForTestCase(20,6));
assertEquals(-1, calculateAnswerForTestCase(20,5));
}

}

The First Refactoring

Look at the logic that calculates k. We can make out that it is calculating the gcd of 2 numbers, and ensuring that we send the larger number first to the actual gcd function. We can extract another function for this, and also write tests for it

In Main.java -

static int orderInsensitiveGCD(int a, int b) {
return gcd((a>b)?a:b,(a<b)?a:b);
}

In MainTest.java -

@Test
public void shouldCalculateGCDWithoutCaringAboutWhatNumberIsLarger(){
assertEquals(4, orderInsensitiveGCD(8,220));
assertEquals(4, orderInsensitiveGCD(220,8));
assertEquals(1, orderInsensitiveGCD(13,2));
}

Understanding the Domain

For this, I needed a long time, and had to put some effort into understanding the code. We were reducing two numbers by dividing them by their greatest common divisor (GCD or HCF). Then we were multiplying one of the numbers by 10, calculating the remainder and repeating this process. This gave me the hint that we were dealing with fractions and the digits after the decimal point.

So I went ahead and introduced the concept of Fraction everywhere in our code. Instead of finding the GCD and dividing both numbers by it, I created a method called reduce that does this. I also started using the words numerator and denominator everywhere.

Then I proceeded to write tests for this function. Now this lies under the category of unit test.

Here is the calculateAnswerForTestCase function in Main.java after the refactoring. See how it looks more understandable after introducing the Fraction class -

static int calculateAnswerForTestCase(Fraction fraction) {
int r;

final Fraction reducedFraction = fraction.reduce();


int x = 0, ans = -1;

int flag = 1;
Fraction fractionWithoutWholeNumber = reducedFraction.withoutWholeNumber();
int numerator = fractionWithoutWholeNumber.getNumerator();
int denominator = fractionWithoutWholeNumber.getDenominator();
int[] A = new int[denominator + 5];

while (true) {
if (numerator == 0) {
flag = 0;
break;
}
numerator *= 10;
while (numerator < denominator) {
numerator *= 10;
x++;
}
r = numerator % denominator;
if (A[r] != 0) {
ans = x - A[r];
break;
}
A[r] = x;
x++;
numerator = r;
}
if (flag == 0) ans = -1;
return ans;
}

The Next Steps

I proceeded to follow these rules ot make the code cleaner -

1. Give meaningful names.
2. Extract logical operations into methods.
3. Move methods close to where the data lies.

The third step above means that if there is a method that is dealing only with numerator and denominator, it probably belongs to the fraction class.

I used these steps again and again to make the code understandable, and then rewrote some of the logic to make it even easier to understand.

What does the Code Do?

Here is the final state of the Main Class. The remaining code is on github here. See it for yourself and tell me if you can now understand what it does?

package com.example.cleancode;

import java.util.Scanner;

import static java.lang.System.in;
import static java.lang.System.out;

public class Main {


public static void main(String[] args) {
int numberOfTestCases, numerator, denominator;
Scanner inputScanner = new Scanner(in);

numberOfTestCases = inputScanner.nextInt();

while (numberOfTestCases-- > 0) {
numerator = inputScanner.nextInt();
denominator = inputScanner.nextInt();

out.println(new Fraction(numerator,denominator).countOfRecurringDecimalDigits());

}
}

}

The code can be improved further. My aim today was to bring it to a state where it is understandable. If I have time, I can continue to improve it. Otherwise, I can leave it as it is, and have faith that the next time somebody from our team touches the code, they will also improve it further.

Summary

To make any changes to a code, you have to understand it first. While you understand the code, you can refactor to leave a trail of understanding, so that the next time you or your team have to work on the code, they don’t have to spend time understanding it.

If you liked this article, please share it on social media.

View my courses on Udemy
Subscribe to us on Linkedin
View all our Youtube Videos
Join our Facebook Group

Subscribe to us on Youtube for an awesome video every Wednesday!

Written by

Educator, Founder @ Interleap

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