M. David Green

Existence is a byproduct of semantics

Refactoring Javascript: Extracting a Function or Method

If you’re looking at a function or method that seems to be getting too long for the task it’s trying to accomplish, and it contains subsets of related variables and methods, or blocks of code that need extensive commenting to clarify how they get the job done, you may need to extract one or more functions or methods.

For example

Given a Person constructor which takes a data object with title, first_name, and last_name attributes:

function Person(data) {
    this.title = (data.title) ? data.title : null;
    this.first_name = (data.first_name) ? data.first_name : null;
    this.last_name = (data.last_name) ? data.last_name : null;
};
//Return an array of lines for a formatted address
Person.prototype.formatted_address = function() {
    var result = [],
        full_name = [];
    //format the name line
    if (this.title) full_name.push(this.title);
    if (this.last_name) full_name.push(this.first_name);
    if (this.last_name) full_name.push(this.last_name);
    result.push(full_name.join(" "));
    //...subsequent elements generated here
    return result;
}

Which might be used this way:

var person = new Person({
    "title" : "Mr."
    , "first_name" : "David"
    , "last_name" : "Green"
});
person.formatted_address() // ["Mr. David Green"]

You can see how a method like this could grow, as more lines get added to the address, and the formatting requirements become more extensive. And if any other part of your code required a single element of a formatted address, such as the name, there would be additional work to either reconstruct the code or pass along more information than is needed.

A Jasmine test to make sure this is working might look like this:

describe("a person", function() {
    it("should have a full name in the formatted address", function() {
        var person = new Person({
            "title" : "Mr."
            , "first_name" : "David"
            , "last_name" : "Green"
        });
        expect(person.formatted_address()).toContain("Mr. David Green");
    });
});

Considerations:

The practice of extracting methods involves careful attention to naming. You want each new method you extract to be obvious about what it does with the parameters you pass into it, and what it will return. Frequently, the time spent considering the best name for a newly extracted method will be the most valuable time spent in this process. The rest is just the mechanics of moving the code around.

When you are finished, your goal is to see code that invokes this method read as clearly as possible without altering the way it behaves. Remember, refactoring is not about making it work; it’s about making the code readable and convenient to a human developer who needs to use or modify it. If there’s no clearly apparent method for the task, the next developer to touch your code might just feel the need to re-invent that functionality.

The highlighted code above could be refactored into:

function Person(data) {
    this.title = (data.title) ? data.title : null;
    this.first_name = (data.first_name) ? data.first_name : null;
    this.last_name = (data.last_name) ? data.last_name : null;
};
//Return an array of lines for a formatted address
Person.prototype.formatted_address = function() {
    var result = [];
    result.push(this.full_name());
    //...
    return result;
}
//Return a formatted full name
Person.prototype.full_name = function() {
    var result = [];
    if (this.title) result.push(this.title);
    if (this.last_name) result.push(this.first_name);
    if (this.last_name) result.push(this.last_name);
    return result.join(" ");
}

By following these steps:

  1. Choose a name for your new function or method that describes what it actually does.
  2. If you are using test-driven development, this is when you would define your first test parameters with an expected return value, and create your first test.
  3. Create a named function or a method on the appropriate constructor, and copy over the part of the original function that did that particular task.
  4. Declare a local variable of the appropriate type for the return value, along with any local variables needed by the code.
  5. If any non-local variables are needed by the code, pass them in as parameters, making sure they are necessary, and that the way you pass them in is consistent and logical.
  6. Test your new method with sample parameters that give a known result.
  7. Delete the duplicated code from your original function and insert in its place a call to this new function or method.
  8. Test your original function to make sure it still behaves the same way as the original.
  9. Eliminate any local variables in the original function that you are certain are needed only for this code.
  10. If you have a test suite, run it to make sure you didn’t break anything that might have relied on the missing local variables.

Extracting the full_name function eliminated the need for a full_name array as a local variable in the original function, and made the format_address function less likely to become a nest of complex formatting code blocks.

Other code may also need access to a formatted full name. Now that will be available in a consistent manner. And if we ever decide to add suffixes or otherwise modify the format of a full name, it can be done in a single place and it will apply wherever this method is called.