Cashier Module Developer Documentation
Design Goals
Entity Service Architecture
(Note that while this is currently a part of the OpenHMIS Cashier module it will shortly be moving into the OpenHMIS Commons module to make it easier to reuse. The commons project is hosted on github here: https://github.com/OpenHMIS/openmrs-module-openhmis.commons)
Designing, testing and using application services can be difficult within the OpenMRS ecosystem. Service interfaces can easily get very large while service implementations often repeat very similar code over and over. Testability of services is also suspect because many methods simply call into the DAL where the actual logic is done. We created the Entity Service Architecture to simplify the design and implementation of application services. This architecture provides a basic end-to-end system for getting data from a database, displaying/editing that data in a web page, validating the data, and saving data back to the database. It also provides a basic structure and implementation for service unit tests with minimal configuration and repeated code.
OpenMRS core services abstract Hibernate from the service via DAO classes. This means that you could, theoretically, switch out Hibernate for another ORM and, theoretically, not have to change your service implementation. Our service design differs from this traditional service design found in OpenMRS in one key area: we removed the separation between the service implementation and the hibernate DAO.
The current OpenMRS Service Design
Why did we decide to move away from this more flexible Service > DAO > Hibernate implementation? We believe that this abstraction adds negligible benefit while simultaneously adding complexity to services and reducing the ability to create a cohesive end-to-end service framework. An example of this can be found in the org.openmrs.api.impl.VisitServiceImpl which, as recent addition to OpenMRS, one would assume is using the current best practices. This service has a total of 29 service methods (ignoring the get/set DAO methods), of those only 10 have any logic beyond simply calling an identically named DAO method. Only 3 of those 10 logic methods do any of the type of processing which would normally necessitate a service layer method. The net result is:
A service layer that does very little besides calling the DAO layer.
A DAO layer that does almost all of the work and cannot be easily replaced.
This design effectively nullifies the benefit it is supposed to provide; the service layer is more flexible only because it doesn't do very much and the DAO layer becomes complex because it takes on the responsibilities normally handled by a more robust service layer. We believe that there is a disconnect here because OpenMRS services are not the application services for which the current design would make sense, but are instead data services that make the DAO layer redundant.
Service Design
To facilitate API development we created an OpenMRS service framework that:
Simplifies service design and implementation
Makes services more DRY
Helps services to adhere to the Single Responibility Principal
Fully supports and utilizes generics
The entity service design and relationship to existing OpenMRS service classes
To create a new servce:
Create a model type that implements from one of the OpenMRS base model interfaces: OpenmrsObject, OpenmrsData, or OpenmrsMetadata
Create a new service interface inherits from the appropriate entity service for the model:
(Model base type) OpenmrsObject -> (_Entity service interface) _IEntityService
OpenmrsData -> IDataService
OpenmrsMetadata -> IMetadataService
Create a new service implementation class which implements the previously created interface and inherits from the appropriate base service implementation class.
Implement the getPrivileges and validate methods in the service implementation class.
As a concrete example, here is all the code that is required to create a service for the cashier item Department model:
// The model
public class Department extends BaseOpenmrsMetadata {
// Normal model implementation
...
}
// The service interface
public interface IDepartmentService extends IMetadataService<Department> {
}
// The service implementation
public class DepartmentServiceImpl
extends BaseMetadataServiceImpl<Department>
implements IDepartmentService {
@Override
protected IMetadataAuthorizationPrivileges getPrivileges() {
return new BasicMetadataAuthorizationPrivileges();
}
@Override
protected void validate(Department entity) throws APIException {
return;
}
}
That is all the is needed to get a service that provides the following methods: save, purge, getAll, getById, getByUuid, retire, unretire, findByName, All of these methods also support paging and the expected overloads for various parameters, where appropriate.
At the core of this design is a new generic DAO class we've (inventively) called GenericHibernateDAO. This DAO class is responsible for all interaction with the database, through Hibernate, and is driven by Hibernate criteria which are set up by the service. The DAO API is very simple and allows services the full range of data operations they might need to do.
Work-in-progress: Sub-pages for:
Configuration
Creating custom methods
How paging works
Unit Tests
Using the entity framework, service tests because much easier to write as all unit tests for the core methods already exist. The entity service class hierarchy has been carried down to the test level as well, so to create tests you simply extend one of: IEntityServiceTest, IDataServiceTest, IMetadataServiceTest. Following the Department example above, here is the test class:
public class IDepartmentServiceTest extends IMetadataServiceTest<IDepartmentService, Department> {
public static final String DEPARTMENT_DATASET = BASE_DATASET_DIR + "DepartmentTest.xml";
@Override
public void before() throws Exception{
super.before();
// Load the test departments
executeDataSet(DEPARTMENT_DATASET);
}
@Override
protected int getTestEntityCount() {
// The number of departments defined in the dataset
return 3;
}
@Override
protected Department createEntity(boolean valid) {
Department department = new Department();
if (valid) {
department.setName("new department");
}
department.setDescription("new department description");
return department;
}
@Override
protected void updateEntityFields(Department department) {
department.setName(department.getName() + " updated");
department.setDescription(department.getDescription() + " updated");
// If the Department model had any properties other than what it gets from BaseOpenmrsMetadata, those should updated here
// department.setWhatever(department.getWhatever() + " something else");
}
@Override
protected void assertEntity(Department expected, Department actual) {
super.assertEntity(expected, actual);
// If the Department model had any properties other than what it gets from BaseOpenmrsMetadata, those should be tested here
// Assert.assertEquals(expected.getWhatever(), actual.getWhatever());
}
}
This will provide tests for all of the entity service methods with minimal code and will hopefully make the barrier to writing quality unit tests that much lower.