Page tree
Skip to end of metadata
Go to start of metadata

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:

  1. A service layer that does very little besides calling the DAO layer.
  2. 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:

  1. Create a model type that implements from one of the OpenMRS base model interfaces: OpenmrsObject, OpenmrsData, or OpenmrsMetadata
  2. 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
  3. Create a new service implementation class which implements the previously created interface and inherits from the appropriate base service implementation class.
  4. 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.  

User Interface

Reflection-Based Configuration

Jasper Reports

  • No labels

3 Comments

  1. @Wes –

    Thanks for raising the question of the structure of services. However, I think you've been led astray by our own lack of consistency in the definition of layers. AFAIK, below is how things are supposed to be.

    *At the lowest level are the persistence objects. These contain properties for the underlying fields and collections of related objects. In general, they should not contain public business-logic methods, although there might be interface-implementing internal classes such as sorts and sets.

    *The DAO level provides CRUD methods and search methods for each persistence object. Each object requires its own DAO interface and object. I think the original idea of this was to allow the use of ORMs other than Hibernate. Again, the DAO should not include business-logic methods We could use a good way to do searches more generically (QBE, passing query text, whatever).

    *The API level provides services for groups of related objects. It is rare that a service will support only one persistence object, but most methods which involve a particular persistence object will be in a single service (let's call it the "home" service of the object). The service will usually provide pass-through methods to all the DAO methods of its home objects; however, in a parent-child situation, frequently only the parent will have a save method in the API service which saves both. It is probably the only service that holds a DAO object of its home objects. In addition, the service adds permissions, validations and @Transactional annotations. The service is the only layer that implements business logic. The service is the only layer that can have a method that writes more than one table.

    I haven't looked at your code, so I can't respond directly as to what fits into the picture, but it looks like you are relayering over the DAO layer. Maybe you should refocus on that layer. There's certainly a lot to be said for having unit tests already in the framework.

    1. Hey Roger,

      Thanks for taking the time to review this (in progress) documentation, we really appreciate the feedback.  It's rare that I get to discuss horribly geeky things such as data access patterns!

      I think we actually agree about a lot of what you say here; we've just come to different conclusions about the best way forward.  At the core of our entity service design is the fact that OpenMRS services are not application services; they are data services.  For the most part, OpenMRS services don't compose, transform, map, or process model objects; these are the things that application services do.  Instead, these services simply get data out of database and put data into it.  Also, given the fact that OpenMRS doesn't have separate DAO, DTO, or ViewModel classes, there is really no need for application services as there really isn't much in the way of business logic in the service.

      I think that we agree about persistence objects, models should not be much more than a bunch of properties, so I'll skip that.  I also agree with your assessment that the DAO level should really just provide CRUD methods and that is exactly what the GenericHibernateDAO class provides (code here).  What I disagree with is that "each object requires its own DAO interface and object".  What benefit does this provide?  Why should I have to write a DAO findById method for every model class when they are all the same?  This design results in a lot of repeated code and negates the benefits of using an OO language like Java.  The only possible benefit that I can see is the rather dubious ability to switch out Hibernate with another ORM;  however I think that is a false benefit for three reasons:

      • Hibernate will almost certainly never be replaced as the core ORM of OpenMRS
      • Most of the logic in the data layer is already in the hibernate-specific DAO objects, so you'd have to rewrite them anyway
      • The service can still use another mechanism to fulfill the request should some type of fancy full text search that is not supported by hibernate be required

      I can see your point about having services that group related objects, and that definitely is not cleanly supported in the entity service design.  However, I actually see this as an advantage because it leads to much smaller services that are tightly focused on a single responsibility.  This is somewhat of a personal choice, but I would much prefer many consistent small services to just a few large ones.  A smaller service API makes it easier to test and use because the API is consistent across all services, it also allows developers to take advantage of the class hierarchy and extend the resue up the stack into the web controllers and even the UI. 

      You mention that "the service layer is the only layer that can have a method that writes to more than one table."  I totally agree with this, but I think that the type of service that generally does this is an application service rather than a data service.  In situations where this is required you are usually dealing with a higher-level abstraction and that logic belongs in an application service anyway.

      This is already much too long but I'll end with this: this entity service design makes it very simple to create a new service that lets you get models into and out of a database, test that service, and then hook it up to the front-end (check out what Daniel Shorten did with backbone.js and the REST module).  It even supports the permissions, validations, and @Transaction annotations that you mention and does so in a customizable manner.  I hope that you do take the chance to look over the code (the current entity stuff has been moved to the OpenHMIS.Commons module but the Cashier still has best implementations).

      A question for you... is part of the problem here is that I am reusing existing terms, Service and DAO, when I really mean something slightly different: Data Service and Repository?

      1. Wes –
        I definitely will look at the code. Just a few comments:

        • I was trying to express the existing OMRS design patterns, not necessarily best practices. If you want to change these patterns, I think it should be a matter of discussion on the dev list and in a design call. However, I come from CDC where process is our most important product. If you raise this with other developers, they will probably say it's Open Source, do what you like.
        • I agree that there are few business rules in OMRS, the business rules are either in the forms (which are only a reference application) or are very low level (what is the "best" concept name to return). Business rules most often arise in modules which are directed toward a business purpose (or in the "apps" of UIv2). And I agree with you that significant business processes should be implemented in a service layer above the data service; modules without their own data objects are exactly that.
        • I agree that the benefits of the DAO layer are largely false. but they are an established OMRS design pattern. There are a number of undesirable side effects of working with Hibernate proxies which might be cured by using disconnected datasets, in which case the DAO might become more significant.
        • When I say that each object needs its own DAO, I didn't exclude using inheritance to generate them. I looked at GenericHibernateDAO and think it is on the right track. You can see this pattern in other places with "Base" rather than "Generic", e.g. in Reporting, "public abstract class BaseDefinition extends BaseOpenmrsMetadata implements Definition". There are a few nits to pick (setting sessionFactory in the constructor rather than having a set method for Spring injection; failure to implement logical deletion; failure to internationalize messages; have select by id and uuid as overloads of selectSingle) but it's a good start.

        I think you are overloading terms, the additional distinction you want to make is between a data service and a business service.