Refactoring: Replace Loop with Collection Closure using Lodash

Brett Cassette
InstructorBrett Cassette
Share this video with your friends

Social Share Links

Send Tweet
Published 10 years ago
Updated 5 years ago

Logic often gets added to our loops. We need to iterate over a collection, and get the sum of values, for instance. This approach can lead to complicated methods, that are hard to test. Using Lodash (Underscore), we are going to extract logic in a large loop body into well named methods. The result will be cleaner and easier to understand.

Narrator: [00:00] Here's a pattern you've probably noticed in your own code. You have a loop that you're using to add up to a total amount. Here we have a customer in our video rental store. For each of the rentals that customer has, we're adding the charge for that rental to the total amount so that we can spit out the result using that total amount.

[00:22] Well, there's a simple way for us to do this and it's a nice refactoring for us to introduce. It's called the replace loop with collection closure method.

[00:30] Let's see how this looks in a terminal first. I've tried to recreate as simply as possible this setup that we have in our actual application. We have two movies with a title and a price and an array of movies, like a customer has an array of movies.

[00:45] We're going to use the Lodash library. It's very similar to Underscore. It's usually just an Underscore and a dot, but in my terminal I have it aliased as underscorelo, because underscore is taken to mean the value of the previous return, so we see it was undefined previously. In order to not override that, I have it as lo.

[01:06] We're going to use Lodash and we're going to wrap movies. What we actually want to call here is lodash.map. What map does it it's going to take a list of something and transform into a list of something else. We have a list of movies and we want to instead return movie.price, so we're going to now have an array of 5 and 10. That seems pretty simple, right? This is how we're going to sum up or get a sum of our group of movies.

[01:42] I need to perform a bit more work here, so what we're actually going to do is chain the movies, which means that we can continue to call methods on it in order. The first thing we're going to call is map, as you saw previously.

[01:57] Then the next thing that we're going to call is called inject or collect. There are a number of names for the same thing. This takes one variable that's an accumulator. This is a number that we're going to keep counting up with, so that's the sum.

[02:15] Then it's going to take each current number, so maybe we'll call that the cost. Here we just want to return sum plus cost, and that's the function we'll use to add it up. Then we need to call value because we called chain on this in the first place, and so this should return 15, which is the cost, the value of all the movies add up, and that's what we want to do.

[02:39] We're going to do this same thing in here, so we'll say a customer has a charge method, and instead of adding these up in-line here we'll use our collection closure method, so we'll chain up this.rentals and we'll call map.

[03:00] We're going to map each rental to this.charge, which is the method that we previously factored out, and by this, I meant rental.charge. Then we'll just inject it the same way we just saw so we'll use sum and maybe charge, return sum plus charge. Now we can call value on this. It looks very similar to what we just did. We can remove the total amount here. We don't need to keep adding it up. We can just call this .charge, and we see that our test pass again.

[03:38] We know that we can do the same exact type of thing here for frequency renter points. We can say that a customer has frequent renter points, which makes a lot of sense in our domain, I think. This is going to pull out this method. We're also using rentals.

[03:55] We're mapping instead to rental.frequent renter points. Then we're injecting and we want to say points+=, how about total points+=points, so I have total points and points. That seems to make sense, right?

[04:21] As we see this going on, we can pull out the frequent renter points. We don't need to add them up anymore. We have no price code method, and we can also call this.frequent renter points. Again, this should just work.

[04:40] Now the next thing to notice is that we actually duplicated a ton of code between charge and frequent renter points. This should be a pretty big code smell for us because it means that if we want to make a change, it'll actually be fairly hard, because we'd have to make the change in two locations for this same algorithm.

[04:59] The other reason that this should raise a few red flags is because it's actually really hard to tell the difference between these two methods. This method, actually the only difference is that it uses charge and this one uses frequent renter points, so I probably think we want to simplify this.

[05:15] This is actually is code that might belong on the rentals object because it's computing the sum of the rentals, so we could say this.rentals.sum and we can get the sum of a particular attribute. We could pull this off, and instead of this.rentals, we'll just do this. So it's still rental.

[05:37] We can also use dynamic dispatch as we've seen in the previous cases where we'll look up that attribute and call that method in both locations. Now we can say return this.rentals.sum of the frequent renter points. Here we can do the same thing except instead of frequent rental points, we use charge. Again, this is another really good reason to have tests because we can be absolutely certain that this same code works.

[06:08] This is what a replace loop with collection closure method type of refactoring always looks like. It will usually contain a map and will always contain an inject or collect to add these all up. We already see that our domain is getting much more readable and manageable.

[06:26] This is really understandable, right? We understand, it's almost like natural English that the charge for a customer is the sum of the charges of each of their rentals. It might sound a little bit like talking to a computer, but that's exactly what it is. It makes a lot more sense and it allows us to dive into the details of these smaller methods when we need them.

wojciech
wojciech
~ 9 years ago

I'm not sure if I missed something obvious here, but why the use of _.chain here? Wouldn't _.reduce(rentals, function(sum,renal){return sum + rental.charge()}) produce the sum?

Markdown supported.
Become a member to join the discussionEnroll Today