Integrant brings order to the practice of defining, composing, and initializing
components. It introduces two architectural abstractions: systems and
components.
As defined above, a component is a computing thing that complies with an
interface. A system is just the composition of all components needed for
whatever application or service you’re trying to build. It’s the outermost
container for all those cute little components.
All of this is a bit abstract; let’s get concrete with some code:
simple integrant example
(ns integrant-duct-example.basic-components
(:require [integrant.core :as ig]))
(defmethod ig/init-key ::message-store [_ {:keys [message]}]
(atom message))
(defmethod ig/init-key ::printer [_ {:keys [store]}]
(prn (format "%s says: %s" ::printer store)))
(ig/init {::message-store {:message "love yourself, homie"}
::printer {:store (ig/ref ::message-store)}})
If you evaluate this code in a REPL, it will print the message,
":integrant-duct-example.basic-components/printer says: love yourself, homie"
.
Let’s work through it. The code, not loving yourself.
Integrant uses the multimethod init-key
to initialize components. Components
are identified by a keyword; this example has components named ::message-store
and ::printer
. The first argument to the multimethod is the component’s name,
and the second argument is the component’s configuration. The body of the
multimethod is the code for constructing and "running" a component. The return
value of ig/init-key
is a component instance, and it can be whatever
construct (atom, object, clojure data structure) you want other components to
interact with.
|
The term component is getting a little fuzzy here. I’ve been using it to
refer to a kind of conceptual entity that can be implemented in terms of a
definition and initialization process. But I’m also using it to refer to an
instance of a component, an actual language object that is returned by
ig/init-key and passed as an argument to other components. I’ve seen the
return value of ig/init-key referred to as a component but I find it useful to
refer to it as a component instance.
|
For ::message-store
the configuration only includes a :message
, but in real
systems component configurations would include things like the port for an HTTP
server to listen to, the max number of threads for a thread pool, or the URI for
a database connection.
::printer’s configuration has the key `:store
and value (ig/ref
::message-store)
. (ig/ref)
produces an integrant reference to the component
named ::message-store
. This makes it possible to pass the ::printer
component the initialized ::message-store component
.
Integrant’s ig/init
function initializes a system. Its argument is a map whose
keys are component names, and whose values are the configuration for that
component. ig/init
uses integrant references to initialize components in
dependency order. In the configuration above, the presence of (ig/ref
::message-store)
in ::printer’s configuration tells Integrant to initialize
the `::message-store
component before ::printer
. Then, when initializing
::printer
, it replaces the ::message-store
reference with the value returned
by (ig/init-key ::message-store)
.
|
ig/init returns a system instance. If you keep a reference to it you can
call ig/halt! or ig/suspend! on the system. Which brings me to another note:
Integrant includes a few other lifecycle methods for components:
ig/halt! and ig/halt-key! ; ig/suspend! and ig/suspend-key! ; plus a
couple more. Check out its README for more details.
|
We can see how Integrant helps us initialize (ig/init
, ig/init-key
) and
compose (ig/ref
) components, but what about defining components? Earlier I
said,
#+begin_quote
To define a component is to establish its responsibilities and its interface.
It also means choosing one or more language constructs to implement the notion
of "component".
#+end_quote
ig/init-key
does help to define a component in that it gives the component an
identity and imposes the constraint that a component be implemented as a single
thing that can get passed as a value to other components (which eliminates some
possibilities for defining components, like saying that namespace defines a
component.)
Integrant doesn’t really prescribe what Clojure language constructs you use to
implement a component; the return value of ig/init-key
can be whatever you
want.
That being said, it’s common to define component interfaces using protocols and
to have ig/init-key
return some object that implements the component’s
protocols. There’s some debate over whether or not it’s a good idea to use
protocols in this context, and ultimately that choice is up to you. I personally
prefer protocols because they force me to make good design choices, and as a
side benefit they make testing easier. As a consequence Sweet Tooth provides
some useful tools for creating test mocks for components that take the protocol
approach.
TODO explain component design more. Link to testing tools.