Fork me on GitHub
#component
<
2022-10-21
>
walterl19:10:08

Hi everyone! 👋 Component's README says: > "... a component is a collection of functions or procedures which share some runtime state." If there's no shared state, what is the Right Way to pass transitive dependencies around to downstream functions that need them? Example: Suppose I have a basic store web app. The Webserver component starts up a web server and hooks up some route handlers. Those handlers call functions in the app.store ns to (e.g.) fetch all products to render in a products list. I.e. app.store ns functions require database access via a Db component. Does this mean that the Webserver component should depend on the Db component, and pass it along to the app.store functions that need it? This strikes me as a bit odd, since, conceptually, the web server itself has nothing to do with the database. In other words, Webserver starts accumulating dependencies of downstream code. It becomes a bigger problem when (e.g.) the app.store code starts using a new external service, represented by another component. Now the web server code needs to know about it. Alternatively one could define a Store component, which has Db as a dependency, have the Webserver depend on Store, and pass that Store along to app.store functions. Those functions can then pick out the Db component from the Store. While the web server depending on a store makes more sense conceptually, this also feels weird/wrong, because the Store component has no runtime state, and essentially serves only as vessel for dependencies. Which is the Right Way, or is there a completely different and better way?

walterl19:10:30

Here's an alternative that has crossed my mind, but the global state feels icky:

(ns app.store
  (:require [app.db :as db]))

(defonce ^:private the-db (atom nil))

(defn list-products
  []
  (db/fetch-products @the-db))

(defrecord Store [db]
  component/Lifecycle
  (start [this]
    (reset! the-db db)
    this)

  (stop [this]
    (reset! the-db nil)))

(defn component
  []
  (component/using (map->Store {}) [:db]))

hiredman19:10:30

I like to make each handler it's own component, and the webserver depends on them all (possibly through an intermediate routes component)

hiredman19:10:02

And then each handler can depend on other components as needed

walterl19:10:06

That's clever! 💡 In that case your handler components are also "just" vessels for dependencies, so I'm taking that as a +1 for that approach.

hiredman19:10:16

At work I wrote a RingHandler record that has single field handler and when invoked as a function the ring handler adds any dependencies to the request map and passes it to the handler

💡 1
seancorfield19:10:49

@UJY23QLS1 We tend to have fairly narrowly-focused components. Our WebServer is just the HTTP server itself. Our Application is for the core, shared code, and includes caching, datasources, email templating, Redis, and several other stateful components that are used by pretty much everything. Then each web app tends to have a "system" component that combines our core Application, the WebServer, and any additional app-specific components (for example, our internal Admin-facing web apps have a component for dealing with O365 authentication).

seancorfield19:10:26

(`Application` is just a collection of other common components so it can be pushed into the Ring request and made available to handlers -- for the apps that don't have a separate component for each Ring handler, which is most of them)

walterl19:10:40

OK, seems like dependency vessel components are common enough. Thanks, both of you 🙂