DI framework makes sense for OOP

In Java (or most OOP languages):

Objects need to be created

In most of the cases they are stateful

Dependencies (state) often need to be injected

Order of the creation needs to be determined/given for the injection to work

Hence an IoC framework such as Spring makes perfect sense (in Java):

for example creating a dataSource, a sessionFactory and a txManager in Spring

DI framework “hurts functionally”

In Clojure (or similar functional languages):

Explicit objects with state and behavior are discouraged

Code organized in namespaces and small functions

Functions are directly referenced across modules/namespaces

DI/IoC framework would hurt all of the above: “beans” with functionality can only be accessed via creating other framework managed “beans”: very much like a need to create an Object to access another Object’s stateful functionality.

Business

Let’s say we need to find a user in a database.

we would need to connect to a database:

;; in reality would return a database connection instance ( defn connect - to - database [ { : keys [ connection - uri ] } ] { :connected-to connection - uri } ) ;; in reality would return a database connection instance (defn connect-to-database [{:keys [connection-uri]}] {:connected-to connection-uri})

and find a user by passing a database connection instance and a username:

;; pretending to execute a query ( defn find - user [ database username ] ( if ( :connection database ) ( do ( println "running query:" "SELECT * FROM users WHERE username = " username "on" database ) :jimi ) ( throw ( RuntimeException . ( str "can't execute the query => database is disconnected: " database ) ) ) ) ) ;; pretending to execute a query (defn find-user [database username] (if (:connection database) (do (println "running query:" "SELECT * FROM users WHERE username = " username "on" database) :jimi) (throw (RuntimeException. (str "can't execute the query => database is disconnected: " database)))))

examples are immediately REPL’able, hence we pretend to connect to a database, and pretend to execute the query, but the format and ideas remain.

Application Context

One way to use a stateful external resource(s) such as a database in the find-user function above, is to follow the Spring approach and to define an almost identical to Spring Lifecycle interface:

( defprotocol Lifecycle ( start [ this ] "Start this component." ) ( stop [ this ] "Stop this component." ) ) (defprotocol Lifecycle (start [this] "Start this component.") (stop [this] "Stop this component."))

Then define several records that would implement that interface.

By the way, Clojure records are usually used with methods (protocol implementations) that makes them “two fold”: they complect data with behavior, very much like Objects do. (Here is an interesting discussion about it)

( defrecord Config [ path ] Lifecycle ( start [ component ] ( let [ conf path ] ;; would fetch/edn-read config from the "path", here just taking it as conf for the sake of an example ( assoc component :config conf ) ) ) ( stop [ component ] ( assoc component :config nil ) ) ) (defrecord Config [path] Lifecycle (start [component] (let [conf path] ;; would fetch/edn-read config from the "path", here just taking it as conf for the sake of an example (assoc component :config conf))) (stop [component] (assoc component :config nil)))

( defrecord Database [ config ] Lifecycle ( start [ component ] ( let [ conn ( connect-to-database config ) ] ( assoc component :connection conn ) ) ) ( stop [ component ] ( assoc component :connection nil ) ) ) (defrecord Database [config] Lifecycle (start [component] (let [conn (connect-to-database config)] (assoc component :connection conn))) (stop [component] (assoc component :connection nil)))

( defrecord YetAnotherComponent [ database ] Lifecycle ( start [ this ] ( assoc this :admin ( find-user database "admin" ) ) ) ( stop [ this ] this ) ) (defrecord YetAnotherComponent [database] Lifecycle (start [this] (assoc this :admin (find-user database "admin"))) (stop [this] this))

Now as the classes (records above) are defined, we can create an “application context”:

( def config ( -> ( Config . { :connection-uri "postgresql://localhost:5432/clojure-spring" } ) start ) ) ( def db ( -> ( Database . config ) start ) ) ( def yet - another - bean ( -> ( YetAnotherComponent . db ) start ) ) ;; >> running query: SELECT * FROM users WHERE username = admin on #boot.user.Database{:config {:connection-uri postgresql://localhost:5432/clojure-spring}, :connection {:connected-to postgresql://localhost:5432/clojure-spring}} (def config (-> (Config. {:connection-uri "postgresql://localhost:5432/clojure-spring"}) start)) (def db (-> (Database. config) start)) (def yet-another-bean (-> (YetAnotherComponent. db) start)) ;; >> running query: SELECT * FROM users WHERE username = admin on #boot.user.Database{:config {:connection-uri postgresql://localhost:5432/clojure-spring}, :connection {:connected-to postgresql://localhost:5432/clojure-spring}}

and finally we get to the good stuff (the reason we did all this):

( :admin yet - another - bean ) ;; >> :jimi (:admin yet-another-bean) ;; >> :jimi

a couple of things to notice:

* Well defined order *

Start/stop order needs to be defined for all “beans”, because if it isn’t:

( def db ( -> ( Database . config ) ) ) ( def yet - another - bean ( -> ( YetAnotherComponent . db ) start ) ) ;; >> java.lang.RuntimeException: ;; can't execute the query => database is disconnected: boot.user.Database@399337a0 (def db (-> (Database. config))) (def yet-another-bean (-> (YetAnotherComponent. db) start)) ;; >> java.lang.RuntimeException: ;; can't execute the query => database is disconnected: boot.user.Database@399337a0

* Reality is not that simple *

All the “components” above can’t be just created as defs in reality, since they are unmanaged, hence something is needed where all these components:

are defined

created

injected into each other in the right order

and then destroyed properly and orderly

Library vs. Framework

This can be done as a library that plugs in each component into the application on demand / incrementally. Which would retain the way the code is navigated, organized and understood, and would allow the code to be retrofitted when new components are added and removed, etc. + all the usual “library benefits”.

OR

It can be done as a framework where all the components live and managed. This framework approach is what Spring does in Java / Groovy, which in fact works great in Java / Groovy.

.. but not in Clojure.

Here is why: you can’t really do (:admin yet-another-bean) from any function, since this function needs:

: access to yet-another-bean

: that needs access to the Database

: that needs access to the Config

: etc..

Which means that only “something” that has access to yet-another-bean needs to pass it to that function. That “something” is.. well a “bean” that is a part of the framework. Oh.. and that function becomes a method.

Which means the echo system is now complected: this framework changes the way you navigate, :require and reason about the code.

It changes the way functions are created in one namespace, :required and simply used in another, since now you need to let the framework know about every function that takes in / has to work with a “component”.

This is exactly what frameworks mean

When they talk about requiring a “full app buy in”

And while it works great for Java and Spring

In Clojure you don’t create a bean after bean

You create a function and you’re “keeping it clean”

“Just doing” it

In the library approach (in this case mount) you can just do it with no ceremony and / or changing or losing the benefits of the Clojure echo system: namespaces and vars are beautiful things:

( require ' [ mount . core :as mount : refer [ defstate ] ] ) (require '[mount.core :as mount :refer [defstate]])

( defstate config :start { :connection-uri "postgresql://localhost:5432/clojure-spring" } ) ( defstate db :start { :connection ( connect-to-database config ) } ) ;; #'boot.user/db (defstate config :start {:connection-uri "postgresql://localhost:5432/clojure-spring"}) (defstate db :start {:connection (connect-to-database config)}) ;; #'boot.user/db

( mount/start #'boot . user / db ) ;; {:started ["#'boot.user/db"]} (mount/start #'boot.user/db) ;; {:started ["#'boot.user/db"]}

( find-user db "admin" ) ;; running query: SELECT * FROM users WHERE username = admin on ;; {:connection {:connected-to postgresql://localhost:5432/clojure-spring}} ;; :jimi (find-user db "admin") ;; running query: SELECT * FROM users WHERE username = admin on ;; {:connection {:connected-to postgresql://localhost:5432/clojure-spring}} ;; :jimi

done.

no ceremony.

in fact the db state would most likely look like:

( defstate db :start ( connect-to-database config ) :stop ( disconnect db ) ) (defstate db :start (connect-to-database config) :stop (disconnect db))

Managing Objects

While most of the time it is unnecessary, we can use records from the above example with this library approach as well:

boot . user => ( defstate db :start ( -> ( Database . config ) start ) :stop ( stop db ) ) #'boot . user / db boot . user => ( defstate config :start ( -> ( Config . { :connection-uri "postgresql://localhost:5432/clojure-spring" } ) start ) :stop ( stop config ) ) #'boot . user / config boot.user=> (defstate db :start (-> (Database. config) start) :stop (stop db)) #'boot.user/db boot.user=> (defstate config :start (-> (Config. {:connection-uri "postgresql://localhost:5432/clojure-spring"}) start) :stop (stop config)) #'boot.user/config

and they become intelligently startable:

boot . user => ( mount/start ) { :started [ "#'boot.user/config" "#'boot.user/db" ] } boot . user => ( find-user db "admin" ) ;; running query: SELECT * FROM users WHERE username = admin on ;; #boot.user.Database{:config #boot.user.Config{:path {:connection-uri postgresql://localhost:5432/clojure-spring}, ;; :config {:connection-uri postgresql://localhost:5432/clojure-spring}}, ;; :connection {:connected-to nil}} ;; :jimi boot.user=> (mount/start) {:started ["#'boot.user/config" "#'boot.user/db"]} boot.user=> (find-user db "admin") ;; running query: SELECT * FROM users WHERE username = admin on ;; #boot.user.Database{:config #boot.user.Config{:path {:connection-uri postgresql://localhost:5432/clojure-spring}, ;; :config {:connection-uri postgresql://localhost:5432/clojure-spring}}, ;; :connection {:connected-to nil}} ;; :jimi

and intelligently stoppable:

boot . user => ( mount/stop ) { :stopped [ "#'boot.user/db" "#'boot.user/config" ] } boot . user => ( find-user db "admin" ) ;; java.lang.RuntimeException: can't execute the query => database is disconnected: ;; '#'boot.user/db' is not started (to start all the states call mount/start) boot.user=> (mount/stop) {:stopped ["#'boot.user/db" "#'boot.user/config"]} boot.user=> (find-user db "admin") ;; java.lang.RuntimeException: can't execute the query => database is disconnected: ;; '#'boot.user/db' is not started (to start all the states call mount/start)

Easy vs. Simple

While usually a great argument, this is not it.

In this case this is pragmatic vs. dogma