I will bite... :) follow my thoughts:
(1) Is this a good problem for rules?
Yes, I would prefer to solve this with rules instead of with an algorithm.
Reasons are because it is well defined problem, it is expressed as a set of
rules by business, it will be easier to implement and maintain long term,
and it is likely that the rules might change over time. The drawbacks are
that the declarative solution will be heavier than the algorithmic solution,
but since it is not a critical path problem and results can even be cached
and only recalculated when data changes, that will probably not be a problem
for your application.
(2) How you would do it?
There is only one thing that is tricky in this problem for a declarative
solution to deal with, and that is the recursiveness of departments, as you
can have an arbitrary number of departments in the hierarchy, and you need
to define relationships between managers and employees in different levels
of the hierarchy. To solve that the best way I know of is by calculating the
transitive closure, either for departments or for employees/managers. Since
you will have a lot less departments than employees, best is to do it for
departments. The closure can easily be calculated with 2 rules, but could
also be calculated in an external procedure and asserted as data into the
working memory. I am doing it in the rules.
Other than that, there are several strategies to model the solution on a
forward chaining engine. In my case, I am opting for finding all eligible
approvers first and then eliminating the "false positives" and finally
electing the best (nearest manager) among the remaining approvers. This
could be done in several other ways, including finding conflicts first and
then the actual approvers or taking incremental approaches.
Also, just to simplify the example, I am using salience as a conflict
resolution, but on a real application I would prefer to use ruleflow-groups.
Let me know what do you think... :)
Cheers,
Edson
-------
With your example dataset, my result was:
Employee: Mike Approver: Hilary
Employee: John Approver: Hilary
Employee: Kate Approver: Hilary
Employee: Janet Approver: Jessica
Employee: Jessica Approver: Kate
Employee: Jane Approver: Erica
Employee: Erica Approver: Kate
Domain model (pseudo-code):
Person { name }
Department { name, parent, managers, employees }
Approver { employee, manager, distance }
DepartmentHierarchy { parent, sub, distance }
Rules (actual code):
============
package org.drools.approver
dialect "mvel"
/*********************************************************
* Calculate the transitive closure for the Departments
*********************************************************/
rule "Create direct department hierarchy"
when
$d : Department( parent != null )
not( DepartmentHierarchy( parent == $d.parent, sub == $d ) )
then
insert( new DepartmentHierarchy( $d, $d, 0 ) );
insert( new DepartmentHierarchy( $d.parent, $d, 1 ) );
end
rule "Create transitive department hierarchy"
when
$dh1 : DepartmentHierarchy( )
$dh2 : DepartmentHierarchy( parent == $dh1.sub )
not( DepartmentHierarchy( parent == $dh1.parent, sub == $dh2.sub ) )
then
insert( new DepartmentHierarchy( $dh1.parent, $dh2.sub, $dh1.distance +
$dh2.distance ) );
end
/*********************************************************
* Business Rules
*********************************************************/
rule "1. Define eligible approvers"
@doc( "An eligible approver must be a direct or indirect manager of the
Person" )
when
// there is a person
$e : Person( )
// that is either an employee or a manager in department d1
$d1 : Department( employees contains $e || managers contains $e )
// and there is a department d2 in which that employee is not a manager
of
$d2 : Department( managers not contains $e )
// and the department d2 is a parent department of the department d1 in
the hierarchy
$dh : DepartmentHierarchy( parent == $d2, sub == $d1 )
// and there is a manager on the department d2
$m : Person( this memberOf $d2.managers )
then
// this manager is an eligible approver for the employee
insert( new Approver( $e, $m, $dh.distance ) );
end
rule "2.a. Approver cannot be himself"
@doc( "An approver cannot be the employee himself" )
when
$a : Approver( employee == manager )
then
retract( $a );
end
rule "2.b. Approver cannot be a peer"
@doc( "An approver cannot be a peer of the employee" )
when
$a : Approver()
Department( employees contains $a.employee, employees contains
$a.manager ) or
Department( managers contains $a.employee, managers contains
$a.manager )
then
retract( $a );
end
rule "3.a. Approver cannot report to the employee"
@doc( "An approver cannot report to the employee" )
when
$a : Approver()
$dh : DepartmentHierarchy()
$d1 : Department( this == $dh.parent, managers contains $a.employee )
$d2 : Department( this == $dh.sub, managers contains $a.manager ||
employees contains $a.manager )
then
retract( $a );
end
rule "3.b. An approver cannot be the peer to someone who reports to the
manager"
@doc( "An approver cannot be the peer to someone who reports to the
manager" )
when
// An approver
$a : Approver()
// That is a manager of a department
$d : Department( managers contains $a.manager )
// Cannot have a peer
$p : Person( this memberOf $d.managers, this != $a.manager )
// That reports to the employee in another department
$dh : Department( managers contains $a.employee, employees contains $p
)
then
retract( $a );
end
rule "Approvers are the nearest managers"
@doc( "From all eligible approvers, the actual approver is the nearest
manager in the hierarchy" )
salience -10
when
$a : Approver()
exists( Approver( employee == $a.employee, distance < $a.distance ) )
then
retract( $a );
end
/*******************************************************
* Define the dataset
*******************************************************/
rule "Setup"
salience 100
then
hilary = new Person( "Hilary" );
john = new Person( "John" );
jane = new Person( "Jane" );
mike = new Person( "Mike" );
kate = new Person( "Kate" );
jessica = new Person( "Jessica" );
janet = new Person( "Janet" );
erica = new Person( "Erica" );
a = new Department( "A", null );
a1 = new Department( "A1", a );
a12 = new Department( "A12", a1 );
a2 = new Department( "A2", a );
a21 = new Department( "A21", a2 );
a22 = new Department( "A22", a2 );
a.managers.add( hilary );
a1.managers.add( john );
a1.managers.add( jane );
a12.managers.add( mike );
a12.employees.add( john );
a12.employees.add( mike );
a2.managers.add( kate );
a21.managers.add( jessica );
a21.employees.add( janet );
a22.managers.add( erica );
a22.employees.add( jane );
insert( hilary );
insert( john );
insert( jane );
insert( mike );
insert( kate );
insert( jessica );
insert( janet );
insert( erica );
insert( a );
insert( a1 );
insert( a12 );
insert( a2 );
insert( a21 );
insert( a22 );
end
/*********************************************************
* Printing the result
*********************************************************/
// in a real application, this would probably be a query instead
// that would be invocated on demand by the application
rule "Print all"
salience -20
when
$a : Approver()
then
System.out.println( "Employee: "+$a.employee.name+" Approver: "+$
a.manager.name );
end
--
Edson Tirelli
JBoss Drools Core Development
JBoss by Red Hat @
www.jboss.com