BladeRunnerJS differs from other toolkits in that is has knowledge of application concepts such as blades (for more information see Modelling Web Apps). What this means is that parts of the application architecture are tied to the toolkit. This doesn't mean that the toolkit or application architecture is inflexible, however. It simply means that there is a solid foundation that everything else is built upon.
The architecture within a running BladeRunnerJS application can be built out of a selection of modules. There are foundation modules that are required within every application, a set of highly recommended modules that we believe should be used in every application (although the toolkit doesn't enforce this) and a set of optional modules that you can pick and choose from. On top of that, you write your application and UI logic. BRJS defaults to using KnockoutJS but you can use any other library that you wish for your app and UI logic e.g. AngularJS, Ember, Backbone, Web Components (Polymer/Brick).
This foundation includes:
- Blades - each application feature should be encapsulated in a blade although what goes into a blade is entirely up to you
- bladeset and libraries - code cannot be directly shared between blades so shared code should be in a bladeset or a library
- Aspects - used to bring blades together to form a fully functional application
- Node.js style modules - code is imported using
requirestatements and exported using
module.exports = myThingor
exports.myThing = myThing†
Of the above points, only the Node.js style modules directly relate to the application runtime (hence why they're not in the above runtime diagram). Blades, bladesets, libraries and aspects are all concepts to ensure separation of concerns and building an application in a modular way. These build restrictions ensure that application features can't directly talk to each other and instead have to use a loosely coupled communication mechanism (e.g. the EventHub).
From that point, there are optional additions that can be used as required.
Of these additions, there are two whose use is highly recommended:
- ServiceRegistry - used to expose services to access shared resources e.g. Web APIs
- EventHub - A publish-subscribe messaging service, accessed via the ServiceRegistry
The additional optional architectural components are:
- AliasRegistry - a form of Inversion of Control/Dependency Injection
- Fell - for application logging
- Internationalization - support for multiple locales via i18n
- emitr - an event emitter library
A traditional view of a full application architecture may look something like the following:
The code within the Application in the diagram handles accessing services, business logic and controlling DOM manipulation. Most would agree that this isn't a good approach. There are two ways we can improve the application.
First, we can split the application up by feature - we call these blades:
In addition, the three concerns - accessing services, business logic, controlling DOM manipulation - should be separated.
Although this looks fine, there are a number of potential problems:
- If two controllers access the same backend service there is potential for duplication of effort and code
- If code is shared between controllers, a change in that code for one feature can have a side effect on another
- This type of solution can result in tight coupling between business logic and the implementation of code that accesses services, making change difficult to manage
- Controllers access backend services directly via AJAX calls or by creating WebSocket connections making testing difficult
- Depending on the MVC/MV* solution, testing may require assertions being made against the DOM. This can result in unreliable and inconsistent result due to browser rendering timings
These potential problems can be avoided with the introduction of two architectural decisions:
- A services layer
- A DOM abstraction
Services can be be used to access shared resources such as backend services. They can solve the potential problems, shown as points 1 to 4 above, by:
- Centralising code that accesses services
- Providing a defined contract/protocol/interface for interacting with services
- Representing an abstraction from the implementation meaning the implementation can be changed without introducing unwanted side effects
- When accessed via a level of indirection through a ServiceRegistry it's easy to return a test double in test scenarios
A DOM Abstraction
By using a DOM abstraction, it's possible to make assertions against that abstraction during test and avoid the problems that direct DOM assertions can introduce. By default a BRJS uses a MVVM (Knockout) solution which provides this benefit directly as the ViewModel is a logical representation of the view. However, as pointed out earlier, it is possible to use any other MV* solution and in doing so, achieve the same benefits.
There are a number of benefits to this approach.
Blades can be Run in Isolation
Since blades cannot access code defined by other blades and they access backend services through a Services layer, it is possible to run a blade in isolation within a workbench. This results in a productive developer workflow, unaffected by changes in unrelated parts of the application.
Apps are Composed of Blades
Blades can be selectively included into a composed view of application functionality known as an aspect. This allows for multiple aspects to be created offering different "views" of an application.
The loosely coupled nature of blade communication within this architecture also means blades can be updated, added or removed from aspects without unwanted side affects.
Full Features can be Tested in Isolation
Since blades don't directly communicate with each other and services are accessed via a ServiceRegistry it's possible to run a full feature in isolation during test.
It's possible to interact with the ViewModel and make assertions against interaction that have been made with a mock service that's been injected into the ServiceRegistry.
It's possible to inject a test double configured to behave in a particular way for a test into the ServiceRegistry and then make assertions agains the ViewModel to check that the view has been changed as expected.
Effective Team Working
Since each feature can be built in isolation, it's possible to have multiple teams working on different features. This can be split into:
- Vertical business features represented by blades
- Access to backend services represented by services
This results in teams working in parallel unaffected by code changes or hold-ups elsewhere in development. Services and aspects are integration points where teams must ensure contracts are defined upfront and are evolved through effective communication, but the surface areas for these are as small as possible to try and reduce integration slow down.
Blades can use any library
Blades can use any library or UI framework. As above, MVVM provides a number of benefits but the architecture isn't tied to using this or the default Knockout solution.
It's even possible for different blades to use different libraries, if required.
For more benefits, please read through each of the BRJS concepts.
A great way to see the benefits in action is to try out the getting started guide.