All well and good, save for a limitation: a fact, being an object, can have only one type hierarchy. Imagine you're building a business management application (policies? mortgages?): you'll likely need rules for employees and customers, but what if you want to write rules for special classes of customers? A few days ago, I was discussing a data validation application: what if you wanted to "tag" some facts as "invalid" or "inaccurate" and then write specific rules for them? In another example, we had a message-driven application, where we had to apply rules to the members of an organization according to the role they had in a message: sender vs recipient vs subject etc.... Take conan's adventure game: right now he has "hero" vs "monster", but soon he'll add classes, say fighter, mage, ... vs orc, undead, ...: what if he wants a fighter/mage to face an undead orc?
All these applications - and many more - have a common problem: the same fact (customer, employee, data sample, character) may have multiple dynamic types which do not fit naturally in a class hierarchy. Arguably, there are many possible solutions one could adopt, including:
- Fit all classes in a hierarchy
- Use labels (strings, enums, etc...) to model types
- Write rules using interfaces
- Use proxy facts
The first method is definitely not recommended. Java does not have multiple inheritance, so fitting a complex hierarchy in a simpler one might result in unnatural "isA" relations between classes.
declare Customer // fields here end declare GoldenCustomer extends Customer // more fields here end
Even then, there is another problem here: imagine you have a Customer you want to promote to GoldenCustomer (which extends Customer). Probably, you'll have to clone your existing fact into an instance of the subclass, with a considerable effort to maintain consistency.
It seems much easier to model roles with strings or other marker objects:
declare Customer roles : Collection // more fields here end
With this solution, one can add multiple types to a fact, but unfortunately those will just be labels, not types, so a change in type will not support a change in behavior. This means that any field which is relevant only for GoldenCustomers will have to moved up to Customer and constantly checked for consistency.
Roles are indeed modeled better using interfaces, which define the fields which should be visible when an object is observed from the point of view of that interface.
declare CustomerImpl implements Customer // interface impl here end
Forgetting that Drools does not support implements, which makes the previous snippet illegal, the problem here is that an interface is attached to a class and not to individual objects. So, our GoldenCustomer can't be an interface unless we want either all Customers to be golden ones, or we provide an implementation class for GoldenCustomers only, effectively going back to the first solution discussed.
A much better solution would be the use of proxy facts:
declare GoldenCustomer customer : Customer // more fields here end
The idea is to create an additional fact, modelling the role that an object would have in the particular context. The obvious advantages of wrapping/decorating against cloning is that information is not replicated and, moreover, the role is applied to a particular fact (not to the entire class) and can be removed as needed. The price to pay is that, in order to write the rules, the user must refer explicitly the inner customer to access its fields. This also implies that the status of GoldenCustomer can be applied to instances of Customer only (think of more generic roles, which do not have a single domain, like Sender...)
In order to get the best of all these solutions, Drools now offers an experimental feature : traits
A trait is an interface that can be applied (and eventually removed) to an individual object at runtime.
To create a trait out of an interface, one has to add a @format(trait) annotation to its declaration in DRL:
declare trait GoldenCustomer // fields will map to getters/setters code : String balance : long discount : int end import LegacyInterface; declare trait LegacyInterface end
In order to apply a trait to an object, we provide the new don keyword, which can be used as simply as this:
when $c : Customer() then GoldenCustomer gc = don( $c, Customer.class ); end
A few important remarks here. First of all, when a core object dons a trait, a proxy class is created on the fly (one such class will be generated lazily for each core/trait class combination). The proxy instance, which wraps the core object and implements the trait interface, is insert-ed automatically and will possibly activate other rules.
An immediate advantage of declaring and using interfaces, getting the implementation proxy for free from the engine, is that multiple inheritance hierarchies can be exploited when writing rules. The core classes, however, need not implement any of those interfaces statically, also facilitating the use of legacy classes as cores.
In fact, any object can don a trait. For efficiency reasons, however, one can add the @Traitable annotation to a declared bean class to reduce the amount of glue code that the compiler will have to generate. This is optional and will not change the behavior of the engine:
declare Customer @Traitable code : String balance : long end
Since the only connection between core classes and trait interfaces is at the proxy level, a trait is not specifically tied to a core class. This means that the same trait can be applied to totally different objects. (The problem of filling the LHS of a donning rule will be the topic of a next post...)
Notice that, for this reason, the trait does not transparently expose the fields of its core object. So, when writing a rule using a trait interface, only the fields of the interface will be available, as usual.
However, any field in the interface that corresponds to a core object field, will be mapped by the proxy class.
when $o: OrderItem( $p : price, $code : custCode ) $c: GoldenCustomer( code == $code, $a : balance, $d: discount ) then $c.setBalance( $a - $p*$d ); end
In this case, the code and balance would be read from the underlying Customer object. Likewise, the setAccount will modify the underlying object, preserving a strongly typed access to the data structures.