Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Table of Contents

Overview

This

...

document outlines the OpenLMIS-UI

...

Table of Contents

Overview

v7 architecture, which is being introduced to avoid AngularJS paradigms that make extending the OpenLMIS-UI difficult. The OpenLMIS-UI is a progressive web application that is URL Driven. The application architecture stresses modular Javascript and DRY HTML markup, so implementers can customize workflows 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 guiding principal behind the v7 architecture is that an extendable codebase is a maintainable codebase. The OpenLMIS-MW implementation has shown that following AngularJS framework standards makes it difficult to extend and maintain logic within the OpenLMIS-UI.

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.

Gliffy
imageAttachmentIdatt233996342
baseUrlhttps://openlmis.atlassian.net/wiki
migration1
nameUI v7 Architecture Diagram
diagramAttachmentIdatt233734218
containerId122716264
timestamp1518740861421
 

Router Moderated Architecture

In a URL-Driven application every screen is directly accessible by a URL. AngularJS and ui.router provide , 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 the presentation and logic of our implementation is becoming less DRY and brittle - which makes debugging and extension difficult. Maintaining unit test for layout logic has become difficultas complexity in the OpenLMIS-UI has grown, configuring UI routes has become verbose and error prone.

To solve these issues, we are going to use a facade pattern to seperate application logic from application style at the application's main point of entry - the routing logic layer. This facade will be implemented by wrapping function calls to ui-router, which will decouple direct dependencies from OpenLMIS-UI modules to ui-router. By decoupling layout and application logic, the OpenLMIS-UI will implement default behaviors that can be overridden by implementers in ways that are easy to document and test.

Since we are initally wrapping ui.router, we can leave the existing modules and update them to take advantage the wrapped router functionality in an incremental manner. This will also ensure backwards compatibility until the OpenLMIS-UI drops support for AngularJS (which will happen eventually).

...

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. Single focus modules help keep code organized, and makes it simple to not include sections of functionality by ignoring those files during the OpenLMIS-UI build process.

We expect most of the OpenLMIS-UI modules to be one of these:

  • Data Modules which provide data into the OpenLMIS-UI. Loading, saving, and validating data should happen in these modules.
  • Route Modules define and register an application route or state. These modules should be "skinny".
  • Component Modules provide building blocks and progressive enhancements to the semantic HTML created in the route modules.

Component Tests

Currently the OpenLMIS-UI has guidance for support of unit tests, but doesn't have any testing structure to ensure that a modules desired functionality doesn't accidentally drift. These tests will function much like the current unit testing practices, but focus on testing the entire end-to-end logic created in a module. For data and component modules, component tests won't provide much more value than unit tests.

Component tests for route modules will provide the most value as it will create coverage of the functional logic that is exposed by the UI. These larger integration tests shouldn't be used to test edge cases, but rather larger functional "happy path" scenarios and and possibly common error scenarios.

...

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 resourceServices AngularJS services that closely map to OpenLMIS Services. This has lead to repeated code with complicated methods. Javascript objects recieved 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.

This v7 architecture improves the OpenLMIS-UI by implementing patterns to aid code reuse and reasoning. ES6 and functional programming approaches in javascript focus on immutable data, which makes reasoning about the current state of a page easier and is more performant in web browsers. Using ES6's modular syntax will allow the UI to avoid global singletons when implementing business logic, which will help both unit testing and code reuse.

To structure this overall approach, the OpenLMIS-UI will use a paradigm that was popularized with the AngularJS module ngResource. The ngResource pattern allows for a "data resource" to be created as a singleton, which will then return objects that use the domain language to express changes to an object's state.

Data Resources

Data resources provide a layer of abstraction between the OpenLMIS Services and the OpenLMIS-UI. These data resources should always be focused on the domain-level object rather than the OpenLMIS Service that provides data for the object.

An example of where the data resource should cover multiple OpenLMIS Services is the "user" data resource. In the OpenLMIS Services, the "user" domain object has the most information available in the openlmis-referencedata service, but a user's password is updated through the openlmis-auth service.

All data resources will be dependency injected, which will allow other modules to add or modify methods exposed by the data resource. Connecting to a OpenLMIS Service should be considered an incremental improvement to the base factory, and these methods should be added with decorators.

Most data resources will implement a local database so that the resources are available offline, but some resources such as orderables might not have functional requirements to function offline.

Domain Objects

Domain objects implement business logic and interactions with data resources with the same terminology that is used in OpenLMIS functional documentation. This is helpful because unit tests on this domain object can be understood by stakeholders, which helps maintain clear communication between developers and stakeholders. These methods implement business logic and interact with data resources, which helps hide the implementation details of the domain object. Hiding the implementation details of domain objects from controller-level methods results is more maintable code and easier extension by implementers.

To ensure that our domain objects work well in the AngularJS view methodology, all object properties will be directly accessible and mutatble.

The methods for domain objects will either return an immutable value or resolve a promise that returns a new instance of the domain object. Immutable return values help avoid accidental manipulation and accidentally updating the DOM.

All methods on a domain object must not take any arguments, which will aid testing and reasoning about these domain objects.

Usability & Affordance

These are things that improve the OpenLMIS-UI experience, and include directive, components, and CSS. 

Components

Components are popular these days.

Directives

Directives are damn useful for enriching content and workflows.

CSS

Route and data modules should never be allowed to add CSS styles. This could cause issues if one page is supposed to look or act different than another page, but having divergent designs per page makes a UI feel inconsistent (so we should avoid this).

Example Page Load Process

GliffyimageAttachmentIdatt122093954baseUrlhttps://openlmis.atlassian.net/wikinameOpenLMIS Router Facade PatterndiagramAttachmentIdatt122224954containerId122716264Articulating 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:

Code Block
languagexml
<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:

Code Block
languagejs
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:

Code Block
languagejs
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