UI Architecture v7

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:

  • Brittle business logic
    Much of the business logic in the OpenLMIS-UI is implemented without a layered application architecture, which has made documenting and debugging the OpenLMIS-UI harder as the application has grown. The OpenLMIS-UI v7 architecture introduces a layered application architecture to formalize a separation between domain objects, UI route configurations, and a component driven design.
  • Configuration-heavy controllers
    Controllers are used to pass variables defined in the URL state to the HTML view — many of these controllers are hundreds of lines long, but don't improve code clarity or add logic to the HTML view. During the development of OpenLMIS v3.1, we found controllers difficult to extend in maintainable ways. The v7 architecture will avoid controllers entirely.
  • Tightly-coupled views: HTML is complex and often contains business logic
    HTML in the OpenLMIS-UI has become increasingly complex, as many pieces of HTML implement business logic. A best practice is to keep HTML as simple as possible, since HTML is difficult to unit test and much harder for an implementer to extend.
  • Too many singletons
    AngularJS creates lots of singletons, which are single objects persisted in memory while the UI is running in the web browser. Singletons in the AngularJS framework are often the cause of memory leaks and other performance bugs in large AngularJS applications. The OpenLMIS-UI v7 architecture will use plain javascript objects, written in modern Javascript, which will make the logic defined in the UI more reusable in other Javascript applications.  

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.

  • Page transitions only in route files — we don't want HTML or other layers to be responsible for directly showing a modal
  • Auto-inject resolved objects into view state — which will remove the need for verbose controller files, which generally just contain configuration

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.

  • HTML templates rendered by route files will not contain "wrapper" elements — these will be provided by the layout service
  • Allows developers to set default layouts, that can be globally overwritten by developers.

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:

  • Infrastructure layer that interacts with local databases and external services
  • Domain layer that organizes business logic around domain objects
  • Application layer that combines infrastructure and domain layers with UI specific workflow requirements
  • Router layer where routes and simple HTML are configured to display domain object
  • Components layer where simple HTML is transformed into richer interactive experiences

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.

  • All methods are asynchronus. By using javascript promises as generic return values we prevent any actions from blocking the HTML DOM from rendering, and stop the OpenLMIS-UI from becoming completely unresponsive.
  • All methods should take Javascript Objects as their primary argument. This will allow for APIs to change their internal contracts, without needing to change the arguments that are passed to them.
  • Most methods exposed by a domain object shouldn't take any arguments. This will force domain objects to act more declaritively.

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:

  • physicalInventory.submit() is directly expressed by the object's method
    • The object is taking an action on its self, rather than passing an object to the HTTP request
    • The method name is declaritive, as it expresses a series of actions but doesn't do those actions
  • There is a physical inventory component, which completely convers up details about how a physical inventory is summarized
  • The physical inventory's submittedDate property is being directly manipulated by the form

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:

  • Repository handles calling its implementation, then taking successful results and making those results into domain objects
  • Each domain object is given a pointer to the repository
    • Ideally, the repository argument would be optional — but then that specific domain object wouldn't be able to have its state changed
  • All implementation details about how or where the repository stores the domain object is hidden in the implementation layer, and the domain's repository has no direct knowledge of those implementations

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:

  • Changing a repository implementation based on application configuration
  • Adding confirmation modals before a domain object's method is actually called by wrapping the domain object method
  • Redirecting a user to a different page after a successful domain object method call

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:

  • Adding webpack with Typescript support to the build process
  • Create a pre-processor that will create ES6 modules from the current AngularJS focused modules that are currently implemented
  • Create a pre-processor that will define a webpack application entry point
    • This will not run if an index.js file is added to the /src file — which will be the generic location for an entry point
  • Move existing AngularJS modules into directories that reflect the domain they operate in