Basic Dependency Injection with RequireJS

Recently, I was having a conversation about the basic concepts of IoC/DI, and, specifically, how they pertain to modern (single page) JavaScript Web Applications. This discussion was quite interesting, and so I felt inclined to share some thoughts on the subject with a wider audience.

Dependency Injection in JavaScript

Being a dynamic language, when designing JavaScript based architectures, in comparison to architectures which are under the constraints of a statically typed language, one is typically less inclined to consider the relevance of, or immediate need for, a complete IoC container. Of course, context is key, and so there are certainly JavaScript applications which can benefit from an IoC container (such as wire.js). As such, it would not be prudent for one to suggest otherwise; but rather, this is simply to say that for the majority of JavaScript applications, standard AMD loaders provide a sufficient means of managing dependencies; as the rigidness inherent to statically typed languages which IoC containers help manage are generally less relevant to dynamic languages.

With that being said, while a robust IoC container may not be necessary for the majority of JavaScript applications, it is quite important to emphasize the benefits of employing basic dependency management and Dependency Injection; as this is an essential design characteristic which is critical to the success and overall maintainability of large scale client-side web applications.

Facilitating Code Reuse

Anyone who has been responsible for developing and maintaining specific core features across multiple applications is likely to understand that the ability to facilitate reuse of JavaScript modules is crucial. This is particularly important in the context of architectures which must account for the ability to support mulitple implementations of the same application across different form-factors; for, the ability to manage and configure dependencies can prove paramount; allowing for a framework upon which various form-factor specific implementations of an application can be supported.

In addition to this, as one might expect, having the flexibility necessary for configuring dependencies lends itself, quite naturally so, to various unit testing scenarios.

Configuring Dependencies with RequireJS

Though not always immediately apparent, applications leveraging RequireJS are essentially using a basic form of Dependency Injection out of the box – even if not in the most purist sense of the term. However, the simple matter of mapping module names to module implementations can be considered, in-of-itself, a basic form of Dependency Injection, or perhaps, one could argue this as being more of a Service Locator, as RequireJS does not instantiate dependencies on a clients behalf. Regardless of the preferred classification, this mechanism of defining dependencies is quite important, as it affords developers the ability to change module implementations as desired without the need to change client code. Of course, such modules must adhere to a specific contract (interface) so as to ensure clients which depend on specific named modules receive the expected API.

Explicit Dependencies

Consider a rather contrived example of a shared Application module which is used across two separate applications; one for Mobile and one for Desktop; with the Application module having a dependency on an AppHelper module:

Both the Mobile and Desktop applications can easily map the AppHelper module to a context specific implementation via their respective main.js configurations:

Based on the above, it is rather evident that the AppHelper module is mapped to the appropriate application specific implementation; MobileHelper for mobile, and DesktopHelper for desktop. Additional context specific APIs can just as easily be defined, and thus provided as dependencies to other modules as needed using this very simple pattern.

Implicit Dependencies

Dependencies need not always be explicit, but rather they can also be implicitly mapped based on the path to which each application’s main.js configuration resides, or based on the configured baseUrl path.

For instance, given the above example, we can map a Templates Module, and implicitly inject the path to each context specific template based on the application’s default path, or baseUrl path:

As can be seen, each application’s main.js defines a Templates module and a TemplateSource module, respectively, with each being shared amongst both the Mobile and Desktop specific applications. The Templates and TemplateSource modules are defined as follows:

While both the Mobile and Desktop applications may share the same Templates and TemplateSource modules, the specific implementation of the templates loaded from TemplateSource is determined via each application’s base path; thus, the path to app/templates/some-view.tpl automatically resolves to the context specific template; i.e.: mobile/app/templates/some-view.tpl for Mobile, and desktop/app/templates/some-view.tpl for Desktop.

Concluding Thoughts

While the above examples are rather basic, they do serve well to demonstrate just how easily one can design for module reuse across different applications with RequireJS, which itself allows for much more robust configurations of modules; such as loading context specific modules at runtime, augmenting modules for differing contexts with mixins, providing third-party libraries based on a particular form-factor (e.g. jQuery for Desktop, Zepto for Mobile, etc.), and more.

You can clone the above example here.

Tags: ,

{Sorry, Comments are currently Closed! }