Chances are that somewhere along the line, you’ve written yourself into a big pile of spaghetti code and you’re left wondering:
Why do I write messy code?
It’s a decent question, and the answer probably has to do with our brain’s working memory and other things we can’t really help. So if you’re trying to develop clean code and you’re asking yourself that question, it’s not really the right thing to be asking yourself at all. There’s nothing wrong with writing messy code. In fact, we need to write messy code before we can write clean code. The answer to clean code development is refactoring.
Every time we finish a section of code, we should stop and ask “How can I refactor this?”. We should be asking ourselves that question continuously as we develop new code. That being said, the following is a short tutorial on how to refactor in AngularJS. First things first:
The following is a controller with a single function
createSessions(). It is part of a larger application that is responsible for managing reservations at different events throughout the year. The function takes a list of reservations, a list of events, and populates a list of available events:
At a glance, we can see a couple of unnerving things. First, the function is doing too much on it’s own. It’s trying to solve the whole problem, while working at every level of abstraction. It’s using basic functions, such as
getTime(), while also trying to solve higher level problems, such as getting the places left for each event. In order to do that, we resort to four levels of nesting (bad). Finally, there’s inconsistency in the function and variable names.
With these problems in mind, let’s get to refactoring.
WHERE TO START
Unit tests. Before touching anything, write some tests. Unless you have passing tests, you can’t be sure that the changes you make won’t break your application. For this example, we’ll be using Karma and Jasmine to run our tests. If you’re using npm, you can install both with a single command:
npm install karma-jasmine --save-dev.
To create karma’s config file, run the following:
Karma will ask you a series of questions to set up your karma.conf.js file. Once you’ve answered these, you’re ready to start testing.
Jasmine’s tests are written using nested
it() blocks. The
describe() block tells us the type of tests we’ll be running, while the
it() block tells us what we expect the code to do. We can use
beforeEach() functions to instantiate variables that will be accessible throughout the
describe() block. Here is a stripped down version of what our tests will look like:
We’ll use the
beforeEach() blocks to define our controller, scope, and the list of reservations. Inside the
it() blocks, we’ll create different event times and test that our function works as intended.
The following is one of a series of tests written for
createSessions(). In practice, we should write a test for each scenario we might encounter, but for brevity we only show one:
Now that we have our tests written, we can run them using:
This gives us the following output:
We see that the green lights are on, which means we’re ready to start cleaning up the code. As we refactor, Karma will watch for changes in our test suite and application and will rerun our tests each time we save our modifications. We’ll want to continuously be checking the tests to see that the changes we make aren’t causing the tests to fail.
Step 1: Rename any variables names that aren’t descriptive enough. This will help you decipher the code, and will make it more readable in the future. Keep in mind that it’s better to have long variable names than short ambiguous ones. For example, the function name in our example code is
createSessions(). To make this more descriptive, we could name the function
getAvailableEvents(). This name implies that we’re checking to see if the event is available, and it’s consistent with our use of the word ‘event’.
Step 2: Remove any unnecessary nesting. In this case, the majority of the function has been wrapped in an
if statement. The extra level of indenting makes the function harder to understand, and forces us to keep track of what condition we’re in throughout the function. Move the
if statement to the top of the function and use
continue to skip the event if the condition fails.
Step 3: Break the code down into smaller functions, each having a single responsibility and a single level of abstraction. In many cases, this will be the most important step in refactoring. Small functions make your code easier to test, easier to read, and easier to reuse.
Once we finished these steps, we end up with the following code:
Taking a look at our initial function, which we’ve renamed to
getAvailableEvents(), we can see that it more or less reads like a sentence. We’ve removed two levels of nesting and clarified the function by using consistent naming. As for the other functions, each works at a single level of abstraction, and does one thing. The function name says exactly what it is doing.
At this point, we can double check to make sure that our tests are still passing. If everything is still green, we’re ready to move on.
LAST BUT NOT LEAST
Our final step of cleaning up will be to move any functions that are reusable into services. Services will help us encapsulate blocks of code and will keep our controllers tidy. Looking at our four functions, there are two that are clearly reusable, namely
getPlacesLeftAtEvent(). Since the two functions are performing tasks that are quite distinct, it makes sense to create separate services for each of these functions. We’ll name these services Date and Event. Here they are:
Note that each service is returning an object that contains a function. This allows us to easily add related functions later on. On the controller side, we just need to inject these services and we’re then able to use them in our functions. Here’s the final state of our controller:
Although there is no surefire way of developing clean code, there’s a few important points to take away from this example:
Be consistent. If you use ‘update’ in one place, don’t use ‘edit’ in another.
Write short functions. They should only do one thing.
Write unit tests. They’re the safety net that gives us the ability to improve our code without the fear of introducing new bugs.
Use descriptive names. Don’t try to shorten variable and function names at the expense of losing clarity.
Keep reusability in mind. Services are your friend. Move any reusable code out of your controller and into a service.