I asked a teammate at CodeRanch if I could use a method he wrote to show how it could be written more clearly/succinctly in Java 8. He said yes. This is the original method. It is 95 lines and has a cyclomatic complexity of 27.
public static Map<Category, List<Forum>> getBetaTopForums(BestTopicsDAO bestTopicDAO, int userId, List<Topic> bestThisYear, List<Topic> secondaryBestList){ Map<Integer, Integer> ratings = bestTopicDAO.selectForumRatingsByUserId(userId); List<Integer> forumIds = new ArrayList<>(); List<Integer> ratedForumIds = CollectionCommon.getKeysSortedByValues(ratings, true); final int defaultUserId = SystemGlobals.getIntValue(ConfigKeys.BEST_TOPICS_DEFAULT_USER_ID); // Let's first add the forums that are rated above 5/10 for(Integer forumId: ratedForumIds){ Integer r = ratings.get(forumId); if(r != null && r > 5){ forumIds.add(forumId); } } // Now let's add the forums found in best topics. if(bestThisYear != null){ for(Topic topic: bestThisYear){ int forumId = topic.getForumId(); if(!forumIds.contains(forumId)){ forumIds.add(forumId); } } } if(secondaryBestList != null){ for(Topic topic: secondaryBestList){ int forumId = topic.getForumId(); if(!forumIds.contains(forumId)){ forumIds.add(forumId); } } } // If this user is not the default-rating-user, let's add the top forums // rated by the default-rating-user. if(userId != defaultUserId) { Collection<Integer> fids = getTopRatedForumIds(bestTopicDAO); for(Integer fid: fids) { if(fid != null && !forumIds.contains(fid)) { forumIds.add(fid); } } } // Let's also add some forums found in hot topics. List<Topic> tmpTopics = TopicRepository.getHottestTopics(); for(Topic topic: tmpTopics){ int forumId = topic.getForumId(); if(!forumIds.contains(forumId)){ forumIds.add(forumId); } } // Finally, let's add the forums that were rated below 6/10 for(Integer forumId: ratedForumIds){ Integer r = ratings.get(forumId); if(r != null && r < 6 && !forumIds.contains(forumId)){ forumIds.add(forumId); } } Map<Category, List<Forum>> forums = new LinkedHashMap<>(); PermissionControl pc = SecurityRepository.get(userId); final int maxTopForums = SystemGlobals.getIntValue(ConfigKeys.BETA_VIEW_MAX_TOP_FORUMS_IN_HOME); int forumsToShow = 0; // We need to remove the forums that are not visible, or rated 0/10 for(Integer forumId: forumIds){ if(pc.canAccess(SecurityConstants.PERM_FORUM, String.valueOf(forumId))){ Forum f = ForumRepository.getForum(forumId); Category c = f != null ? ForumRepository.getCategory(f.getCategoryId()) : null; if(f != null && c != null && f.isShowInForumList()){ if(++forumsToShow > maxTopForums){ break; } List<Forum> fs; if(forums.containsKey(c)){ fs = forums.get(c); }else{ fs = new ArrayList<>(); forums.put(c, fs); } fs.add(f); } } } return forums; }
First step – write unit tests
First, I wrote unit tests for the existing code to ensure I didn’t break anything. While doing that, I did some minor refactorings. (I didn’t do extract method to make the method shorter since I knew I’d be updating that later). What I did change:
- Removed BestTopicsDAO as a parameter. It comes from a factory and we have a mock framework setup for that factory already. So there was no reason it had to be a parameter
- Renamed bestThisYear to primaryBestList. (It doesn’t always represent the best for the year)
I couldn’t get 100% test coverage because a few of the null checks were unreachable due to early logic (in the helper methods.) I left them in to see the effect they’d have on conversion.
Refactoring to Java 8 – Getting started
There’s a lot of for loops here. And I suspect they are very similar. Let’s start with the first one:
for(Integer forumId: ratedForumIds){ Integer r = ratings.get(forumId); if(r != null && r > 5){ forumIds.add(forumId); } }
I converted this to a helper method and a stream:
forumIds.addAll(ratedForumIds.stream().filter(id -> { Integer r = ratings.get(id); return r != null && r > 5; }).collect(Collectors.toList()));
I’ll grant you that this code isn’t shorter than the original code. It is more similar in form to the code I expect to write next though. Also, it’s not easier to read given I didn’t extract the two line lambda. I’m going to do that after I get rid of the similar for loops.
The next refactoring
The next one is interesting. It’s just a for loop and if statement which is easy to rewrite.
for(Topic topic: primaryBestList){ int forumId = topic.getForumId(); if(!forumIds.contains(forumId)){ forumIds.add(forumId); } }
which is equivalent to
forumIds.addAll(primaryBestList.stream() .map(Topic::getForumId) .filter(f -> ! forumIds.contains(f)) .collect(Collectors.toList()));
However, it is interesting because it check if the forum is already in forumIds. This shouldn’t be necessary as it is something a data structure could do. We need a data structure that is both a list (preserves order) and a set (checks for uniqueness.) Enter LinkedHashSet. It’s a set and preserves the insertion order. The JavaDoc even specifies that if you add the same element again, the order doesn’t change. Perfect. Switching forumIds to a LinkedHashSet allows for getting rid of the filter intermediate operation. And the unit tests still work. I used the same techniques for the next five for loops.
Time to use methods
I converted two methods that had to do with ratings and three that were straight conversions. I could have one common method they all use with identities for the filter/map that don’t apply. Two methods would be clearer. Giving me:
private static void addForumIds(Collection<Integer> forumIds, List<Topic> candidates) { if (candidates != null) { List<Integer> toAdd = candidates.stream().map(Topic::getForumId).collect(Collectors.toList()); forumIds.addAll(toAdd); } } private static void addForumIds(Collection<Integer> forumIds, Collection<Integer> candidates, Predicate<Integer> predicate) { if (candidates != null) { List<Integer> toAdd = candidates.stream().filter(predicate).collect(Collectors.toList()); forumIds.addAll(toAdd); } }
The last one
The last chunk of code needed more refactoring to take care of all the forum filtering logic first. It also needed a mutable object for the counter rather than a primitive so it could be updated inside a lambda.
Conclusion
I could refactor this more. There’s duplication in the two lambdas. And I could extract more code into separate methods. Interestingly, the total code base is about the same as before. (It’s actually three lines longer, but I have more than three lines of comments.) But the method with the main logic is shorter and far less complicated:
public static Map<Category, List<Forum>> getBetaTopForums(int userId, List<Topic> primaryBestList, List<Topic> secondaryBestList){ BestTopicsDAO bestTopicDAO = DataAccessDriver.newBestTopicDAO(); Map<Integer, Integer> ratings = bestTopicDAO.selectForumRatingsByUserId(userId); Collection<Integer> forumIds = new LinkedHashSet<>(); List<Integer> ratedForumIds = CollectionCommon.getKeysSortedByValues(ratings, true); final int defaultUserId = SystemGlobals.getIntValue(ConfigKeys.BEST_TOPICS_DEFAULT_USER_ID); // Let's first add the forums that are rated above 5/10 addForumIds(forumIds, ratedForumIds, id -> { Integer r = ratings.get(id); return r != null && r > 5; }); // Now let's add the forums found in best topics. addForumIds(forumIds, primaryBestList); addForumIds(forumIds, secondaryBestList); // If this user is not the default-rating-user, let's add the top forums // rated by the default-rating-user. if(userId != defaultUserId) { Collection<Integer> fids = getTopRatedForumIds(bestTopicDAO); addForumIds(forumIds, fids, id -> id != null); } // Let's also add some forums found in hot topics. List<Topic> tmpTopics = TopicRepository.getHottestTopics(); addForumIds(forumIds, tmpTopics); // Finally, let's add the forums that were rated below 6/10 addForumIds(forumIds, ratedForumIds, id -> { Integer r = ratings.get(id); return r != null && r < 6; }); Map<Category, List<Forum>> forums = new LinkedHashMap<>(); PermissionControl pc = SecurityRepository.get(userId); final int maxTopForums = SystemGlobals.getIntValue(ConfigKeys.BETA_VIEW_MAX_TOP_FORUMS_IN_HOME); AtomicInteger countForumsToShow = new AtomicInteger(0); // We need to remove the forums that are not visible, or rated 0/10 forumIds.stream() // skip if can't access forum .filter(id -> pc.canAccess(SecurityConstants.PERM_FORUM, String.valueOf(id))) // get forum .map(ForumRepository::getForum) // skip if forum not found or can't access .filter(f -> f != null && f.isShowInForumList()) // get category .forEach(f -> addForumToCategory(forums, f, maxTopForums, countForumsToShow)); return forums; } private static void addForumToCategory(Map<Category, List<Forum>> forums, Forum f, int maxTopForums, AtomicInteger countForumsToShow) { Category c = ForumRepository.getCategory(f.getCategoryId()); if (c != null && countForumsToShow.incrementAndGet() <= maxTopForums) { List<Forum> fs; if (forums.containsKey(c)) { fs = forums.get(c); } else { fs = new ArrayList<>(); forums.put(c, fs); } fs.add(f); } } private static void addForumIds(Collection<Integer> forumIds, List<Topic> candidates) { if (candidates != null) { List<Integer> toAdd = candidates.stream().map(Topic::getForumId).collect(Collectors.toList()); forumIds.addAll(toAdd); } } private static void addForumIds(Collection<Integer> forumIds, Collection<Integer> candidates, Predicate<Integer> predicate) { if (candidates != null) { List<Integer> toAdd = candidates.stream().filter(predicate).collect(Collectors.toList()); forumIds.addAll(toAdd); } }