Bones Architecture
December 21, 2020
Roles
Senior Front End Engineer
Technology
Javascript

Introduction

Bones is an application framework that emphasizes small loops and separation of concerns. It's designed to allow for evolving view layers on top of long-lasting, robust business logic and state engines.

Bones is made up of various pieces that build upon one another:

  • Observable

  • Store

  • Interactor

  • Gateway

  • Builder

  • Presenter

  • View

Observable

The observable is the core building block of Bones. Observables are tiny publication/subscribe engines. They maintain a list of observers and notify them. The implementation is super simple:

Observables make Bones reactive. When state changes, observers of that state are notified of the change. Redux is a popular library that implements this kind of reactivity and the use of observables here is heavily inspired by Redux and similar implementations.

This observable is written from scratch rather than using an off-the-shelf implementation like RxJS. By writing and maintaining an implementation that is comprehensible and teachable, it's easy to explain how Bones works down to the nuts and bolts.

The update function on the observable is asynchronous which allows users of the observable to know when all observer functions have been called. Imagine you're building a one-to-many solution and you need to know when all observers are done making remote requests before moving forward. Asynchronous update is great for that!

Store

The store is just an observable with a tiny layer built on top to maintain state. When state changes, update is called on the observable to notify observers of the new state.

Stores can be observed in one of two ways: through subscription or connection. Subscribing to a store is the same as subscribing to the underlying observable powering the store. Connecting, on the other hand, is unique to state in that the callback function provided is immediately fired with the current state. A default state is usually preferable when first rendering to a view and connecting to the store is great for that. You get the state right away.

Interactor

Interactors bring business logic and store together to form a cohesive interactive component. They expose a self-documenting interface through actions and selectors. Actions can be called to change state, while selectors are used to retrieve state.

All interactors should have the same interface. Simple template below.

Here's a simple counter interactor:

To achieve part of the interactor interface, we can simply spread the store into the returned interface. Stores already have the ability to get state, subscribe and connect.

For this counter example, we initiate a store with some state, provide two actions to increment and decrement the count, and include a selector to get the count.

Looking at the interactor interface, we can start to see some resemblance to Redux implementations with the use of actions and selectors. The difference being that in Bones, you're conventionally forced to encapsulate actions and selectors inside an interactor, rather than having them be importable from anywhere. Interactors are intended to be use-case specific as well. So an application built with Bones contains many smaller state loops rather than one large state loop. (Of course this isn't to say one couldn't use a Redux store inside an interactor.)

Large monolithic Redux implementations can be hard to untangle and migrate. The small, use-case specific, state loops built with Bones allow engineers to separate useful chunks of business logic into reusable pieces that can be composed together, swapped out, and moved around fluidly.

Gateway

Gateways provide access to external APIs. They're usually collections of fetch functions but they could also connect to a local GraphQL client like Apollo. In order to easily unit test and develop asynchronously with back end services, gateways should be swappable and mockable. It's common to write a gateway mock first and replace it with a real gateway later in development as back end and front end connect.

The gateway file above has just one function to get villains and it's simply injected into the interactor. Injecting the gateway makes unit testing straight forward.

The injecting is done with a builder (more on builders in a bit) function that composes the gateway and interactor together and calls an initiation function to get initial data.

There is nothing particularly fancy about gateways, but structuring remote connection code like this helps engineers move quickly and unit test with mocks. One of the main characteristics of Bones is flexibility in architecture and development. The interchangeability of gateways lends to architectural flexibility, but it also helps the engineering process more broadly as front end can get started with development even with a back end that lags behind.

Builder

Builders are the glue that hold all the bones together. Builders are responsible for creating gateways and interactors and tying them to views through presenters. You can think of builders as main functions; they're called to create an application or a piece of an application.

Builders can be flexible as they shape to the view framework they're connecting to. For instance, in a React/Next application, builders consist of a interactor creator function and a hook.

Next controls interactor creation via it's own control flow. Next spins up, creates the interactor, and subscribes all in the hook. In this case, Bones gives up some of its control to Next.

Outside of any view framework, Bones can run any application with builders that create interactors, subscribe, and manually render on state changes.

Tests can be written for a Bones application by creating modified builders that mock specific parts. Common approaches include replacing gateways with mocks to allow tests to run offline and target core interactor logic. Views can also be mocked simply by calling actions directly.

Again, builders are the glue that piece a Bones application together; they can be as flexible as needed to connect all the parts together. Builders adapt to the view framework used.