Home Redundancy
What is redundancy?
You should aim to eliminate as much logical redundancy from your code as possible. If you need to repeat a task, don't copy-and-paste the code. Instead, take the common lines and refactor them into a helper method.
A large part of this class and of your grade will be oriented around being able to detect and eliminate redundancy.
When writing code, we want to reuse much of the old code we've written as possible. We shouldn't have to constantly re-invent the wheel and rewrite the same code over and over, nor should we take old code and copy-and-paste it everywhere we need it. After all, having to both copy and maintain copy-pasted code is tedious. If we were to copy-and-paste, and later discover a bug in our code, we'd suddenly have to go back everywhere we copied-and-pasted and fix every instance of the bug which is boring and error-prone.
We call repeated code redundant code, and a large part of this class is oriented around being able to detect and eliminate redundancy.
As an example, consider the following code:
// Produces the following output: // 171717171717 // 444444444 // 00000000 public static void main(String[] args) { for (int i = 1; i <= 6; i++) { System.out.print(17); } System.out.println(); for (int i = 1; i <= 9; i++) { System.out.print(4); } System.out.println(); for (int i = 1; i <= 8; i++) { System.out.print(0); } System.out.println(); }
If we look at the expected output, we can see that there's a definite pattern and a fair bit of redundancy: we print a value a certain number of times. We consider this sort of repetition redundant and not very concise. The code in the above sample also happens to look very similar to each other, though that is not always the case – code does not need to look similar to be considered redundant. Consider:
// Produces the same output as above public static void main(String[] args) { System.out.println(171717171717); for (int i = 1; i <= 9; i++) { System.out.print(4); } System.out.println(); int i = 0; while (i < 8) { System.out.print(0); } System.out.println(); }
While this code snippet doesn't really have "repeated code," it should be clear that both blocks are redundant.
We can improve on both versions by making use of methods to eliminate redundancy like so:
public static void main(String[] args) { printNumber(4, 9); printNumber(17, 6); printNumber(0, 8); } public static void printNumber(int number, int count) { for (int i = 1; i <= count; i++) { System.out.print(number); } System.out.println(); }
This code is pretty simple, but illustrates our goal when dealing with redundancy. In many programs, we have tasks that we need to perform more than once – if there is such a task, it's important to make sure we don't write the same code for it twice.
This is usually more straightforward in 142, but as code gets more complex, we occasionally have two code blocks that are similar enough to seem redundant, but different enough to make us pause before creating a generalized function to handle both cases.
In these cases, it's helpful to think about your methods not in terms of their similar
code, but in terms of their purpose – if the code blocks are performing the same
task, such as printNumbers
above, it's probably a good idea to reduce the code.
However, if the code blocks are performing fundamentally different tasks, you should
pause before creating a generalized method. A method should stand alone and have a single
purpose.
Detecting redundancy
Sometimes, it can be hard to tell when something is worth refactoring or not. If you
have 6-8 identical lines of code, it's pretty obvious that should be refactored, but what
about 1-2 lines of code? What about expressions like 'a' + 1
? Where does the
boundary lie?
When considering this sort of question, you should ask yourself how many "operations" you'll end up saving if you refactor something into a method. If you end up with a net gain after refactoring something, it was probably the correct decision.
For example, let's say that I have the following method:
public static void printTree() { System.out.println(" ** "); System.out.println(" **** "); System.out.println("******"); System.out.println(" ** "); System.out.println(" **** "); System.out.println("******"); }
The redundancy in this method is pretty obvious: we're repeat the exact same set of printlns twice. Now, let's say we refactor that:
public static void printTree() { printTriangle(); printTriangle(); } public static void printTriangle() { System.out.println(" ** "); System.out.println(" **** "); System.out.println("******"); }
If we take a look at our original method, we were in essence performing exactly six logical operations: we had six printlns.
Now, if we look at our newly refactored printTree
method, it looks as if we've
simplified it down to just two operations: two method calls.
Of course, in the end, we still need to run six println statements, but within the
context of printTree
we've turned six "operations" into just two, which is a net
improvement. As a result, we know that this was a good refactoring to have
performed.
What if instead we tried refactoring printTree
to look like this?
public static void printTree() { myPrintln(" ** "); myPrintln(" **** "); myPrintln("******"); myPrintln(" ** "); myPrintln(" **** "); myPrintln("******"); } public static void myPrintln(String line) { System.out.println(line); }
In this case, we didn't end up reducing the logical redundancy at all. Our
printTree
method started off with six operations, and after our refactoring, it
still performs six operations. The method didn't end up getting any simpler, and we ended
up gaining a method in the process, which is a slight net loss.
We may have less redundancy if we look at the amount of literal characters that make up our code, but that sort of redundancy is incidental to what the code is actually logically doing.
Let's take another example: the Pythagorean Theorem. It looks something like this:
double sideA = 3; double sideB = 4; double hyp = Math.sqrt(sideA * sideA + sideB * sideB);
Ignoring the variable assignment, this would be about four "operations"
– we call Math.sqrt
once, we multiply two times, and we add once.
Now, let's replace this with a method call:
double sideA = 3; double sideB = 4; double hyp = pythagorean(sideA, sideB);
This is a net gain for us – we've turned four operations into just a single one again. And if we're repeating the same calculation all over our code, we've just simplified every single one of them down into a single operation.
We can even be pseudo-mathematical if we want – let's say we want to compute the hypotenuse 10 times throughout our code. That means before refactoring, we had about 40 operations scattered throughout our code in order to compute the answer. After refactoring it into a method, we call the method 10 times and have the 4 operations inside the method – we're now performing about 14 operations in total. We've saved 26 operations in total, which is fantastic.
Of course, counting the number of "operations" is a bit of a fuzzy and subjective metric. It's also sort of pointless in the sense that the computer will run 40 operations regardless or not if we refactor. Reducing the number of operations is mostly something we do for the sake of readability and concision, and so you should use the number of operation simply as a rule of thumb to help you refactor. Eventually, you'll get to the point where you do not have to explicitly compute some arbitrary number and will instinctively feel when it's appropriate to refactor.
Now, let's take this from a different perspective. Let's say that we have the following code:
int a = 1; int b = 2; int c = a + b;
Setting aside the variable assignment, we have only a single operation – the addition. We could refactor this into a method, and do something like this:
int a = 1; int b = 2; int c = add(a, b);
...but that was pretty pointless – we're still doing only one operation, so never really reduced the complexity at all. We might be doing addition all over the place, but replacing every instance of addition with our `add` method doesn't simplify the code the same way it did with the Pythagorean Theorem algorithm. There's no net gain, and using `add` methods would most likely obfuscate and confuse the code.
Now, on rare occasion, it might still be good to make a method to wrap around a single operation, especially if that operation is really confusing -- see the section on trivial methods for more discussion on this topic.
However, this is really something you need to evaluate on a case-by-case basis. Would sticking your code into a method reduce the number of "operations", reducing the redundancy? And if not, is there some other technique you could be using instead? And finally, does the advantages in your case of sticking a single operation in a method cancel out the badness of having a trivial method with only one operation?
It really depends, and being able to determine when it's appropriate and when it's not appropriate to pull a chunk of code into a separate method is an important part of what we try and teach in CSE 142 and 143.