Building a simple rule engine with Java 8 Stream
A simple case as an example:
Rule 1: if a cashier forgets to enter a price, set to $10
public static class SetToTenDollarsIfZero
implements Function<Double, Double> {
@Override public Double apply(Double in) {
double out = in == 0 ? 10D : in;
log.trace("{} -> {}", in, out);
return out;
}
}
Rule 2: Apply a 20% off anyway
public static class TwentyPercentOff
implements Function<Double, Double> {
@Override public Double apply(Double in) {
double out = in * 0.8;
log.trace("{} -> {}", in, out);
return out;
}
}
Function<Double, Double> rules = Arrays.asList(
new SetToTenDollarsIfZero(),
new TwentyPercentOff()
).stream().reduce((a, b) -> a.andThen(b)).get();
double result = Stream.of(0D).map(rules).findFirst().get();
log.info("result: {}", result);
SetToTenDollarsIfZero | 0.0 -> 10.0
TwentyPercentOff | 10.0 -> 8.0
result: 8.0
Remarks
The following lambda expression for reduce():
"(a, b) -> a.andThen(b)" is self-explained.
andThen() is a default method of the Function interface. (default method is new in Java 8, btw)
reduce() is to reduce a stream with a reducer function. (well...)
(a,b) - take two parameters as input, return one.
Improvement
map() is for one-to-one mapping.
In many cases, flatMap is more useful as that return 0, or 1 or many results. Say we want to add the 3rd rule to drop any order below $0.
Change the original rules to "implements Function<Double, Stream<Double>>", e.g.
public static class SetToTenDollarsIfZero
implements Function<Double, Stream<Double>> {
@Override public Stream<Double> apply(Double in) {
double out = in == 0 ? 10D : in;
log.trace("{} -> {}", in, out);
return Stream.of(out);
}
}
public static class DiscardIfUnder5Dollars
implements Function<Double, Stream<Double>> {
@Override public Stream<Double> apply(Double in) {
return in == null || in < 5 ? Stream.empty() : Stream.of(in);
}
}
Build the rules with a slightly different syntax:
Function<Double, Stream<Double>> rules = Arrays.asList(
new SetToTenDollarsIfZero(),
new TwentyPercentOff(),
new DiscardIfUnder5Dollars()
).stream().reduce((a, b) -> a.andThen(d -> d.flatMap(b))).get();
The way to call it is slightly different as well:
double result = Stream.of(0D).flatMap(rules).findFirst().get();
Remarks:
filter() can do dropping as same as returning an empty, but is no good for this case.
if the requirement is just to send an alert rather than do dropping, peek() may be used. peek() works like a listener that process on the course of the stream, but do not alter the return value.
If I'll write a part 2, it will cover:
Define the Rule interface with a weight/priority default method
Use service loader or other mechanism (such as Guice Multibindings) to load a list of Rules.
Sort the rules by weight/priority.