Logic 2.0 Design
High-level plans
New functionality
A new "rule" module will be authored which has the core set of functionality that is desired. This functionality will be primarily focused on producing a particular piece of data for a single patient or a cohort of patients. This module will use the "org.openmrs" namespace, rather than the conventional "org.openmrs.module" namespace in order to more easily support the goal of eventually incorporating this module into core. By keeping it as a module initially, this allows implementations running older versions of OpenMRS (1.6, 1.8, 1.9, etc) to take advantage of, and test out, this work, without requiring them to upgrade. It also will allow the module to evolve at a different pace than the core code.
TODO: What do we think of the name of this module? rule? patientcalculation? patientdata?
For the purposes of this design, and for brevity, I will use "Rule". If we change the name, we can replace "Rule" throughout as appropriate.
Existing functionality
The existing "org.openmrs.logic" package that exists in core will be removed into the existing logic module. The logic module will be updated such that it requires the module described above, and that it implements the interfaces created and exposed in it. The logic module will no longer be a "core" or "required" module within an OpenMRS distribution. Backwards compatibility will be maintained with a few minor exceptions, including the need to explicitly require the module within any module that uses it, and the need to use Context.getService(LogicService.class) rather than Context.getLogicService().
Requirements for the new functionality
Design for new functionality
Rule / ParameterDefinition
A Rule represents a definition that can be evaluated to produce patient data. A Rule can expose parameters which control the results of it's calculation.
interface Rule {
Set<ParameterDefinition> getParameterDefinitions(); // Returns all of the parameters supported by this Rule
}
interface ParameterDefinition {
String key; // "startDate". unique per rule and expected to be a valid java variable name.
String name; // this is a display label, like "Start Date"
String description; // "This is the start date for ..."
String datatype; // The Java class of this parameter
Boolean required; // If true, this parameter must have a non-null value for evaluation to occur
// we decided to get rid of allowMultiple, since description can be List<String>, though Burke wants to keep it according to the notes // Boolean allowMultiple; // If true, this parameter would accept a Collection of values of the declared "type"}
RuleContext
The RuleContext contains any contextual information that may be shared across one or more Rule evaluations. This includes the "index date" for the evaluation and a cache for storing the results for previously evaluated rules. The index date represents the date on which the evaluation should occur. It should essentially replace any call for "new Date()" in evaluation code, and should return the data that was accurate as of that particular date and time.
interface RuleContext {
public Date getIndexDate();
public void setIndexDate(Date date);
public CohortResult getFromCache(Cohort, Rule, Map<String, Object>);
// the cache-related methods still need some design (one option is to have a "RuleProvider owner" argument too)
public void addToCache(String key, Object value);
public Object getFromCache(String key);
public void removeFromCache(String key);
}
Result
A Result is the data that is produced from evaluating a Rule for a single patient. Results are strongly typed, but provide a convenience method for casting to other datatypes.
interface Result {
public Rule getRule(); // Returns the Rule that was evaluated to produce this result
public RuleContext getRuleContext(); // Returns the RuleContext used when the Rule was evaluated
public Object getValue(); // Returns the raw object value (eg. a Patient or an Obs)
public boolean isEmpty(); // Return true if the object value is null, an empty list, or an empty string?
public T as(Class<T> clazz) throws ConversionException; // Tries to convert to the passed type
}
interface DateBasedResult extends Result {
public Date getDateOfResult();
}
class EmptyResult extends Result {
public Object getValue() { return null; }
public boolean isEmpty() { return true; }
public T as(Class<T> clazz) { ... }
}
class ObsResult implements DateBasedResult {
private Obs value;
public Object getValue() { return value; }
public boolean isEmpty() { return value == null; }
public Date getDateOfResult() { return value == null ? null : value.getObsDatetime(); }
public T as(Class<T> clazz) { ... }
}
class EncounterResult implements DateBasedResult {
...
}
class VisitResult implements DateBasedResult {
...
}
class ListResult extends Result {
private List<Result> results;
public Object getValue() { return results; }
public boolean isEmpty() { return results == null || results.isEmpty(); }
public T as(Class<T> clazz) { ... }
public Result getFirstResult() { return isEmpty() ? new EmptyResult() : results.get(0); }
public Result getLastResult() { return isEmpty() ? new EmptyResult() : results.get(results.size()-1); }
}
We will likely employ a library of utility methods as well, to support conversion of Result types. For example:
CohortResult
A CohortResult is the data that is produced from evaluating a Rule for a Cohort of patients. It is essentially a wrapper of a Map<Integer, Result>, but provides the flexibility to add additional methods and/or data as needed down the road.
RuleEvaluator
A RuleEvaluator is responsible for evaluating one or more types of Rules into Results. This is where the bulk of all calculations occur, either by performing these calculations directly within the evaluator, or by calling service methods / DAOs that perform calculations. RuleEvaluators will likely be wired to Rule classes either via a registry or via annotations.
As an implementation detail, we probably want an abstract class to simplify writing RuleEvaluators that evaluate patients one at a time, like:
RuleProvider
A RuleProvider is responsible for retrieving a Rule instance given a rule name and an optional configuration string. A typical implementation would be such that ruleName is the rule class to instantiate and configuration represents the serialized property values that need to be configured on this rule instance, however it is totally up to the Provider to define this. For example, to retrieve the Rule for "Most Recent Weight":
Rule Name: org.openmrs.rule.definition.MostRecentObsRule
Configuration: concept=<UUID for Weight (KG) concept>
There would then be a RuleProvider registered to handle this type of Rule which would know it needed to first instantiate a new instance of MostRecentObsRule, configure it's properties via the parsed values from the configuration string, and then return configured Rule instance. Like RuleEvaluators, RuleProviders will likely be wired to Rule classes either via a registry or via annotations.
TokenRegistration
A TokenRegistration represents a saved Rule instance in the database, and includes a unique name, the RuleProvider, the ruleName, and the configuration for the rule. The intention is to allow a fully configured Rule instance to be retrieved given a unique name String. This class is a hibernate-managed class.
RuleService
The RuleService is the primary mechanism for evaluating Rules and for associating Rule instances with saved tokens.