8.5 Refactor - Video Tutorials & Practice Problems
Video duration:
13m
Play a video:
<v Instructor>We saw in the last section</v> how to get from red, that is to say a failing test suite to green, a passing test suite, using whatever method that worked, the result though was a little ugly, can see here, it's kind of a cumbersome for loop, it's got some repetition and this brings us to the third step in this red, green, refactor cycle, which is probably my favorite step. So, refactoring involves changing the form of the code without changing it's function, refactoring is potentially dangerous because it's easy to introduce errors in code that used to be working, these are called regressions and one of the most valuable roles played by a test suite is catching regressions immediately, having a good test suite allows us to refactor with confidence that we're not breaking our application, so let's begin. We'll start by running the test suite, just to make sure it's green. (keyboard keys clicking) All right, five passing and now let's take a look at the code, well one thing we notice is that there's some repetition, this.carAt(i) is used twice, so one thing I'd like to do is factor that out, (keyboard keys clicking) so let's call it character, (keyboard keys clicking) character.match and then here's the character, this is already clear, write if character.match, theLetters.push(character), let's run the test suite to make sure it's still passing, oh and it's not, and I see what I did, I swear I did not do this intentionally, this really goes to show how easy it is to introduce regression when refactoring, this let, I define kind of in parallel with the other one, but this has to be inside the loop because it make reference to the loop variable i, this here. (keyboard keys clicking) Ah, there we go. All right, so this is just a little bit of refactoring, eliminating some duplication by defining a new variable, it's also sometimes nice to give names to things, even if they're not strictly necessary, just to make the code easier to read, so for example, this here is a regular expression designed to match letters, so we can do something like this, this can go outside, this won't ever change, so I'm gonna call it a constant, const, let's call it letterRegex, (keyboard keys clicking) it's that and then I can use it in here, it's a little nicer, good. Now, we can refactor this regular expression, this works perfectly well, but there's also a way to do case insensitive matching, which arguably better captures what this thing is supposed to detect, we don't really care if it's an uppercase or a lowercase letter, we can just do [a-z]/i for case insensitive, does that still work? It does, all right, now let's refactor this loop, this is not as nice as it could be, remember we talked about replacing these regular for loops with forEach loops, so we can do that by saying this.content and turning it into an array, (keyboard keys clicking) which is Array.from, (keyboard keys clicking) this is an array of characters and then forEach, (keyboard keys clicking) forEach character and notice, we can get rid of this binding, and reuse this, (keyboard keys clicking) that was quite a bit of editing we just did, it's exactly the kind of thing that could easily introduce an error, (keyboard keys clicking) but it didn't because our test suite still passes. All right, this is already a significant improvement, but you might notice that there's a pattern here we've seen before, we have something that is maintaining some sort of state, in this case, an array of characters, we're going through and we're pushing to that array and then we're joining it at the end, and in this case, each character matches a particular boolean criterion, character.match(letterRegex), recalling our work on functional programing, this is exactly what filter does, let's review that quickly. (keyboard keys clicking) Let's look at Array.from our test string, (keyboard keys clicking) remember this is an array of characters like that and we can filter the characters that match the regular expression [a-z] case insensitive like this, (keyboard keys clicking) what you see is the variable just for brevity, => c.match, this is so compact we can just put it in line /i, (keyboard keys clicking) and there we go, those are the letters, (keyboard keys clicking) join on the empty string and the result is exactly what we're looking for, that means we can replace all of this stuff in here with one call to filter and copy from the Rebel, (keyboard keys clicking) Array.from(this.content) filter, match, join, this is only 70 columns, so it's even short enough, can move it over like this, (computer mouse clicking) (keyboard keys clicking) this is the kind of refactoring I would be terrified to do without a test suite, but here, (keyboard keys clicking) still passing. So, I hope you can see this is an incredibly powerful technique, get the code working however we can and then refactor it until it's as nice, and as simple to understand as possible, in this case, I really like the one-liner, although an argument can be made for retaining the letters.Regex variable, but in the case of functional programing, I find the idea of the single line so appealing that whenever it's possible, I use it, it is potentially confusing, but for anybody familiar with functional programing, this sort of line is relatively easy to understand. Using a filter like this is an excellent technique, in particular, this is the kind of construction that will work in lots of different languages that support functional programing, but there's an even shorter way to do this in JavaScript by applying match directly to this.content, let's take a look at that in the Rebel, (keyboard keys clicking) we'll use a sample string, (keyboard keys clicking) now if we call match on this with the same regex, (keyboard keys clicking) we get only the first letter, but recall from the section of regular expressions that there's a G or global flag that returns a full array, we can combine this with the case insensitive flag like this and that's exactly the array we want, this is the same as Array.from(this.content) filtered on the match, so all we have to do is join on the empty string, (keyboard keys clicking) there is one caveat here, which is that if we do Array.from(this.content).filter and there aren't any matches, the result is the empty array, which when joined on empty string, is just the empty string. (keyboard keys clicking) Do this, (keyboard keys clicking) we can look at this here and then if the content is say, all numbers, so there are no matches for letters, (keyboard keys clicking) this still works and that's because this here is the empty array, but matches behavior is just slightly different, (keyboard keys clicking) you can see, instead of returning the empty array, it returns null, which means that if we try to join it on the empty string, we get an error, (keyboard keys clicking) there's a nice trick to get around this that uses the OR operator and the fact that null is false in a boolean context, let's see how that works, we can do null or empty array and the way JavaScript works is it evaluates null, says it's false and then moves to the next thing in the list, and returns it, we can apply this to the present case like this, we can say content.match, handle the null case like this, || [].join(''), now we're ready to put this in our code, we just copy this, replace Array.from, all of this stuff with this and then change it to this.content, so return (this.content.match) on this regex, if this is null, then return the empty array and then join the result on the empty string, (keyboard keys clicking) and there we go, still passing. You might've noticed that we don't actually have a test for the case when there are no matches, such as for example, all numbers, and adding that has left us an exercise. (keyboard keys clicking) All right, that's it for Testing and Tester-Driven Development, all that's left is to publish this NPM module, now as you might imagine, I've already published a module called m.hardel-palindrome you can see the text for the series of steps necessary to make that happen in your case, but for now, I'll just show you that in fact, I can install and use m.hardel-palindrome, which I arranged by doing NPM add user and then NPM publish as described in the text, to do this, I'm gonna go back to the JS tutorial directory, (keyboard keys clicking) install the module, (keyboard keys clicking) and now let's run node, and require it, (keyboard keys clicking) remember it exports the Phrase object, (keyboard keys clicking) we saw napoleonsLament before, now let's add some punctuation, (keyboard keys clicking) ("Able was I, ere I saw Elba,"), oops, let's bind it properly and then, (keyboard keys clicking) is it a palindrome? It is. One of the great benefits of publishing an NPM module publicly is that we can install and require it like this, something we'll put to good use in a live web application, in the next chapter.