Hello there, I discovered integrant and duct a couple of weeks ago and I felt in love with the data approach they take.
I have built a new app name Mr Hankey at work and I would be very happy if someone could review my config file
Please share as much feedback as you can. I really want to learn the duct way! @weavejester
looks fine to my eyes @viebel… Only thing that stands out is having an intermediate aggregating component… presumably to avoid the boilerplate. So long as you’re happy with the trade offs that brings in terms of convenience vs potentially inflated responsibilities, e.g. is it the case that ::list-dbs
needs the :rabbitmq-producers
?
I tend to favour only passing things the components actually require; at the cost of verbosity
That’s an interesting tradeoff
What do you think is the cost of inlfated responsibilites?
I mean in that specific case of handlers
- Potentially security (though unlikely in practice) - Understandability, i.e. if I’m debugging a component and I see it has something given to it, I expect it to use it… - Minimising dependencies is usually a good practice; as it can introduce artificial problems… e.g. you might end up starting an otherwise unused component and never know… that component may be unused but prevent your app from starting without it… e.g. your app may require a connection to a service during init, and expect that service to be there… but in practice never use it.
would probably cover the main issues
but you might be able to split handlers into for example readers and writers… and mitigate that way whilst retaining more of the DRYness you have.
Good idea
Another thing that bothers me is that all the keys for the handlers appear at the root of the config. I would prefer to nest them into a :handlers key
But I don’t if it is doable in duct and also if it follows the spirit of duct
What do you think @rickmoynihan?
I personally wouldn’t do that.
What you might do, is abuse the profiles feature to group things together… derive :mr-hanky/handlers
from :duct/profile
, and group the handlers there. Then include the profile in the set that get merged.
But I doubt it’s the duct way — if there is such a thing beyond the basics of integrant etc.
But I also have some objections with the default duct template layout
What objections?
And why won’t you do stuff like that?
Firstly duct is great. I don’t like to criticise.
But I strongly dislike frameworks and templates that group by the incidental complexity of web apps; rather than by the concepts of your domain.
So I really, really dislike having app.handlers
app.models
app.views
and vastly prefer app.feature-1
app.feature-2
…
I think grouping handlers is following that trend. Grouping by feature, and I’d be with you.
You mean renaming the keywords. Instead of :mr-hankey.handler.realm/list-realms
have :mr-hankey.realm/list-realms
?
and the accompanying namespaces
yes
Well you’re creating a dummy intermediate component; that only serves to group things. It doesn’t need the things it groups. To me it feels artificial.
Would you put the code that connects to the db also in this namespace? Or would you keep the tradidional split bewteen handlers namespaces and models namespaces?
But the cost of repeeating the components 10 times or more is very high!
Yes, simple not easy 🙂
It hurts but I tend to agree with you
I don’t really understand why you want to group them together anyway.
to avoid the need to declare the components again and again in the config file
I definitely would yes.
If features need to views and database models etc… and theres enough code to warrant splitting those concerns, then yes I’d have app.feature-1.db
app.feature-1.view
whatever you want.
and also, it makes it easier in the REPL to access a componet
i.e. pattern is app.[vertical|feature].[horizontal]
I can write (-> system :components :mongo-connection)
instead of (-> system :duct.database.mongodb/monger)
with a few things like app.lib
or whatever for supporting concerns, likewise probably an app.middleware
everything in the system map is a component though.
also not sure what you’re saving, you still need to have the config for each component.
You can dedupe that in various ways with duct…
- Modules is one (it’s very heavy handed and increases complexity a lot in my mind) — it’s kinda like macros for duct config data, but less terse, and less constrained.
- ig/prep-key
- derived components
- composite keys
- and refsets.
If for example all of your handlers follow the same shape/pattern you can have one defmethod
for say a ::html-rendering-handler
. It would then take all the standard stuff, :db
a :view
component etc… and wire them together in the a standard way.
Then all you would have is config, along with view components, and :db-model
components… You’ll end up with smaller components and more config. It’s the duct way. If the quantity of config then bothers you and you have sufficient commonality you can generate the config with modules… though I personally think modules tend to be a step too far.
At the end of the day, duct config is pretty easy to write and maintain… even when you have thousands of lines of it.
But we run a multi-tennant duct app… so each customer has a profile of config, that is merged over a common core layer. It’s not a standard duct app by any means.
I need to think about it
I need to think about how to apply this pattern in my case
> It would then take all the standard stuff, :db
a :view
component etc… and wire them together in the standard way.
What do you mean by “wire them together in the standard way”?
Sorry, I meant “a standard way for your app”.
Still, I don’t understand what you mean
:thinking_face:
I just mean that if you can create a common abstraction across all your handlers or a subset of them; then you can use that to reuse code, effectively trading it for more config.
Can you show me a edn snippet that illustrates your point?
ok this isn’t necessarily suitable for everyone… it depends on what you’re doing — also there are other decisions/choices one can make. For example the view and handler separation may be overkill depending on what you’re doing… Also this is not tested just written into slack — so just illustrative of the ideas:
Firstly the config:
{
:app/layout {:db #ig/ref :app.feature-1/db}
:app/db {,,,}
:app.feature-1/view _
:app.feature-2/view _
:app.feature-1/model {:db #ig/ref :app/db}
:app.feature-2/model {:db #ig/ref :app/db}
[:app.handler :feature/1] {:view #ig/ref :app.feature-1/view
:layout #ig/ref :app/layout
:model #ig/ref :app.feature-1/model}
[:app.handler :feature/2] {:view #ig/ref :app.feature-2/view
:layout #ig/ref :app/layout
:model #ig/ref :app.feature-2/model}
:duct.router/ataraxy {:routes
{[:get "/feature-1"] :feature-1
[:get "/feature-2"] :feature-2}
:handlers {:feature-1 #ig/ref [:app.handler :feature/1]
:feature-2 #ig/ref [:app.handler :feature/2]}}
}
Then the handler defmethod would be shared and might look something like this:
(defmethod ig/init-key :app.handler [_ {:keys [layout db view]}]
(fn [req]
(if-let [view-model (model req)]
[::response/ok (layout (view view-model req)
req)]
[::response/not-found "Not found"])))
Our apps nav is driven by the data, so we made the layout/navbar etc a component in its own right, that gets given to every handler.
Also perhaps a page in your app is made of several panels… they can also be components… so perhaps you have:
[:app.page :page/dashboard] {:panel [#ig/ref :app.foo/panel
#ig/ref :app.bar/panel
#ig/ref :app.baz/panel]}
and the page doesn’t see the insides of those panels at all. Really depends on the granularity of HTTP responses to panels etc; and also what level of reuse you need.