set.seed(1234L) library(networkD3) library(dplyr) knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.path = "motivation-" ) Sys.setlocale("LC_TIME", "en_DK.UTF-8") Sys.setenv(TZ = 'UTC') BUILD <- identical(Sys.getenv("BUILD"), "true") && !identical(Sys.getenv("TRAVIS"), "true") && identical(Sys.getenv("NOT_CRAN"), "true") knitr::opts_chunk$set(purl = BUILD)
This article motivates the use of Dependency Injection (DI) in R from a rather academic and theoretical point of view, that should be complemented with a more practical approach, as exposed in the package's vignette for instance (see vignette("modulr")
). It mainly resumes the motivation and explanation given for Angular's use of DI (credit to Google). For in-depth discussion about DI, see Dependency Injection at Wikipedia, Inversion of Control by Martin Fowler, or read about DI in your favorite software design pattern book.
Generally speaking, there are only three ways a component can get a hold of its dependencies:
The first two options of creating or looking up dependencies are not optimal because they hard code the dependency to the component. This makes it difficult, if not impossible, to modify the dependencies. This is especially problematic in tests, where it is often desirable to provide mock dependencies for test isolation.
The third option is the most viable, since it removes the responsibility of locating the dependency from the component. The dependency is simply handed to the component. To illustrate this, let's build a simplistic car composed of a roaring engine and some gleaming wheels.
# This function returns a roaring engine. engine_provider <- function() { list( start = function() message("Engine started."), stop = function() message("Engine stopped.") ) } # This function returns gleaming wheels. wheels_provider <- function() { list( roll = function() message("Wheels rolling."), brake = function() message("Wheels braking.") ) } # This function returns a car provided with an engine and some wheels. car_provider <- function(engine, wheels) { list( start = function() { message("Car started.") engine$start() }, drive = function(speed, destination) { wheels$roll() message("Car driving at ", speed, " to ", destination, ".") wheels$brake() }, stop = function() { engine$stop() message("Car stopped") } ) }
In the above example, car_provider
is not concerned with creating or locating the engine
and wheels
dependencies, it is simply handed the engine
and wheels
when it is called. It is desirable, but it puts the responsibility of getting hold of the dependencies on the code that calls car_provider
.
engine <- engine_provider() wheels <- wheels_provider() car <- car_provider(engine, wheels) car$start() car$drive("50 km/h", "home") car$stop()
For instance, if one decides to change wheels
, a new dependency has to be explicitly created and passed to car_provider
.
To manage the responsibility of dependency creation, modulr relies on an injector. The injector is a service locator that is responsible for construction and lookup of dependencies. Here is an example of using the injector service:
Create a new injector that can provide modules.
library(modulr) my_injector <- new_injector()
Teach the injector how to build the car
, engine
and wheels
modules. Notice that car
is dependent on the engine
and wheels
modules.
my_injector$provider( name = "car", dependencies = list(engine = "engine", wheels = "wheels"), provider = car_provider) my_injector$provider(name = "engine", provider = engine_provider) my_injector$provider(name = "wheels", provider = wheels_provider)
Request our car
module from the injector.
car <- my_injector$get("car") car$start(); car$drive("120 km/h", "the University of Lausanne"); car$stop()
In this setting, changing wheels
is then straightforward:
my_injector$provider( name = "wheels", provider = function() { list( roll = function() message("Brand-new wheels rolling."), brake = function() message("Brand-new wheels braking.") ) } ) car <- my_injector$get("car") car$start(); car$drive("150 km/h", "the University of Lausanne"); car$stop()
Notice that the injector did only re-evaluate wheels
and car
, while engine
was kept untouched: modulr treats modules as singletons.
Asking for dependencies solves the issue of hard coding, but it also means that the injector needs to be passed throughout the application. Passing the injector breaks the Law of Demeter. To remedy this, we combine the use of an ambient injector (a default injector is bound to modulr) and a declarative notation, to hand the responsibility of creating modules over the injector, as in this example:
modulr::reset()
"car" %requires% list(engine = "engine", wheels = "wheels") %provides% car_provider "engine" %provides% engine_provider "wheels" %provides% wheels_provider car <- make("car") car$start(); car$drive("120 km/h", "the University of Lausanne"); car$stop()
When modulr makes a module, it asks the ambient injector to create the dependencies. The injector infers the names of the dependencies by examining the module declaration, constructs the related directed acyclic graph and computes a topological sort to produce a well ordered sequence of evaluations. This is all done behind the scenes.
# Read from right to left to follow the dependencies. plot_dependencies()
This is the best outcome. The application code simply declares the dependencies it needs, without having to deal with the injector. This setup does not break the Law of Demeter.
Finally, here is how the above example typically looks like with the use of modulr DI's philosophy and implementation:
modulr::reset()
library(modulr) "car" %requires% list( engine = "engine" ) %provides% { #' This module can start, drive and stop a car. # It just returns a list of methods. list( start = function() { message("Car started.") engine$start() }, drive = function(speed, destination) { wheels$roll() message("Car driving at ", speed, " to ", destination, ".") wheels$brake() }, stop = function() { engine$stop() message("Car stopped") } ) } "engine" %provides% { #' This module can start and stop an engine. list( start = function() message("Engine started."), stop = function() message("Engine stopped.") ) } "wheels" %provides% { #' This module can roll and brake wheels. list( roll = function() message("Wheels rolling."), brake = function() message("Wheels braking.") ) } info("car") ## `info()` outputs #'-comments (aka docstrings) car %<=% "car" ## syntactic sugar for `<- make(` car$start(); car$drive("the speed of light", "the boundaries of the universe"); car$stop()
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.