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}) |
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))))) |
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.")) |
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 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)) |
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}} |
and finally we get to the good stuff (the reason we did all this):
(: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 |
* 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”.
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]]) |
(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"]} |
(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)) |
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 |
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 |
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) |
Easy vs. Simple
While usually a great argument, this is not it.
In this case this is pragmatic vs. dogma
Mount relies on the Clojure compiler to tell it what states are defined and what order they should be started / stopped in. So, I thought, I can just use this intel, create a “system” and simply detach it from vars. In other words, using the intel mount has and only using the vars for the bootstrap I can spawn as many local systems as needed.