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()); } } </schooldistrict></house></house></house>
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.