Fork me on GitHub
#component
<
2021-06-17
>
stephenmhopper23:06:26

I have another design question. I’ve now moved my app over to use component. However, I’m not entirely sure if I’ve designed things properly and could use some advice. Almost everything in my application is now idempotent in that whatever context a function requires in order to execute is passed in as a parameter. Before the move to component, I had several client namespaces, each for calling a different external API. Each one had something like this: (defonce conf-atom (atom nil)) . The value of conf-atom would be initialized when my app started up (and sometimes updated if conf-atom had tokens which expired and needed refreshed regularly). Just about every public function in an API client namespace accepts conf as the first parameter. (This value was typically just @conf-atom , but the deref happened in the code calling the API function). Now that I’m on component, the API layer remains unchanged. But namespaces that call into an API namespace can no longer deref the conf-atom (since it’s gone now) and must take the necessary conf as a parameter. This has greatly increased the average number of parameters per function throughout my codebase and has left me wondering if I’m doing something wrong. For example, I have a top-level process which requires 6 different “confs” as the functions it calls interact with 6 different external APIs (or DBs) in some fashion. On the one hand, this is better than it was before as I can look at any function in my codebase and know exactly which external systems it might try to invoke. Furthermore, I think it will be easier to write tests than it was before. On the other hand, the function signatures throughout my codebase are more cumbersome (on average) than they were before. So I’m wondering if there’s not a better way to structure things. Have you run into this design issue before? How do you typically address this?

seancorfield23:06:18

You have a single “system” component and all of the external API configs are subcomponents of that?

seancorfield23:06:44

It’s common to pass “system” around at the top-level and then pass just the piece(s) necessary at the lower levels. So I would not expect “the average number of parameters per function throughout my codebase” to “greatly increase[d]” because most things are just going to pass the “system” through if they don’t care about it.

seancorfield23:06:15

Only in code that cares about parts of the system rather than the whole system do you need to pick it apart. Since you calling-into-API code used to pass @conf-atom, I would expect that to be the point where you extract the appropriate part from “system” and pass it down into the actual API code. If I’m understanding your structure correctly.

seancorfield23:06:35

At work, we pass a “system” through most code until we get to a layer that only needs/cares about one or a few parts of it, and at that point we’ll pass (:environment app) or (:caches app) or (-> app :database :pooled-db) down into the next layer. That latter case is to recognize that it can be up to the calling code to decide whether to pass the main db-spec or a reporting db-spec (different credentials/pooling setup) etc.

stephenmhopper23:06:53

Yeah, that makes sense, and I would have taken that approach sooner except the “https://github.com/stuartsierra/component#usage-notes” section on component reads, “Do not pass the system around” and “No function should take the entire system as an argument.” The suggested alternative in the usage notes is this:

Rather, each function should be defined in terms of at most one component.

If a function depends on several components, then it should have its own component with dependencies on the things it needs.
So it sounds like the suggestion is to just pass around one map which is some subset of the system then, no? What are your thoughts?