Refactoring Techniques: Extract Method
A while back, I’d thrown out a question in the Laravel Slack channel asking people what the “must-reads” were for devs.
Along with the obvious Uncle Bob books, someone (I believe it was Matt Stauffer) mentioned Refactoring - Improving the Design of Existing Code by Martin Fowler. I looked it up on Amazon and while it was available, I found myself questioning whether a book that was dated 1999 would have any validity in the “modern” coding world I find myself in… not to mention that it didn’t appear to be written for PHP, which is the primary language I use.
I’m glad to say that I put aside my doubts, followed the recommendation, and picked up a copy!
Initially I was disappointed to see that the examples are written in Java, a language I know nothing about. However, I’ve been surprised to see how the broader concepts, such as organization and classes, relate very similarly to what I find myself dealing with in the PHP/Laravel world.
Setting the stage for refactoring
Fowler starts out with a walkthrough on how he would refactor a program that was designed to calculate charges for renting VHS tapes (that should give context on the book's era!). Through this example, he demonstrates that by using specific techniques he is able to take a convoluted script and turn it into a much more understandable program, while making it easier to accommodate future change requests.
Throughout the illustration, Martin points out areas of the code that would constitute a Code Smell, which he defines as a technique or implementation that should cause you to pause and think “is this the best way to accomplish ____?”
Following the illustration and description of potential code smells, Fowler devotes an entire chapter to explaining why backing tests are necessary for successful refactoring. As I’ve grown in my development, I've learned just how important these tests are.
Before you proceed with any refactoring, it’s imperative you have the assurance that your changes will not break the application. Check out this article to read some thoughts on testing in Laravel.
Actual refactoring techniques
At this point in the book, Martin has laid the groundwork for why we should be refactoring. Now he takes the remainder of the book to catalogue how he goes about refactoring.
This brings us to the point of this blog entry: I want to outline some of these refactoring techniques here. Hopefully writing about these will cement them into my workflow, and at the same time offer a benefit to you, the reader, as well. Let’s kick it off with the first technique: Extract Method.
Almost all the time the problems come from methods that are too long. Long methods are troublesome because they often contain lots of information, which gets buried by the complex logic that usually gets dragged in.
- Martin Fowler, “Refactoring - Improving the Design of Existing Code”
The Extract Method is probably a technique you use on a daily basis; if not, it should be. It really isn’t any more complicated than the title would suggest. It’s looking through a method and determining what lines of code could easily be grouped together into another function.
Let’s look at this method, which assembles a CSV file from a Collection of data using the League\CSV package.
public function exportReport()
{
$csv = League\Csv\Writer::createFromFileObject(new \SplTempFileObject());
$csv->insertOne([
'Id'
'Last Name',
'First Name',
]);
foreach (App\Users::all() as $user) {
$csv->insertOne([
$user->id,
$user->lastName,
$user->firstName,
]);
}
return $csv->output('user-results.csv');
}
Do you notice how this code does basically four things? It builds the Writer object, populates the heading row, loops through the collection to build the data rows, and then sends it to the browser.
Using the Extract Method refactoring technique, we could grab those four “chunks” of code and move them into their own method, like this:
public function exportReport()
{
$csv = $this->buildCsvWriter();
$this->addHeadingsToCsv($csv);
$this->addRowsToCsv($csv);
return $this->outputCsv($csv);
}
public function buildCsvWriter()
{
return League\Csv\Writer::createFromFileObject(new \SplTempFileObject());
}
public function addHeadingsToCsv($csv)
{
$csv->insertOne([
'Id',
'Last Name',
'First Name'
]);
}
public function addRowsToCsv($csv)
{
foreach (App\Users::all() as $user) {
$csv->insertOne([
$user->id,
$user->lastName,
$user->firstName
]);
}
}
public function outputCsv($csv)
{
// Returns the CSV as a string instead of forcing a download
if (env('APP_ENV') == 'testing') {
return $csv->__toString();
}
return $csv->output('user-results.csv');
}
Here is the test that ensures we are getting the right data in our CSV file:
/** @test */
public function it_exports_a_csv_of_all_users()
{
$users = factory(App\User::class, 10)->create();
$csv = (new App\CsvExporter())->exportReport();
foreach($users as $user) {
$this->see($user->first_name, $csv);
$this->see($user->last_name, $csv);
}
}
What have we gained?
There are two specific pluses with this particular refactor. First, we’ve opened up a few methods that may be useful for other methods in the class, and second, we’ve made the higher level method much simpler to understand. By choosing descriptive method names, we’ve made it clear what each step is going to do.
Yes the refactored code is longer. BUT, the refactored code is also easier to understand and will be easier to manage over time.
Want to read more tips and insights on working with a website development team that wants to help your organization grow for good? Sign up for our bimonthly newsletter.
By Jesse Schutt
Director of Engineering
Jesse is our resident woodworker. His signature is to find the deeper meaning in a project and the right tool for the job.