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.

Passing dependencies

Generally speaking, there are only three ways a component can get a hold of its dependencies:

  1. The component can create the dependency, typically by binding a new object to an environment in R.
  2. The component can look up the dependency, by referring to a global variable.
  3. The component can have the dependency passed to it where it is needed.

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.

Using a service locator

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.

Relying on a framework

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()


openscienceunil/modulr documentation built on May 3, 2019, 5:49 p.m.