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