Budzu
Budzu

Reputation: 183

Rule engine to filter multiple input objects based on multiple criteria

I want to design a rule engine to filter incoming objects as follow:

At the beginning, I have three different classes: A, B, C. The list of classes is dynamic, i.e: I wanna extend this to work with class D, E, etc if D and E will be added later.

public class A {
   int a1;
   String a2;
   boolean a3;
   Date a4;
   List<String> a5;
   ...
}

public class B {
   String b1;
   boolean b2;
   Date b3;
   long b4;
   ...
}

public class C {
   String c1;
   boolean c2;
   Date c3;
   long c4;
   ...
}

There will be different objects of class A, B, or C that are gonna be filtered by my rule engine.

The users can define different rules based on a set of predefined operations that each member variable of a class can possibly have.

An example of some operations:

Some example rules for an object of class A would be:

One bullet is one rule. In a rule, there can be one criterion or more criteria (multiple criterion's that AND's together).

Each criterion is defined by a member variable with an operation that I can apply on that variable.

The rule engine must be able to process rules defined by users for each object of class A, B, or C. For every rule (A Rule, B Rule, or C Rule) coming in, the return will be a list of objects that matched the specified rule.

I can create Criterion, Criteria, ARule, BRule, CRule, Operation objects, etc; and I can go with the Brute Force way of doing things; but that's gonna be a lot of if...else... statements.

I appreciate all ideas of any design patterns/design method that I can use to make this clean and extendable.

Thank you very much for your time.

Upvotes: 0

Views: 2379

Answers (1)

fps
fps

Reputation: 34460

Sounds like a rule is actually a Predicate that is formed by and-ing other predicates. With Java 8, you can let the users define predicates for properties:

Predicate<A> operationA1 = a -> a.getA1() >= 10;           // a1 is int
Predicate<A> operationA2 = a -> a.getA2().startsWith("a"); // a2 is String
Predicate<A> operationA3 = a -> a.getA3(); // == true;        a3 is boolean

Predicate<A> ruleA = operationA1.and(operationA2).and(operationA3);

Now you can stream your List<A>, filter and collect to a new list:

List<A> result = listOfA.stream()
    .filter(ruleA)
    .collect(Collectors.toList());

You can use similar approaches for B and C.

Now, there are several ways to abstract all this. Here's one possible solution:

public static <T, P> Predicate<T> operation(
        Function<T, P> extractor, 
        Predicate<P> condition) {

    return t -> condition.test(extractor.apply(t));
}

This method creates a predicate (that represents one of your operations) based on a Function that will extract the property from either A, B or C (or future classes) and on a Predicate over that property.

For the same examples I've shown above, you could use it this way:

Predicate<A> operation1A = operation(A::getA1, p -> p >= 10);
Predicate<A> operation2A = operation(A::getA2, p -> p.startsWith("a"));
Predicate<A> operation3A = operation(A::getA3, p -> p); // p == true ?

But, as the method is generic, you can also use it for instances of B:

Predicate<B> operation1B = operation(B::getA1, p -> p.startsWith("z"));
Predicate<B> operation2B = operation(B::getA2, p -> !p); // p == false ?
Predicate<B> operation3B = operation(B::getA3, p -> p.before(new Date()));

Now that you have defined some operations, you need a generic way to create a rule out from the operations:

public static <T> Predicate<T> rule(Predicate<T>... operations) {
    return Arrays.stream(operations).reduce(Predicate::and).orElse(t -> true);
}

This method creates a rule by and-ing the given operations. It first creates a stream from the given array and then reduces this stream by applying the Predicate#and method to the operations. You can check the Arrays#stream, Stream#reduce and Optional#orElse docs for details.

So, to create a rule for A, you could do:

Predicate<A> ruleA = rule(
    operation(A::getA1, p -> p >= 10),
    operation(A::getA2, p -> p.startsWith("a")),
    operation(A::getA3, p -> p));

List<A> result = listOfA.stream()
    .filter(ruleA)
    .collect(Collectors.toList());

Upvotes: 4

Related Questions