Spacewalkers Inspect, Replace Pump Controller Box

Extracting function logic from controllers

Let’s take a look at how we can refactor an AngularJS controller even further by extracting some some logic from the controller’s methods into separate components.

Here’s what we’ve got so far:

class Controller {
  constructor() {
    this.todos = [
      { text: "learn AngularJS", done: true },
      { text: "build an AngularJS app", done: false },
    ];
  }

  addTodo() {
    this.todos.push({ text: this.todoText, done: false });
    this.todoText = "";
  }

  remaining() {
    var count = 0;
    this.todos.forEach((todo) => {
      count += todo.done ? 0 : 1;
    });
    return count;
  }

  archive() {
    var oldTodos = this.todos;
    this.todos = [];
    oldTodos.forEach((todo) => {
      if (!todo.done) this.todos.push(todo);
    });
  }
}

Right away I can see that the remaining method has functionality that’s perfect for extracting to a separate function.

Why refactor method logic into separate functions?

You gain some advantages when you refactor the logic inside your controller’s methods into separate functions:

  1. You can reduce the amount of code within a controller, making it hopefully easier to read.
  2. You can then create functions that are more testable, as you can control the input and test for expected output, rather than relying on testing the entire controller.

In the case of the remaining method, there’s nothing being modified within the controller itself. We could easily replace this:

class Controller {
  ...  
  remaining() {
    var count = 0;
    this.todos.forEach((todo) => {
      count += todo.done ? 0 : 1;
    });
    return count;
  }
  ...
}

with this:

class Controller {
  ... 
  remaining() {
    getRemaining(this.todos);
  }
  ...
}

// Returns the count of the remaining todos.
function getRemaining(todos) {
  var count = 0;
  this.todos.forEach((todo) => {
    count += todo.done ? 0 : 1;
  });
  return count;
}

Now we have a function that can exist outside the Controller class, making it easier to read and test.

Similarly, the archive method has logic that can be extracted to a separate function.

class Controller {
  ... 
  archive() {
    var oldTodos = this.todos;
    this.todos = [];
    oldTodos.forEach((todo) => {
      if (!todo.done) this.todos.push(todo);
    });
  }
  ...
}

It’s easy to miss, but what’s happening here is the controller’s todos property is being updated by a filtered array of only the incomplete todos. So we could replace it with something like:

class Controller {
  ... 
  archive() {
    this.todos = filteredTodos(this.todos);
  }
  ...
}

// Returns todos that aren't done
function filteredTodos(todos) {
  var oldTodos = todos;
  todos = [];
  oldTodos.forEach((todo) => {
    if (!todo.done) this.todos.push(todo);
  });
  return todos;
}

What you’re left with then is a controller with methods that are easier to read:

class Controller {
  ...    
  remaining() {
    return getRemaining(this.todos);
  }

  archive() {
    this.todos = filteredTodos(this.todos);
  }
  ...
}

Also, because the newly created getRemaining and filteredTodos functions are independent, they can be tested to see if input results in expected output.

Next time we’ll make the controller a little cleaner by creating a class for the Todo itself.