On http://mindbat.com/2013/03/clojurewest-2013-day-one-notes/ there is a note that reads:
I think this is good advice but I am not totally sure how to implement this in a Ring/Compojure application. Can anyone give a concrete example of how this would work?
I'm specially interested how to combine defroutes
, init
and app
together this way and get rid of global variables in that scope.
What I understand from Stuart's talk is something like this:
(ns state.core)
(defn create-user-module [] (atom []))
(defn add-user [module user]
(swap! module conj user))
(defn get-users [module]
@module)
Now there is no global state in your "core" as the functions that manipulate the state expect to get it as a parameter. These allows easy testing as you can create a new instance of the "user-module" for each test. Also, the clients of this module shouldn't care about what they get in the create-user-module function, they should just pass it around without inspecting it, that way you can change the user module implementation whenever you want. Stuart also talks about creating protocols for those modules if you are going to have more than one implementation.
Trying to answer your question, a ring adapter is just a function of 1 param, and compojure is just a routing library, so you could create a web-app using closures like:
(ns state.web
(:use compojure.core)
(:require [state.core :as core]))
(defn web-module [user-module]
(routes
(GET "/all" [] (core/get-users user-module))))
Now you can call the web-module to create a webapp, passing as a parameter the dependencies needed. Of course you still need somebody to create the web app with the correct user-modules, so you just need a "main" function that wires everything together:
(ns state.main
(:require state.core
state.web)
(:use ring.adapter.jetty))
(defn start []
(let [user-module (state.core/create-user-module)
web-module (state.web/web-module user-module)]
(run-jetty web-module {:port 3000 :join? false})))
(defn stop [app]
(.stop app))
start
will be called from your app main
method. This just means that you need to switch to the lein-run plugin.
Now, given that you are asking about init
(from the lein ring plugin I assume), I guess that you plan to deploy your webapp in a container. As the lein ring plugin has to work within the java servlet fw constraints and that the handler ends up compiled to a java servlet, the best that you can probably do is something like:
(ns state.web
(:use compojure.core)
(:require [state.core :as core]))
(def module-deps (atom {})
(defn init-app [] (swap! module-deps conj [:user-module (core/create-user-module)]))
(defroutes web-module []
(GET "/all" [] (core/get-users (:user-module @module-deps))))
This still means that your core namespace is easy to test, but you still have global state in the web namespace, but I think that is "properly" encapsulated and probably is good enough if you have to use a java container.
And this is just another argument of why libraries are "better" than frameworks :)
There are many cases where you need global state hence you cannot avoid it, what you can do is manage it properly and I guess that's what the 2 points talk about:
Not a good way:
(ns data)
(def users (atom []))
(ns pages)
(defn home []
(do-something data/@users)
(defn save []
(let [u @users]
(swap! data/users ....)
Good way:
(ns data)
(def- users (atom []))
(defn get-users [] @users)
(defn update-user [user] (swap! @users ...))
(ns pages)
; use the functions exposed by data ns to interact with data rather then poking the atom directly.
Basically all the access to any type of state should be abstracted away from other parts of the application. All your business logic function should take the state as parameter and return new state rather than picking the state themselves and updating it directly.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With