Monday, December 19, 2011

Dynamic typing in rules : Traits (part 2)

In a previous post, we discussed the experimental feature called "traits", a way to combine strong and weak typing in Drools. A trait is an interface which can be attached ("donned"), even temporarily, to a fact. It is particularly suitable to model roles or temporary behaviors. Let's take, for example, the following fact model:

declare Person
  @Traitable
  code         : String
  name         : String
  address      : String
  balance      : long
  registerDate : Date
  orders       : Collection
end

declare OrderItem
  @Traitable
  itemId       : String
  price        : long
end

//new syntax!
declare trait HasDiscountApplied
  discount     : long
end


declare trait Customer
  code         : String
  balance      : long
  registerDate : Date
  orders       : Collection
end

declare trait GoldenCustomer extends Customer, HasDiscountApplied
  maxExpense   : long  
end

declare trait SeniorCustomer extends Customer, HasDiscountApplied
  wasAwarded   : boolean
end


A shop registers visitor to prepare special dedicated offers. When a visitor makes a purchase, they become Customers. For some reasons, the shop cares about Golden customers (those who spent more than a certain amount of money over the last year) and Senior customers (those who have been loyal for a certain time).
While some of the items on sale may have a discount, golden and senior customers always receive an (additional), personalized discount. Senior customers may even receive a public mention on the website, if they wish so.

The toy example is not accurate, but will serve to discuss some issues. Here, people and items are the only concrete domain entities: Customer, Golden and Senior are all statuses gained (or lost) by people according to some conditions. Likewise, being applied a discount is another transient property of customers and items.

We have seen previously that :

when
  $p : Person( ... )        // some conditions apply
  // exists Order( ... )
then
  don( $p, Customer.class ) // a Customer proxy is insert
end


The proxy will allow to write rules against the fields defined by the Customer interface, but the getters/setters will be remapped internally to the concrete fields of the wrapped core object (the Person, in this case). Notice that, as a side benefit, using interfaces allows to exploit multiple inheritance.

Now, one might wonder what happens when a core class does NOT provide the implementation for a field defined in an interface. We call hard fields those trait fields which are also core fields and thus readily available, while we define soft those fields which are NOT provided by the core class. Hidden fields, instead, are fields in the core class not exposed by the interface.
In our example, code, balance, registerDate and orders are hard fields for Customer provided by Person. GoldenCustomer adds discount (from HasDiscountApplied!) and maxExpense, which are soft fields. Eventually, name is a hidden field.

So, while hard field management is intuitive, there remains the problem of soft and hidden fields. The solution we have adopted is to use a two-part proxy.
Internally, proxies are formed by a proper proxy and a wrapper. The former implements the interface, while the latter manages the core object fields, implementing a name/value map to supports soft fields. The proxy, then, uses both the core object and the map wrapper to implement the interface, as needed. So, you can write:

when
  $sc : SeniorCustomer( $c : code, // hard getter
                        $award : wasAwarded == true // soft getter
                      )        
then
  $sc.setDiscount( ... ); // soft setter
end


The wrapper itself is exposed through the fields special getter available to all trait proxies. It is used to access soft fields as well as hard ones. The wrapper, in fact, mimics soft access to hard fields too, for uniformity.

  $sc : SeniorCustomer( $name : fields[ "name" ],
                        $code : fields[ "code"],  
                        $award : fields[ "wasAwarded" ] == true 
                      )        


The wrapper, then, provides a looser form of typing when writing rules. However, it has also other uses. The wrapper is specific to the object it wraps, regardless of how many traits have been attached to an object: all the proxies on the same object will share the same wrapper. Secondly, the wrapper also contains a back-reference to all proxies attached to the wrapped object, effectively allowing traits to see each other. To this end, we have provided the new isA operator:

  $sc : SeniorCustomer( wasAwarded == true, 
                        this isA "GoldenCustomer",
                        $maxExpense : fields[ "maxExpense" ]
                      )        


This rule would be triggered by a SeniorCustomer, but propagated only if the fact is "also" ( i.e. the same core object also has donned the trait of ) a GoldenCustomer. The only possible disadvantage is that this type of cross-access requires loose typing, although if an explicit join is always possible thanks to the core field, again common to all traits:

  $sc : SeniorCustomer( wasAwarded == true, 
                        $core : core
                      )        
  $gc : SeniorCustomer( core == $core,
                        $maxExpense : maxExpense
                      )       


Eventually, the business logic may require that a trait is removed from a wrapped object. To this end, we provide two options. The first is a "logical don", which will result in a logical insertion of the proxy resulting from the traiting operation:

then
  don( $x, // core object
       Customer.class, // trait class 
       true // optional flag for logical insertion
     )


The second is the use of the shed keyword, which causes the retraction of the proxy corresponding to the given argument type:

then
  Thing t = shed( $x, GoldenCustomer.class )


This operation returns another proxy implementing the org.drools.factmodel.traits.Thing interface, where the getFields() and getCore() methods are defined. Internally, in fact, all declared traits are generated to extend this interface (in addition to any others specified). This allows to preserve the wrapper with the soft fields which would otherwise be lost.

(Note: in addition to Thing, we also provide the class org.drools.factmodel.trait.Entity, an empty class with just an id and the data structures to make it @Traitable. We might rename it to Individual in the near future. And if this rings two bells to some of you, yes, the reason would be exactly that. Stay tuned.)