Simplify Code by Encapsulating Collections

A quick survey: Below are two methods that return school district ratings given a list of houses. Which method do you find more to the point?

Method A
public List<Rating> getDistrictRatings(List<House> houses, Price maxPrice) {
  Set<SchoolDistrict> districts = houses.stream()
      .filter(house -> house.price().isLessThan(maxPrice))
      .map(house -> house.getSchoolDistrict())
      .collect(Collectors.toSet());

  return ratingService.rateDistricts(districts);
}
Method B
public List<Rating> getDistrictRatings(Houses houses, Price maxPrice) {
  Set<SchoolDistrict>=> districts = houses.below(maxPrice)
      .getSchoolDistricts();
  return ratingService.rateDistricts(districts);
}

Notice that the logic in method A that filters and maps the collection of House objects is tangential to its primary purpose. Worse it is likely this code is duplicated in other methods. The code in method B abstracts this logic and minimizes the responsibilities of its class, making the program easier to reason about. This pays dividends whenever the code is read and especially when a developer is new to the project (see Single Responsibility Principle).

The Houses class allows the streamlined method B instead of the muddled method A. Houses encapsulates a collection of House objects and provides a central place for useful methods that operate on the collection. Here’s an example of what the Houses class could look like. By extending guava’s ForwardingCollection Houses objects can be used directly in for each loops or in other cases where a type of Iterable or Collection is needed.

import com.google.common.collect.ForwardingCollection;
import com.google.common.collect.ImmutableList;

import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Houses extends ForwardingCollection<House> {

    private final Collection<House> delegate;

    @Override
    protected Collection<House> delegate() {
        return delegate;
    }

    private Houses(Collection<? extends House> delegate) {
        this.delegate = ImmutableList.copyOf(delegate);
    }
    
    public static Houses from(Collection<? extends House> delegate) {
        return new Houses(delegate);
    }
    
    public static Houses from(Stream<? extends House> delegate) {
        return new Houses(delegate.collect(Collectors.toList()));
    }

    public Houses below(Price maxPrice) {
        return from(delegate().stream()
            .filter(house -> house.price().isLessThan(maxPrice)));
    }

    public Set<SchoolDistrict> getSchoolDistricts() {
        return delegate().stream()
                .map(House::schoolDistrict)
                .collect(Collectors.toSet());
    }
}

Encapsulating the collection in a dedicated class reduces duplication of code and more importantly splits separate responsibilities into separate classes. Encapsulated collections are also great for providing methods that create maps from lists (say a map of ZipCode -> House), and for sorting. The underlying collection can even be changed to a sorted collection type such as TreeSet without changing any of the client code (changes are much easier when data is well encapsulated!). The next time you find yourself writing code around a collection try encapsulating it in a class like Houses, I think you’ll be happy with the results.




Leave a Reply

Your email address will not be published. Required fields are marked *