Skip to main content

Section 6.4 Function Size and the Introduce Method refactoring

Our next refactoring is in many ways similar to the Introduce Variable refactoring. To understand its usefulness, let’s first discuss function size.

Subsection 6.4.1 Function size

When we start out in programming, we tend to write long functions. We think of the problem we have, then we come up with a list of specific steps needed to solve the problem, then we add each of these steps into the function’s body. And as long as our code works and solve the problem, we think that we are done.
But nothing could be further from the truth. As we saw earlier, the most important property that software must have is that it should be flexible enough to be adjusted to solve our future needs. This means that sooner or later we are going to have to go back to that function, and understand what it does. Even worse, we might need to modify it in some way, so that it still does its old job but now also does something slightly different. Or otherwise, we might find ourselves creating another function that is almost identical to the first but that does something a bit different. The bottom line is this:
Long functions that do many things are hard to understand, test, revise and reuse. Your functions should do one and only one thing, and they should do it well.
So what does doing one thing mean anyway? It means that your function will only do one part of its work, and it will delegate the rest of the needed work to other functions and object methods. It will collaborate with other parts of the program in order to achieve its objectives.
The mechanism for achieving this is the Introduce Method refactoring, also called the Extract Method refactoring.

Subsection 6.4.2 The Extract Method refactoring

The goal of the Extract Method refactoring is simple:
Extract Method: Find a part of a function/method which performs a clearly identifiable task, and replace it with a call to a another (new) function dedicated to that task.
This has two benefits:
  • The new function has a concrete objective to it, which can be modified and tested independently of the original function that uses it.
  • The new function has a name, which describes what it does. And using that name instead of the whole bit of code makes understanding the original function easier.
Names are signposts that help us find our way in our code’s jungle.
I like to think of refactorings as solutions to code smells, and in this case the code smell is clear: A long function, which does more than one thing. We can eliminate that smell with judicious use of Introduce Method refactorings.
Before we discuss the mechanics of the refactoring, let’s look at a small example from earlier, our "calculate total" function. Here it is again:
int total = 0;
for (int i = 0; i < input parts.length; i++) {
  total += parts[i].pricePerItem * parts[i].quantity;
}
Earlier we improved on this function’s readability by introducing some explanatory variables. Now we will simplify it by extracting a method.
"How is this function long" I hear you asking. And I will agree with you, it is not "long". But it does do more than one thing.
Let’s think of what this function does:
  • It calculates the total price of each part.
  • It loops over all parts and adds that total price of each part to the overall total.
It is that first thing, the calculation of the total price of each part, that we can extract into a method. Let’s see how it would look:
  int total = 0;
for (int i = 0; i <parts.length; i++) {
  total += calculatePartTotal(parts[i]);
}

// Somewhere else
int calculatePartTotal(OrderPart part) {
  return part.pricePerItem * part.quantity;
}
Compare this with our example from earlier, where we extracted explanatory variables. You should see some similarities: In both cases we get a new name that describes to us what is going on. And that’s the important bit.
Later we will improve on this even further, when we discuss "Tell, don’t ask".
Let’s discuss some common targets for method extractions, (i.e. when should we do it):
  • The bodies of loops or conditionals
  • Whole loops when they are not the entire function
  • the test part in a conditional or while loop
  • complex computations
  • bodies of try-catch blocks
A thorough extraction process will typically leave you with functions that are 3-4 lines long.

Subsection 6.4.3 Mechanics of Introduce Method refactoring

  • Identify the part of the code that you would like to extract as a separate function.
  • Look at the local variables and parameters whose value changes in that piece of code. If there is more than one value "produced" from this function, you will not be able to extract method, as methods can only return one thing.
  • Copy this piece of code and paste it as the body of a new function, with an appropriate return statement, if it is meant to return something.
  • Introduce parameters to your new function as needed, based on what the extracted code needs in order to execute correctly.
  • Replace the piece of code in the original method with a call to this new function, passing appropriate arguments.
In our example from earlier, we identified the code parts[i].pricePerItem * parts[i].quantity as the piece we wanted to extract. This piece of code results in a value we need, so it has that return value. Luckily for us it doesn’t try to make any other changes.
As we extract the function, we could have extracted it as is, and then we would have needed two parameters to it, like so:
  int total = 0;
for (int i = 0; i <parts.length; i++) {
  total += calculatePartTotal(parts, i);
}

// Somewhere else
int calculatePartTotal(OrderPart[] parts, int i) {
  return parts[i].pricePerItem * parts[i].quantity;
}
But we notice that we never use the parameters parts and i separately. So we can fold them into one, by instead passing parts[i]. This would then result in the original solution.