I'm working through the podcast again on my commute, and in today's episode, @nate was expressing his affinity for Component, and it made me wonder two things: 1. How do you and @neumann (and anyone else who wants to chime in) feel about Integrant by comparison? It seems to take the same "table of contents" approach of having a system map, so I wonder if there's anything else there, beyond familiarity, that would make you reach for Component over it 2. Integrant and Component kind of remind me of the old days of Spring DI, where everything was wired together with XML. Everyone dumped on it, and then we got the annotations approach that I guess everyone loves? But the annotations approach seems to me to have the problem of "multiple entry points" and no table of contents: the wiring is magic and there's no clear place to look to get a cohesive picture. Does replacing XML with EDN really make so much of a difference that the old Spring DI approach is good again?
I just want to take a moment to thank everyone on this thread for sharing from their experience. I'm finding the comparison and contrast helpful. Thanks! @jason.bullers @nate @lukaszkorecki @markbastian @jr0cket
Ah, I hadn't considered navigation as a difference. That's interesting. So with a system map, you have the leaf nodes as symbols, allowing you to navigate directly to the source of the component, whereas with Integrant, the leaf nodes are keywords, and you've got to do some mapping (even if it's trivial, like removing the : prefix) to get from one to the other. I suppose the hierarchy of keywords can also complicate this kind of lookup
How do you feel about everything being a record in Component?
Not Nate but I never quite understood why we have any alternatives to Component ;-)
nowadays you don't even have to use records - https://github.com/lukaszkorecki/utility-belt/blob/73ef8c073a09b2f2edc691950106c675f1b95532/src/utility_belt/component.clj#L56 and if you want almost declarative way of defining Component systems, https://github.com/lukaszkorecki/oreo
That's true, anything can be a component (atom, function, core async channel). It just won't get lifecycle methods called on it. But if it doesn't need those, then just throw it in. I've used bare atoms as components in the past.
As far as using a record instead of a multimethod (or macro like defstate) for initialization and teardown, I think they're all just different forms of the same, and that it doesn't really matter. It's not a very big part of the application (hopefully), so most of the time is spent elsewhere using the components to get stuff done. So, most of the time it's looked at is when you're either learning the system or updating it's design, and discoverability is king in both cases.
@nate since you used Integrant in anger - I wonder how you'd tackle this. At $dayjob I created a component which is a Mongodb connection (I know, I know...), it implements the Lifecycle protocol but also a bunch of interfaces from Mongo's Java driver, so for all intents and purposes it behaves just like the standard connection instance. Can you do something like that with Integrant?
Initialization in Integrant is done via a multimethod, so whatever it returns is the "component". So I think you would create an instance of the Mongodb record (that implements everything but Component's Lifecycle) and return it.
ah gotcha, so there's that extra wiring needed, with Component I can have one thing basically
@jason.bullers I affirm @nate's comments. I really like having the system map with explicit relationships. I really hate having to hunt through a codebase for related things. I think your question about XML is also worth addressing. I think the information about how to wire the application together belongs in the code and not as a separate artifact outside of the code (XML file). Changing the wiring has application-level considerations, and requires all the same consideration and testing as other coding decisions. If you need variation for operational reasons (eg. using the Real Thing™ in prod but not dev), make a config option for that scenario, and keep the problem of determining the "right" wiring for it in the code. @nate and I have written code that generates different Component maps based on different config options—it's testable too.
I'm an integrant fan. It feels more data-driven since you just have a config map. I find it easier to implement and use. That said, Component is a great solution, so use what works for you.
Yeah, I've found a lot of things tend to go that way: certain tools or approaches just click well and sit right with personal mental models. I do really like some of the data driven solutions in Clojure land: Integrant, reitit, replicant. There's certainly something there that tickles my brain, but there's also a part of me that feels nervous about how the linkages are always at least one step removed: there's coupling, but it's not direct and in your face, and so it feels maybe a little "cleaner" but maybe that's just because certain parts have been swept under the rug rather than done away with entirely. And it's certainly true that sometimes, you just don't need to look under the rug at all, and maybe that's fine. I don't have enough experience with either, so it's great to get different perspectives and a sense of why it fits different people's models. My own DI experience is in Java land, and I've certainly got... opinions about overuse, excessive granularity, and not being able to see the forest for the trees
I am still a fan of Component. I've started using Integrant in a personal side project (because it was based on a https://kit-clj.github.io/ starter), so I now have experience with it in anger. I like multimethods and how they can be extended in multiple namespaces. Integrant uses that to allow initialization in various places in the app. However, I still gravitate back toward the explicit linking that Component uses. If I want to see how my application is stitched together, I go to the main namespace and then I can use lsp-powered source jumps to get to each component. Simple. In Integrant, I have to resort to source greps (which I've made relatively convenient) to get to the initialization code for a component. The multimethod stuff bit me a bit when I went to look for the http server component by grepping and found it was actually in the Kit library (and therefore not in my local tree). Wiring things together in XML was definitely cumbersome (as was anything in XML), but then annotations felt like swinging the pendulum to the other side and having magic assemble your app. I like the code-based solution that Component provides as a good compromise between explicitness (I specify the dependencies) and magic (it does the initialization in the right order).
I really enjoy using https://github.com/donut-party/system
It feels to me a much simpler approach than other component libraries (hash-map & clojure functions).
Its easy to defined a system (and variations) as a map.
The start and stop are normal functions (lambda or shared).
Its easy to define different systems for dev (and other cases) by defining a separate map or defining a base system and merging only the differences from a dev maps.
There was no need for a separate REPL library with donut (unlike Integrant & Component)
Intergrant was my preferred component library before donut, but Integrant felt more complicated as it didnt use function definitions. I had to relearn a few things each time I used it to feel confident.
Integrant did encourage the use of juxt/aero, which is a great way of defining different profiles (although I found it is easy to get carried away with this)
I was put off Stuart Sierra's Component library initially, as it used records and I felt too much like the Java I had done for many many years. I never looked at it since.
For simpler systems (e.g. only one component or no component relationships) I will use mount or write my own stop/start/restart clojure functions (with an atom for state.
In my dev/user.clj file I load a https://practical.li/clojure-web-services/service-repl-workflow/system-repl/ file, where I define start/stop/restart functions to abstract over what ever component library (or not) I am using