Let’s start with a fact: You can write great code in bad frameworks and terrible code in good frameworks. But, frameworks can do things to encourage certain styles of development that lead to better code quality. Some great examples of this are Angular’s adoption of RxJS to populate the UI with data asynchronously, or the UI state management patterns introduced by React and Redux which are now starting to filter into other frameworks. Many of the design choices made by the Aurelia core team when building framework help developers fall into the pit of success (a term coined by Rico Mariani in relation to language design). The techniques that I’ll cover in this post include Separation of Concerns, Dependency Injection & IOC, the Event Aggregator, Convention over Configuration, and modular design. Combine these techniques, and what you end up with is a framework that promotes the code quality attributes like reusability, testability, maintainability, and extensibility that we all want to see in our applications.
Separation of Concerns
In computer science, separation of concerns (SoC) is a design principle for separating a computer program into distinct sections, such that each section addresses a separate concern.
— Wikipedia
The concept of separation of concerns between your HTML file (which provides the structure of the page), your CSS file (which determines how the page looks), and your JavaScript (which determines how the page behaves) has been around in Web development for some time. The idea is that by splitting out these concerns and managing them separately, you should be able to work on any of these independently of the other. It also means that team members with the relevant skill set should be able to work on one piece of the picture without needing to know the in-depth details about the other pieces. For example, a developer should be able to put together the basic HTML structure and JavaScript behavior to then be styled by a team member with more of a focus on User Experience or Design.
Aurelia’s opinion on this is that that this concept of separation of concerns is no less important in the world of SPA development than it is in a more traditional server-centric web development approach. Aurelia achieves this by having a clearly defined view (an HTML template) and view-model (a JavaScript/Typescript file) as seen below.
Building components in this way allows you to easily test the JavaScript view-model. Since it contains most (if not all) of the logic for the component you only have one place to test. Typically Aurelia view templates are kept clean and simple and can be covered with an end to end test instead. The HTML is then also easier to reason about and style, since it’s basically just a regular HTML file.
This approach gives you the maximum flexibility. You still have the option of having one person manage an entire vertical slice of the application – JavaScript, HTML, and CSS – but you also have the option of splitting these out if that’s the way you’d prefer to work within your team or company.
Dependency Injection / IoC
In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object
— Wikipedia
Traditionally, objects in a system are responsible for managing their own dependencies. This can become challenging as the application grows in scale, increasing the complexity of the relationships between these objects. Dependency injection simplifies this problem by moving the responsibility of creating objects away from the objects themselves and placing it in the hands of the dependency injection (DI) framework. This transition of responsibility is known as Inversion of Control (IoC). Typically, in an application using DI, an object declares which dependencies it requires (often as constructor parameters), and the DI framework provides the relevant implementation of these dependencies at run-time.
Aurelia uses Dependency Injection to simplify the way that components receive their dependencies. The simplest example of this is a JavaScript service class. Imagine the case where we have the app
component wants to show greetings. To do this it brings in the greetings child-component using a require
statement, as shown in the below example:
app.js – (main app view-model)
app.html – (main app view)
The view will require a greeter
component, which it’ll then use to render greetings to the page.
Lifting the lid of the greeter
component and looking down another level, we’ll see that the greeter
component as an external dependency (the GreeterService
class) which is actually responsible for retrieving the greeting to show to the user. In this case, the Aurelia framework will ask the DI container for an instance of the GreeterService
class and inject it into the Greeter
constructor. All injected instances in Aurelia are singleton by default within a given container where each component (such as a custom element or attribute) has its own container. It’s possible to override this convention if needed.
greeter.js
This greeter view-model would have a corresponding view greeter.html
which simply renders the greetings to a list:
greeter.html
The greeting
component uses a class called GreetingService
to actually load the greetings, so let’s take a quick look at that to get the full picture:
greeting-service.js – (service class)
There are some major benefits of managing dependencies in this way including but not limited to the following:
- Testability: It’s easier to swap out the implementation of the
greeter
service (which may, for example, be doing an HTTP call with a mock implementation of this service to quick and easy testing. - Another example of where I’ve found the Aurelia dependency injection system to be particularly useful is managing shared services like the Aurelia fetch client module (a simple but powerful abstraction over the Fetch API). By injecting this service into classes via the DI container it’s possible to configure common settings such as the base URL of the Web API and default headers that should be sent with each HTTP request in the
app
module once rather than configuring them separately in each service class.
To find out more about how Aurelia’s dependency injection system works, I’d recommend checking out this great page on the Aurelia docs authored by the Aurelia framework instigator Rob Eisenberg himself.
Event Aggregator
An Event Aggregator acts as a single source of events for many objects. It registers for all the events of the many objects allowing clients to register with just the aggregator.
— Martin Fowler
The Event Aggregator pattern is great for when you have a complex graph of objects that need to communicate with each other. Instead of each object needing a reference to every other object in the graph, you can centralize these connections in the Event Aggregator object, greatly reducing the level of coupling in the system.
How different does this use case sound from the component hierarchy present in Aurelia applications? Not much at all really, and hence it’s also a great solution for reducing coupling between components. In fact, the Aurelia core team had this idea and implemented this pattern in a framework module called the aurelia-event-aggregator
.
The Aurelia Event Aggregator
To use the Aurelia event aggregator you simply import the EventAggregator
class from the aurelia-event-aggregator
package. If you’re using the Aurelia CLI as your project creation tool then this should be installed by default, if not then you can install this package using npm (npm install aurelia-event-aggregator
). Once you’ve imported the event aggregator into a class in your application (typically a view-model) you can inject it using DI (which provides you with a shared instance). To demonstrate I’ve added the EventAggregator
to the greeter
view-model, to publish a greeting
event whenever a user adds a new greeting to the list.
The EventAggregator
class has three methods: publish
, subscribe
and subscribeOnce
. To begin with, we’ll use the publish
method, which we’ll pass the name of the event greeting-added
and the data to be passed along with the event (the greeting). The name of the event is often described as a channel, because any event publisher publishing events with this name, will be passed through the Event Aggregator to any subscribers that are listening for events with this name, creating a virtual channel between the event publishers and subscribers.
greeter.js
For completeness, the view changes required to add greetings are as follows:
greeter.html
To make use of these messages published on the EventAggregator
you need to use the second key method subscribe
which takes the name of the channel you’re subscribing to greeting-added
and a function to call when the event is received. One important thing to note here is that the subscribe method returns a subscription
object which needs to be disposed of when you’re finished with it (for example when the user navigates away from a particular screen in the app), otherwise you’ll continue to receive zombie messages even when that screen is no longer active. This is not required on the subscribeOnce
method since as the name suggests it will only listen for one message on the channel before closing up shop.
app.js – (main app view-model, EventAggregator added)
To read more about the Aurelia Event Aggregator, I’d recommend the article on the Aurelia Hub, and also just reading through the source code of the module itself. Like much of the Aurelia source code this module is simple, well structured, and easy to understand.
Convention over Configuration
Convention over configuration (also known as coding by convention) is a software design paradigm used by software frameworks that attempt to decrease the number of decisions that a developer using the framework is required to make without necessarily losing flexibility.
— Wikipedia
One aspect of Aurelia that differentiates it from similar frameworks is its use of conventions to minimize boilerplate code. Some of the conventions provided by the Aurelia framework include:
- Custom element naming. To create a custom element, you create two files, one for the view, and one for the view-model. For example to create the
<greeting>
custom element you needed a JavaScript filegreeting.js
and a corresponding HTML templategreeting.html
. By convention creating a class namedGreeting
Aurelia will create a custom element for this class. You can also follow the{Name}CustomElement
convention if you want to be more explicit with how this is named. - Databinding. Conventions are used extensively throughout Aurelia’s binding system. For example in the
greeter
component we used an input to allow the user to enter new greetings:<input value.bind="greeting"></input>
. You’ll notice that we didn’t need to specify that this should be a two-way binding. By default, Aurelia has determined that this should be a two-way binding because we attached the binding expression to aninput
element. You can override these conventions if you need to. For example, if we for some reason wanted a one-way input binding we could use<input value.one-way="greeting"></input>
. In that case, the value would be rendered into the input from the view-model, but no changes in the input would be reflected back into the view-model. - Custom attributes. Custom attributes allow you to create custom HTML attributes to augment the behavior of DOM elements. The convention for custom attribute class names is ${Name}CustomAttribute, which is then converted to kebab-case for use in HTML. For example, to add the ability to attach a tooltip to any HTML element, we could create a class
TooltipCustomAttribute
, which would have a nametooltip
in the view (used like<button tooltip></button>
. When you attach this attribute to an element in your view you instruct Aurelia to create a new instance of the class for theTooltipCustomAttribute
. Again, this is handled for you via naming conventions so to avoid you needing to specify these mappings explicitly.
Modular approach
Modular programming is a software design technique that emphasizes separating the functionality of a program into independent, interchangeable modules, such that each contains everything necessary to execute only one aspect of the desired functionality.
— Wikipedia
The Aurelia framework consists of a small, light-weight core, combined with a suite of features provided by small well-defined modules. Some of the modules include:
- The binding library, which implements Aurelia’s powerful data-binding system.
- The template engine which supports custom elements, data-binding, view-slots, attached behaviors and more.
- The dependency injection package, which provides an extensible DI container for JavaScript (which can work independently of the Aurelia framework
- and much more
This means you can start an Aurelia project with the minimal set of packages (such as binding and templating), and bring in more as an when you need them. This has two major benefits: It keeps the framework footprint as small as possible, while at the same time making it easier for new team members to hit the ground running.
In summary
The Aurelia framework uses a number of techniques to encourage good code quality. Far from hindering development speed, these techniques (or framework design choices) actually improve development speed, by reducing boilerplate code, and adding simple patterns to solve common problems like dependency management and inter-component communication.
Leave a Reply