-
Notifications
You must be signed in to change notification settings - Fork 65
Selection Explained
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.
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.
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));
}
...
}