clojurereitit

avoid circular dependency when I access reitit route info from handler


Assuming I have some kind of router set up that maps some routes to handlers something like this...

(ns myapp.user.api
  (:require [reitit.core :as r]))

; define handlers here...

(def router
  (r/router
    [["/user" {:get {:name ::user-get-all
                     :handler get-all-users}}]
     ["/user/:id"
      {:post {:name ::user-post
              :handler user-post}}
      {:get {:name ::user-get
             :handler user-get}}]]))

And those handlers then call services that want access to the routing information...

(ns myapp.user-service
  (:require [myapp.user.api :as api]))


; how can I get access to the route properties inside here..?
(defn get-all-users [])
  (println (r/route-names api/router)))

When I try to import the router from the api file, into the service, I get a problem with circular dependencies, because the api requires handler, which requires service, so service can not then require api.

What's the best way to avoid this circular dependency? Can I look up values and properties of the router from within services?


Solution

  • I use six general approaches to avoid circular dependencies in clojure. They all have different tradeoffs and some situations one will fit better than another. I list them in order from what I prefer most to what I prefer least.

    I show one example for each below. There may be more ways I haven't thought of, but hopefully this gives you some ways of thinking about the issue.

    1. Refactor the code to remove the commonly referenced vars into a new namespace and require that namespace from both original namespaces. Often this is the best and simplest way. But can't be done here because the root handler var is a literal containing a var from the other namespace.

    2. Pass in the dependent value into the function at runtime so as to avoid having to require the namespace literally.

    (ns circular.a)
    
    (defn make-handler [routes]
      (fn []
        (println routes)))
    
    (ns circular.b
      (:require [circular.a :as a]))
    
    (def routes
      {:handler (a/make-handler routes)})
    
    ;; 'run' route to test
    ((:handler routes))
    
    1. Use multimethods to provide the dispatch mechanism, and then defmethod your binding from the other namespace.
    (ns circular.a
      (:require [circular.b :as b]))
    
    (defmethod b/handler :my-handler [_]
      (println b/routes))
    
    (ns circular.b)
    
    (defmulti handler identity)
    
    (def routes
      {:handler #(handler :my-handler)})
    
    (ns circular.core
      (:require [circular.b :as b]
    
                ;; now we bring in our handlers so as to define our method implementations
                [circular.a :as a]))
    
    ;; 'run' route to test
    ((:handler b/routes))
    
    1. Use a var literal that is resolved at runtime
    (ns circular.a)
    
    (defn handler []
      (println (var-get #'circular.b/routes)))
    
    (ns circular.b
      (:require [circular.a :as a]))
    
    (def routes
      {:handler a/handler})
    
    ;; 'run' route to test
    ((:handler routes))
    
    1. Move the code into the same namespace.
    (ns circular.a)
    
    (declare routes)
    
    (defn handler []
      (println routes))
    
    (def routes
      {:handler handler})
    
    ;; 'run' route to test
    ((:handler routes))
    
    1. Use state. Store one of the values in an atom at runtime.
    (ns circular.a
      (:require [circular.c :as c]))
    
    (defn handler []
      (println @c/routes))
    
    (ns circular.b
      (:require [circular.a :as a]
                [circular.c :as c]))
    
    (def routes
      {:handler a/handler})
    
    (reset! c/routes routes)
    
    ((:handler routes))
    
    (ns circular.c)
    
    (defonce routes (atom nil))