Overview

This document outlines the OpenLMIS-UI v7 architecture, which is being introduced to avoid AngularJS paradigms that make extending the OpenLMIS-UI difficult. The OpenLMIS-UI is a URL driven application with a modular architecture that allows implementers to modify workflows and logic to meet their communities needs. During the development of OpenLMIS v3.2.1, a need for a more explicit application architecture is needed to keep application components decoupled so extensions by implementers can be more easily maintained. The extensions developed by OpenLMIS Malawi implementation have shown flaws in the AngularJS-based application architecture.

The v7 architecture aims to improve the following issues that have become problematic in the OpenLMIS-UI as of OpenLMIS v3.2.1:

Architecture

By further formalizing the OpenLMIS-UI's architecture, we intend to avoid issues that have been part of the OpenLMIS-UI. To better support the extendability of the OpenLMIS-UI we will focus on extending routes that define pages within the OpenLMIS-UI, since a majority of extensions focus on modifying data that is loaded into the view or adding specific bits of HTML. Additionally, by adopting a layered architecture pattern, developers will be able to more easilty reason about how the OpenLMIS-UI functions. Neither of these concepts are completely new to the OpenLMIS-UI, but by further formalizing the concepts the OpenLMIS-UI will become more consistent and maintainable – espically when using extensions from implementers – the largest change is how these concepts interact.

 

Router Moderated Architecture

In a URL-Driven application every screen is directly accessible by a URL, which is moderated by a "router" that loads data into the application state and renders HTML templates. Angular-UI Router provided a great start for an application, but as complexity in the OpenLMIS-UI has grown, configuring UI routes has become verbose and error prone.

To solve these issues, we are going to wrap Angular-UI Router in a way that will force presentation and business logic to be separate. The goal is to create a route registry that implements a facade pattern, such that routes are primarily responsible for loading objects into the application's current state. This way routes can focus on configuration, not presentation.

OpenLMIS Layout Service

OpenLMIS router states will be able to express presentational needs by adding key/value pairs to the route configuration. The route configuration will be interpreted by a completely separate layout system, which can be changed or evolve separately from the business logic. This will allow implementers to create large UI layout changes with minimal code.

Extendable Routes

Routes are simpler to extend than the current strategy that we are using in the OpenLMIS-UI, which is decorating functions. Most of our modifications focus on changing workflows based on the data that is loaded into a view, or adding additional HTML templates to a view. Since the routes are accessible as pure javascript objects, they are simple for an implementer to modify or replace.

This method of route focused extension will be easier to reason about, than following our current convention of using AngularJS decorators. AngularJS decorators rely of Angular's dependency injection system, which makes the implementation details of objects harder to reason about.

React Support

Support the React presentation framework isn't a primary focus of the v7 architecture change, but as we move the application logic and route away from the AngularJS framework we will be able to support specific routes and components to be rendered in React. Using React instead of AngularJS components is a preference of many developers, and it also would allow sections of the OpenLMIS-UI to be rendered into native Android applications. Libraries exist for supporting both React and AngularJS in the same application, one such application is ui-router react hybrid which is an extension of the same core library behind Angular-UI Router.

Layered Architecture Overview

In the OpenLMIS-UI, we try to keep each module focused on a single application function. Yet, in OpenLMIS v3.2.1, a module would be responsible for providing HTML for a view and sending/receiving HTTP requests — which is a bad practice in that it makes debugging difficult, and extendability much more fragile.

Our solution is to adopt a layered architecture, which will consist of:

This is a very traditional layered architecture, and by respecting application layers our sections of code should be able to more cleanly have a single responsibility — which will become easier to maintain and debug.

There are a few general conventions we will follow to keep application layers performant and decoupled. These conventions primarily apply to domain objects, infastructure repositories, and application services.

Below are descriptions of how domain objects, repositories, and application services will interact. Details, conventions, and examples for other sections will be developed in more formal documentation.

Domain Objects

Currently the OpenLMIS-UI implements logic that is structured around AngularJS services that closely map to OpenLMIS Services. This has lead to repeated code with complicated methods. Javascript objects received from an OpenLMIS Service are directly passed to route and component level modules, which makes reasoning about the current behavior of a screen difficult. There are many cases where implementation logic is difficult to follow because of how objects are mutated and passed between services.

Articulating a domain layer in an application will help avoid these pitfalls by encouraging a clear object oriented style — rather than methods where javascript objects that represent the domain are mutated and passed to other objects. Ideally, domain objects will establish declaritive methods that can be meaningfully used in HTML to express logic.

An example of an ideal domain object used in HTML would be:

<form ng-submit="physicalInventory.submit()">
    <physical-inventory-summary physical-inventory="physicalInventory" />
    <label for="submittedDate">Submitted Date</label>
    <input id="submittedDate" type="date" name="submittedDate" ng-model="physicalInventory.submittedDate" />
    <input type="submit" value="Submit Physical Inventory" />
</form>

What's worth noting in the example above is:

Repositories

Repositories are intended to model a collection of domain objects, and are responsible for connecting domain objects to the infastructure layer that communicates with OpenLMIS Services. In domain driven design, repositories can feel like an odd concept — as the interface for a repository is expressed in the domain layer, while the repository implementation is expressed in the infastructure layer.

To keep configuration simple between a domain object and it's repository — domain objects wilil always take a Javascript Object as their first argument, which will represent the object's properties, while a second argument will be the repository directly injecting its self.

An example of this would look like:

import PhysicalInventory from openlmis-stockmanagement-ui.domain.physicalInventory

class PhysicalInventoryRepository {
    constructor(repositoryImplementation) {
        this.implementation = repositoryImplementation;
    }

    findAll() {
        return this.implementation.findAll()
        .then(function(results) {
            var domainResults = [];
            domainResults.forEach(function(result) {
                domainResults.append(new PhysicalInventory(result, this));
            });
            return domainResults;
        });
    }

    submit(physicalInventory) {
        return this.implementation.submit(physicalInventory)
        .then(function(result) {
            return new PhysicalInventory(result, this);
        });
    }
}

export PhysicalIncentoryRepository;

What's important to note is:

Application Services

Application services combine domain repositories with repository implementations, which are exposed through methods that implement flow control. Types of flow control could include:

An example of this would be:

import PhysicalInventoryRepository from openlmis-stockmanagement-ui.domain.physicalInventory
import PhysicalInventoryRepositoryImpl from openlmis-stockmanagement-ui.infastructure.physicalInventory

class PhysicalInventoryService {
	constructor() {
		let impl = new PhysicalInventoryRepositoryImpl();
		this.repository = new PhysicalInventoryRepository(impl);
	}
    findAll() {
		return this.repository.findAll();
    }
}

Transition Plan

To move from the implementation of the OpenLMIS-UI in OpenLMIS v3.2.1 to the v7 architecture, we want to provide a new way for working with the OpenLMIS-UI, without breaking any of the current work.

This will be done by: