2011-11-16 Logic Planning Discussion
Requirements/Goals
- Existing logic engine can be implemented as an optional module without too much trouble.
- A shared Rule interface
- Rules provide a method to evaluate within a context + parameters
- A shared Result interface
- Result declares it's type explicitly
- Provides a mechanism for consumers to easily distinguish between & use lists vs. single values
- Provides an easy way to coerce results between different types and between lists vs. single value
- A shared LogicContext interface within which rules are evaluated
- Centralized token registration by providing token, rule ID, provider, and configuration (unique across providers)
- Support for parameters.
- Caching is deferred to rule evaluators for now
Skeleton Interfaces / Implementations
Rule
interface Rule { public Set<RuleParameterInfo> getParameterList(); // TODO: We didn't discuss this interface, but we did agree that rules need to support parameters }
RuleEvaluator
interface RuleEvaluator { boolean canEvaluate(Rule rule); // TODO: This might be better implemented through annotations. Needs further discussion Map<Integer, Result> evaluate(Cohort, Rule, Map<String, Object>, RuleContext); // TODO: We probably want to wrap Map<Integer, Result> in a proper class }
RuleProvider
interface RuleProvider { Rule getRuleInstance(String ruleName, String extraConfig); // ruleName could be anything the provider wants. might typically be a classname. }
RuleService
interface / implementation RuleService/RuleServiceImpl { RuleContext createContext(); // Ensures that RuleContext can be overridden as needed void registerToken(String token, RuleProvider provider, String ruleName, String extraConfig); void unregisterToken(String token, RuleProvider provider); // provider for safety (the below methods might also have implementations without RuleContext and/or without params for convenience) Result evaluate(Integer ptId, String token, Map<String, Object> params, RuleContext context) { // create a cohort with a single patient, call the evaluate method on the cohort, return the result for that patient } Map<Integer, Result> evaluate(Cohort c, String token, Map<String, Object> params, RuleContext context) { // find the appropriate RuleProvider, ruleName, extraConfig for the given token; get the Rule from the RuleProvider passing in the ruleName and extraConfig; call evaluate method on the Rule } Result evaluate(Integer ptId, Rule rule, Map<String, Object> params, RuleContext context) { // construct a cohort of one; get the evaluator for the passed rule; call evaluate passing in the cohort, params and the context; return the result for the patient } Map<Integer, Result> evaluate(Cohort c, Rule rule, Map<String, Object> params, RuleContext context) { // get the evaluator for the passed rule; call evaluate passing in the cohort, params and the context; return the result } }
RuleContext
interface RuleContext { // TODO: Figure out whether this has indexDate, any caching, etc. }
Result
interface Result { public Object getValue(); public Date getDatetime(); // Needs more discussion if this is appropriate on the base interface (eg. what to do for Lists) public String formatAsString(); } class NumericResult { private Double result; private Date datetime; public Object getValue() { return result; } public Date getDatetime() { return datetime; } } class ListResult { private List<Result> results; public Object getValue() { return results; } public Date getDatetime() { // Not sure what to do here. Get the datetime of the first result? Maybe this method doesn't belong } class ObsResult { private Obs obs; public getValue() { return obs; } public Date getDatetime() { return obs.getObsDatetime(); } }
Use of Result
For coercing / converting Results to scalars for use in comparisons etc, for example:
if (eval("BMI") > 23) { ... }, we discussed a few possible approaches: (TODO: Decide on one)
- Add methods like "asDouble()", "asDate()", "asString()", "asBoolean()" to the Result interface (as we have now)
- Add single method to Result like: eval("BMI").coerce(Double.class) > 23
- Add utility method like: LogicUtil.toDouble(eval("BMI")) > 23
- Add utility method like: LogicUtil.coerce(eval("BMI"), Double.class) > 23
- Add a variety of converters like: new DoubleConverter().convert(eval("BMI")) > 23
Benefit of #5 is that it is cleaner than a massive utility method with lots of conditional logic, and that it allows modules to plug new converters into the framework as needed. It might look something like this:
interface ResultConverter<T> { public T convert(Result); } class DoubleConverter<Double> { public Double convert(Result result) { return Double.valueOf(result.toString()); } }
We ran out of time before we could really discuss next steps on all of this. Should we carve out time in another design forum in December?