Skip to content

Selection Explained

Ben Yu edited this page Mar 16, 2024 · 14 revisions

The Danger of Special Casing

Have you seen code that looks like this?

Set<CustomerId> customerWhitelist = ...;
if (customerWhiitelist.isEmpty() || customerWhitelist.contains(customerId)) {
  ...
}

The business logic requires a whitelist for customer ids, except if the whitelist is empty, it means all customers are selected.

The problem? it's easy to forget to check for the empty case, especially if this kind of check needs to be done at multiple places. And when you do forget, it won't be easy to find out the bug by reading the code.

Once upon a lifetime in a real-life horror story, there was this one production binary that used empty to mean "all". And then one day an unsuspecting engineer made some innocent change in a config file, which through some rippling effect, caused a production outage. Tech debt piles up inconspicuously just like that, because every time it seems no big deal to just add a little bit of hack, until the day nothing is innocent.

Can you just use a Predicate<CustomerId> and call test(customerId)?

Sometimes yes. You can hand-roll this predicate (which is the core of the Selection class). But repetitively implementing this predicate for diffrent projects isn't ideal. And if we set out to build a generic solution to this problem, we might as well address most of the shortcomings of the naive predicate:

  • The Predicate interface is too abstract. It no longer conveys the "it's a whitelist/blacklist" concept.
  • Occasionally we may also need to enumerate the explicit members of the whitelist/blacklist.
  • equals(), hashCode().
  • When debugging, we'd want to see useful toString().
  • If specified through a command-line flag, it helps to have a standard, unassuming syntax to represent the "all" case.
  • Some applications need to handle and compose "constraints". In addition to all(), none(), has(), we'll also need the ability to take intersection and union of multiple constraints.

Selection class is a predicate with proper abstraction and extra utilities

Using the Selection class, the above example code will become:

Selection<CustomerId> customerWhitelist = ...;
if (customerWhitelist.has(customerId)) {
  ...
}

You can create Selection objects programmatically:

Selection<CustomerId> allCustomers = Selection.all();

Selection<CustomerId> noCustomer = Selection.none();

Selection<CustomerId> whitelist = Selection.only(foo, bar);

Or parse from commmand line flags:

String flagValue = "*";  // or can be "a,b,c"
Selection<String> customerWhitelist = Selection.parser().parse(flagValue);

You can log the content of the Selection:

logger.log("customer whitelist: %s", customerWhitelist);

You can check the explicit members of a Selection:

selection.limited().ifPresent(members -> validateMembers(members));

And finally, Selection can be used as a constraint library.

For example, consider this example structured search query with AND and OR logical operators and atomic search terms:

location:washington AND time:afternoon OR location:la AND title:music

If we need to extract location constraints from such query, we can recursively analyze each subclause, with all() for non-location-constrained subclauses, and union()/intersect() away the results:

Selection<Location> extractLocation(Query query) {
  ...
  if (query = location:foo) {
    return Selection.only(foo);
  } else if (query is atomic but not location:foo) {
    return Selection.all();
  } else if (query is AND) {
    return extractLocation(query.left)
        .intersect(extractLocation(query.right));
  } else if (query is OR) {
    return extractLocation(query.left)
        .union(extractLocation(query.right));
  }
  ...
}