• Tidak ada hasil yang ditemukan

Refactoring Legacy Code

Dalam dokumen Book Java 8 Lambdas EN (Halaman 45-48)

Having talked a bit about refactoring already, let’s look at an example of some legacy collections code that uses loops to perform a task and iteratively refactor it into a stream-

Refactoring Legacy Code | 31

based implementation. At each step of the refactor, the code continues to pass its tests, though you’ll either have to trust me on that one or test it yourself!

This example finds the names of all tracks that are over a minute in length, given some albums. Our legacy code is shown in Example 3-19. We start off by initializing a Set that we’ll store all the track names in. The code then iterates, using a for loop, over all the albums, then iterates again over all the tracks in an album. Once we’ve found a track, we check whether the length is over 60 seconds, and if it is the name gets added to a Set of names.

Example 3-19. Legacy code finding names of tracks over a minute in length

public Set<String> findLongTracks(List<Album> albums) { Set<String> trackNames = new HashSet<>();

for(Album album : albums) {

for (Track track : album.getTrackList()) { if (track.getLength() > 60) {

String name = track.getName();

trackNames.add(name);

} } }

return trackNames;

}

We’ve stumbled across this code in our code base and noticed that it has a couple of nested loops. It’s not quite clear what the purpose of this code is just from looking at it, so we decide to undertake our refactor. (There are lots of different approaches to re‐

factoring legacy code for using streams—this is just one. In fact, once you are more familiar with the API itself, it’s pretty likely that you won’t need to proceed in such small steps. It serves educational purposes here to go a bit slower than you would in your professional job.)

The first thing that we’re going to change is the for loops. We’ll keep their bodies in the existing Java coding style for now and move to using the forEach method on Stream. This can be a pretty handy trick for intermediate refactoring steps. Let’s use the stream method on our album list in order to get the first stream. It’s also good to remember from the previous section that our domain already has the getTracks method on the album, which provides us a Stream of tracks. The code after we’ve completed step 1 is listed in Example 3-20.

Example 3-20. Refactor step 1: finding names of tracks over a minute in length

public Set<String> findLongTracks(List<Album> albums) { Set<String> trackNames = new HashSet<>();

albums.stream()

.forEach(album -> { album.getTracks() .forEach(track -> {

if (track.getLength() > 60) { String name = track.getName();

trackNames.add(name);

} });

});

return trackNames;

}

In step 1, we moved to using streams, but we didn’t really get their full potential. In fact, if anything the code is even less pretty than it was to begin with—d’oh! So, it’s high time we introduced a bit more stream style into our coding. The inner forEach call looks like a prime target for refinement.

We’re really doing three things here: finding only tracks over a minute in length, getting their names, and adding their names into our name Set. That means we need to call three Stream operations in order to get the job done. Finding tracks that meet a criterion sounds like a job for filter. Transforming tracks into their names is a good use of map. For the moment we’re still going to add the tracks to our Set, so our terminal operation will still be a forEach. If we split out the inner forEach block, we end up with Example 3-21.

Example 3-21. Refactor step 2: finding names of tracks over a minute in length

public Set<String> findLongTracks(List<Album> albums) { Set<String> trackNames = new HashSet<>();

albums.stream()

.forEach(album -> { album.getTracks()

.filter(track -> track.getLength() > 60) .map(track -> track.getName())

.forEach(name -> trackNames.add(name));

});

return trackNames;

}

Now we’ve replaced our inner loop with something a bit more streamy, but we still have this pyramid of doom in our code. We don’t really want to have nested stream operations;

we want one simple and clean sequence of method calls.

What we really want to do is find a way of transforming our album into a stream of tracks. We know that whenever we want to transform or replace code, the operation to use is map. This is the more complex case of map, flatMap, for which the output value is also a Stream and we want them merged together. So, if we replace that forEach block with a flatMap call, we end up at Example 3-22.

Refactoring Legacy Code | 33

Example 3-22. Refactor step 3: finding names of tracks over a minute in length

public Set<String> findLongTracks(List<Album> albums) { Set<String> trackNames = new HashSet<>();

albums.stream()

.flatMap(album -> album.getTracks()) .filter(track -> track.getLength() > 60) .map(track -> track.getName())

.forEach(name -> trackNames.add(name));

return trackNames;

}

That looks a lot better, doesn’t it? Instead of two nested for loops, we’ve got a single clean sequence of method calls performing the entire operation. It’s not quite there yet, though. We’re still creating a Set by hand and adding every element in at the end. We really want the entire computation to just be a chain of Stream calls.

I haven’t yet shown you the recipe for this transformation, but you’ve met one of its friends. Just as you can use collect(toList()) to build up a List of values at the end, you can also use collect(toSet()) to build up a Set of values. So, we replace our final forEach call with this collect call, and we can now delete the trackNames variable, arriving at Example 3-23.

Example 3-23. Refactor step 4: finding names of tracks over a minute in length

public Set<String> findLongTracks(List<Album> albums) { return albums.stream()

.flatMap(album -> album.getTracks()) .filter(track -> track.getLength() > 60) .map(track -> track.getName())

.collect(toSet());

}

In summary, we’ve taken a snippet of legacy code and refactored it to use idiomatic streams. At first we just converted to introduce streams and didn’t introduce any of the useful operations on streams. At each subsequent step, we moved to a more idiomatic coding style. One thing that I haven’t mentioned thus far but that was very helpful when actually writing the code samples is that at each step of the way I continued to run unit tests in order to make sure the code worked. Doing so is very helpful when refactoring legacy code.

Dalam dokumen Book Java 8 Lambdas EN (Halaman 45-48)