Logic Service Technical Overview
- 1 Layout
- 2 Typical usage scenario
- 3 Dissection
- 3.1 Upper layer
- 3.1.1 LogicServiceImpl
- 3.1.1.1 RuleCache
- 3.1.1.2 RuleFactory
- 3.1.2 Rule
- 3.1.1 LogicServiceImpl
- 3.2 Lower layer
- 3.2.1 LogicDataService
- 3.2.2 LogicDataSource
- 3.2.2.1 DataCache
- 3.1 Upper layer
- 4 Supported operators
Layout
The logic service is divided in two layers:
the*upper layer that handles userland requests and rules
the*lower layer that handles data sources and access to them
The details of the layers involved will be explained by analyzing a typical usage scenario.
Typical usage scenario
The main entry point into the logic service is the LogicService class. Let's assume for a moment that we want to retrieve all CD4 COUNTs from the database for a given cohort of patients. We would then use the LogicService class like this:
PatientSet cohort = ... // build or retreive a cohort
Result result = Context.getLogicService().eval(cohort, "CD4 COUNT");
or, for a single patient:
Patient patient = ... // get patient
Result result = Context.getLogicService().eval(patient, "CD4 COUNT");
This simple call to the eval() method is enough to get the CD4 COUNTs for our cohort of patients. As in this example, rules are invoked through a String token associated with the rule ("CD4 COUNT" points to the rule obtaining CD4 COUNT results in this example). Prior to invoking any rule (e.g., during startup before the example code above is run), the rule is registered through the Logic Service API (and handled by the RuleFactory described below). As rules are registered with the Logic Service, they are bound to a token String which can be used to invoke the rule.
The Result class is the only return type for every data request made into the logic service. It can be a single result (when we make a single-patient request) or a list of results (when dealing with a cohort of patients). The Result class implements the List interface, so it can be traversed in the same fashion as a regular list. It also has numerous methods for coercing the values contained inside it into regular data types (e.g. a boolean value, a number, a date, etc.).
Now let's take a look at what happens when we want to place some restrictions on our logic request. Let's say we only want Results that have CD4 COUNT < 200 to be returned:
Here we introduce a new class - LogicCriteria. This is probably one of the central classes in logic service. It is able to represent simple criteria (like the one above), as well as complex expressions. By using LogicCriteria, we let the logic service know about the type of data we want to retrieve from the lower layers.
Now let's dissect this last example and dive into the actual technical overview.
Dissection
We'll dissect a call to LogicService by following the top-to-bottom path that our request travels in order to satisfy the user.
Upper layer
LogicServiceImpl
First, LogicServiceImpl (a class implementing the LogicService interface) checks if the same request has already been made to the logic service (the same patient(s) + criteria pair). If so, then we already have the Result cached, and if not, we need to instantiate a Rule that will process the request.
In a nutshell:
RuleCache
The RuleCache class acts as a buffer for all requests made into the logic service. It's primary purpose is to speed things up when we have a situation where the same data is asked for the same patient(s) a couple of times - for example, a user views the results via the web interface, leaves the page, and then comes back to it again. The user's return to the page wouldn't trigger a new (possibly slow) database lookup, but only a (quick) cache lookup.
RuleFactory
If the request isn't cached, then we need to proceed with instantiating a Rule instance to suit the request. We (the developers) didn't want to burden the LogicServiceImpl class with deciding which Rule it needs to instantiate, so we moved this decision making into the RuleFactory class. There, rules are looked up by using the LogicCriteria object.
Rule
The request is then passed down to the Rule object, received from the RuleFactory. The Rule object knows what it needs to do with the request (it is a part of Rule's business logic), so it initializes the data sources it needs in order to pass the request further down the path. Once it collects all the data it needs from the data sources, it performs the logic and returns the result upward to the LogicService object.
The Rule is designed to hold all the business logic, and it is central to the person wanting to expand logic service's functionality (the rule writer). It is an interface which has only a couple of methods, with eval() being the central one. Rules have two sources for data: (1) the logic data sources and (2) other rules.
The general idea of writing new rules (whether manually or automatically through a tool like the Arden-to-Java translator):
Fetch any necessary data from either the upper level (other rules invoked through the LogicService) and/or lower level (data sources)
Perform any business logic to derive a Result
Implement the Rule interface — e.g., implement an eval() method that fetches data for a given patient, transforms it into the appropriate Result, and returns that Result
Register the Rule with the LogicService, binding it to a unique String token, thereby allowing other code in userland to access the new Rule through that token
Lower layer
LogicDataService
At the lower level, there's yet another service - the data service. However, its purpose looks less like LogicService's, and more like RuleFactory's.
The main difference between LogicService and LogicDataService is that the data service doesn't have a "pass-through" nature, like the logic service has. While user requests have a single entry point into the logic service (the eval() method), a Rule entering the lower level might have more than one entry point. Think, for example, of rules that need to access several different data sources in order to perform their business logic. If there were a single entry point into the data service (such as an eval() method), then rules would have no control over what data sources they are connecting to.
In order to avoid that, LogicDataService acts as a provider - it takes care of data sources and provides them to the rules using simple getter methods (e.g. getObsDataSource() ).
Although it is not intended to be a public service, LogicDataService may evolve over time and become one. At the moment, however, it is completely internal.
LogicDataSource
A part of our top-level request reaches the LogicDataSource instance. Only a part of the original request is received, because the Rule class already chopped the request up into several data source-specific sub-requests. So, we process the received subrequest by querying the data provider we're representing - for example, we query a certain database table, or we call some API function (if the data isn't already in the cache, see below). Once we have the data, we pass it upward to the Rule, which then assembles all the subrequests into some kind of a Result using it's business logic.
LogicDataSource handles caching on the lower layer, with its subclasses dealing with actual data retrieval. Caching is handled by the DataCache class and LogicDataSource's subclasses are completely unaware of it.
DataCache
This class handles caching of low level data elements. While the upper level's caching mechanism provides sufficient Rule caching, it is useless when it comes to caching data that may be needed by several different rules. This is where DataCache comes to play. It handles caching of low-level data elements (database column data, most of the time), so that data can truly be reused in the upper level.
It also needs to take care of data consistency issues, e.g. checking if data has been modified since the last time it was stored in the cache.
Supported operators
Logic Service currently supports the following operators that can be applied to Logic Criteria:
After
And
Before
Contains
Equals
Exists
First
Greater Than
Last
Less Than
Not
Or
Within
However, not all of them are implemented for every data source. Currently ObsDataSource and PersonDataSource do not support And, Or, Exists and Within operators. However, some of these can be used implicitly:
new LogicCriteria("CD4 COUNT").lt(200).before(someDate).first() – there's an implicit "and" operator in between these methods
eval(patient, "CD4 COUNT").toBoolean() – the Result class' toBoolean() method behaves like an Exists operator - it returns true if the Result exists (in this case, if the patient has any CD4 COUNT observations)