Fork me on GitHub
#duct
<
2019-09-04
>
Yehonathan Sharvit10:09:42

Hello there, I discovered integrant and duct a couple of weeks ago and I felt in love with the data approach they take.

Yehonathan Sharvit10:09:18

I have built a new app name Mr Hankey at work and I would be very happy if someone could review my config file

Yehonathan Sharvit10:09:36

Please share as much feedback as you can. I really want to learn the duct way! @weavejester

rickmoynihan15:09:40

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?

rickmoynihan15:09:36

I tend to favour only passing things the components actually require; at the cost of verbosity

Yehonathan Sharvit15:09:56

That’s an interesting tradeoff

Yehonathan Sharvit15:09:21

What do you think is the cost of inlfated responsibilites?

Yehonathan Sharvit15:09:46

I mean in that specific case of handlers

rickmoynihan15:09:42

- 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.

rickmoynihan15:09:06

would probably cover the main issues

rickmoynihan15:09:09

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.

Yehonathan Sharvit15:09:28

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

Yehonathan Sharvit15:09:53

But I don’t if it is doable in duct and also if it follows the spirit of duct

rickmoynihan15:09:20

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.

rickmoynihan15:09:11

But I also have some objections with the default duct template layout

rickmoynihan15:09:24

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.

Yehonathan Sharvit15:09:24

You mean renaming the keywords. Instead of :mr-hankey.handler.realm/list-realms have :mr-hankey.realm/list-realms?

rickmoynihan15:09:56

and the accompanying namespaces

Yehonathan Sharvit15:09:00

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?

rickmoynihan15:09:47

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.

rickmoynihan15:09:41

i.e. pattern is app.[vertical|feature].[horizontal]

rickmoynihan15:09:08

with a few things like app.lib or whatever for supporting concerns, likewise probably an app.middleware

Yehonathan Sharvit16:09:18

I need to think about how to apply this pattern in my case

Yehonathan Sharvit15:09:33

And why won’t you do stuff like that?

rickmoynihan15:09:53

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.

Yehonathan Sharvit15:09:52

But the cost of repeeating the components 10 times or more is very high!

rickmoynihan15:09:47

Yes, simple not easy 🙂

Yehonathan Sharvit15:09:13

It hurts but I tend to agree with you

rickmoynihan15:09:31

I don’t really understand why you want to group them together anyway.

Yehonathan Sharvit15:09:57

to avoid the need to declare the components again and again in the config file

Yehonathan Sharvit15:09:53

and also, it makes it easier in the REPL to access a componet

Yehonathan Sharvit15:09:56

I can write (-> system :components :mongo-connection) instead of (-> system :duct.database.mongodb/monger)

rickmoynihan15:09:33

everything in the system map is a component though.

rickmoynihan15:09:26

also not sure what you’re saving, you still need to have the config for each component.

rickmoynihan15:09:18

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.

rickmoynihan15:09:58

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.

rickmoynihan15:09:06

At the end of the day, duct config is pretty easy to write and maintain… even when you have thousands of lines of it.

rickmoynihan15:09:07

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.

Yehonathan Sharvit16:09:56

I need to think about it

Yehonathan Sharvit16:09:32

> 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”?

rickmoynihan16:09:18

Sorry, I meant “a standard way for your app”.

Yehonathan Sharvit17:09:01

Still, I don’t understand what you mean

rickmoynihan17:09:11

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.

Yehonathan Sharvit17:09:56

Can you show me a edn snippet that illustrates your point?

rickmoynihan17:09:21

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:

rickmoynihan17:09:25

Firstly the config:

rickmoynihan17:09:27

{
 :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]}}

 }

rickmoynihan17:09:11

Then the handler defmethod would be shared and might look something like this:

rickmoynihan17:09:15

(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"])))

rickmoynihan17:09:05

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.

rickmoynihan17:09:23

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]}

rickmoynihan17:09:07

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.