A dependency injector for Clojure and ClojureScript.
Dependency injection refers to the process of deferring bindings between components in a system. Exhibiting dependencies allows to reify the system topology as plain data, loosening coupling and increasing modularity.
injure
aims to capture the essence of this pattern, providing helpers to describe component systems as data and arbitrarily resolve them later. Lifecycle management and other OO-related details are outside of the scope of this library.
Dependency resolution happens at compile-time and produces plain local bindings, avoiding hashmap lookups. Therefore, the resulting system has zero run-time overhead and will perform as fast as a hardwired one.
injure
exposes a single namespace injure.core
with 2 macros, target
and inject
.
(require '[injure.core :as i])
target
requires a dependency. It takes an arbitrary number of compile-time forms and emits a symbol bound to the resolution of the dependency matched by provided forms. It is illegal to call target
outside of an inject
context, but resolution can be deferred with quoting.
(def system ;; defines a system of 3 components identified by keywords :a, :b, :c
`{:a 6
:b (inc (i/target :a))
:c (* (i/target :a) (i/target :b))})
inject
performs the dependency resolution of a fully defined system. The first argument must evaluate (compile-time) to a function providing forms associated to required targets (arguments to target
will be passed as-is). Next arguments are forms to be evaluated within this context.
(i/inject system (str (i/target :c))) ;; emits (let [a 6, b (inc a), c (* a b)] (str c))
The following patterns will be illustrated with an example taken from plumatic/plumbing, a library providing a similar feature (with a different strategy, however).
First, let's define stats-graph
, a partial dependency graph defining a bunch of statistics operations that can be performed on an input sequence of numeric values.
(def stats-graph
`{:n (count (i/target :xs))
:m (/ (reduce + (i/target :xs)) (i/target :n))
:m2 (/ (reduce + (map #(* % %) (i/target :xs))) (i/target :n))
:v (- (i/target :m2) (* (i/target :m) (i/target :m)))})
We can abstract away input to build a plain function performing the minimal subset of required computations :
(defn mean [xs] ;; computes :n and :m
(i/inject (assoc stats-graph :xs 'xs)
(i/target :m)))
(defn mean-and-variance [xs] ;; computes :n :m :m2 and :v
(i/inject (assoc stats-graph :xs 'xs)
[(i/target :m) (i/target :v)]))
(mean [1 2 3 6]) ;; returns 3
(mean-and-variance [1 2 3 6]) ;; returns [3 7/2]
We can unit test each computation step by binding inputs to static values and validating results according to these inputs :
(deftest unit
(i/inject (assoc stats-graph :xs [1 2 3 6])
(is (= (i/target :xs) [1 2 3 6]))
(is (= (i/target :n) 4))
(is (= (i/target :m) 3))
(is (= (i/target :m2) (/ 25 2)))
(is (= (i/target :v) (/ 7 2)))))