Fork me on GitHub
#polylith
<
2024-04-25
>
tengstrand04:04:46

A Polylith song.

5
polylith 5
ag07:04:51

I have a toddler's question: why is that in the documentation, in poly project and in realworld-example-app the workspace level :extra-deps are prefixed with poly/, and in user-manager-example they are like this:

:extra-deps {poly-usermanager/web {:local/root "bases/web"}
             poly-usermanager/app-state {:local/root "components/app-state"}
             ...
Is it because these are :local/root deps, and it doesn't really matter, and you can have them like foo/web, bar/app-state, etc.?

tengstrand07:04:28

Yes, that's the reason (`poly/` is just a convention). As long as you don't share code outside the workspace, you can use any valid prefix.

šŸ‘ 2
jasonjckn05:04:39

in our project, we use the interface name

tengstrand06:04:02

Yes, that should work too, because interface names are unique per project.

šŸ‘ 1
Vladimir Bokov09:04:39

Regarding https://clojurians.slack.com/archives/C013B7MQHJQ/p1712118156257669?thread_ts=1712106688.405469&cid=C013B7MQHJQ I took a look into seancorfield/usermanager-example and noticed schema component https://github.com/seancorfield/usermanager-example/blob/polylith/components/schema/src/usermanager/schema/core.clj#L2C14-L2C34 usermanager.database.interface like you said above ā€¢ schema doesn't specify the dependency on database in components/schema/deps.edn like you said ā—¦ imo it's unexpected, but let's assume it's ok ā€¢ there's {:local/root "../../components/database"} in projects/usermanager/deps.edn ā—¦ but if I comment it out this single line, I'm still able to run clojure -M:dev -m usermanager.web.main and the app works ā—¦ building the app clojure -T:build uber fails ā€¢ there's {:local/root "components/database"} in root deps.edn ā—¦ if I comment it out, app cannot start with clojure -M:dev -m usermanager.web.main regardless dependency stated in projects/usermanager/deps.edn TLDR: effectively for repl/dev only toplevel deps.edn matters, for the build - only projects/usermanager/deps.edn, do I miss anything @seancorfield? imo it's a bit error-prone and non-intuitive when you try to think about components as dependencies, the most expected behaviour (again imo) would be ā€¢ either including :local/root only in components/schema/deps.edn and hook/patch into tools.deps or preprocess/generate projects/*/deps.edn and toplevel deps.edn ā€¢ or force people to a total DI and pass all database functions as arguments into schema (looks too enterprisy tbh)

tengstrand10:04:52

> ā€¢ schema doesn't specify the dependency on database in components/schema/deps.edn like you said > ā—¦ imo it's unexpected, but let's assume it's ok Bricks (components and bases) only know about interfaces (not other bricks). That's why the concrete implementation of an interface (component) is specified by each project, so that the implementation of an interface can vary between projects.

Vladimir Bokov12:04:50

ok, so this is kinda DI on namespace level? in comparison to DI on argument level when we could rely on actual definterface (including system's component DI being passed through)

tengstrand12:04:38

On namespace level yes, but they are not injected. The bricks become available when we specify them in a project, in the same way we specify libraries (which are also not injected). I tend to see bricks (and libraries) as living side by side. I try to explain this in the Composable section in https://medium.com/@joakimtengstrand/understanding-polylith-through-the-lens-of-hexagonal-architecture-8e7c8757dab1 blog post.

Vladimir Bokov13:04:14

Here we illustrate how bricks are used across projectsthanks for the article, however interestingly I noticed a "development" project after this line. does it mean there should be no toplevel deps.edn at all (but projects/development/deps.edn instead)?

tengstrand13:04:08

The development project is configured in ./deps.edn and not under projects as other projects.

tengstrand13:04:55

Bases can depend on other bases, but that's quite unusual.

Vladimir Bokov13:04:42

ok, got it, the projects/development/deps.edn -> ./deps.edn was forced by something like projectile looking for repl's deps.edn and trying to find files in this subfolder only? or any additional more important reason?

Vladimir Bokov13:04:52

and second question - suppose we really want DI on ns level and use different components (`compA` and compB providing ns.comp.interface) should we have identical fn signatures in compA/src/ns/comp/interface.clj and compB/src/ns/comp/interface.clj ? (in short there's no defprotocol on ns-level which declares interface once)

tengstrand14:04:11

tools.deps and other tooling want it at the root.

šŸ‘Œ 1
seancorfield16:04:12

Since you @'d me... > but if I comment it out this single line, I'm still able to run clojure -M:dev -m usermanager.web.main and the app works Because :dev is how you specify the development project and it has "all" the dependencies defined in it. You could also run it via cd projects/usermanager && clojure -M -m usermanager.web.main > imo it's a bit error-prone and non-intuitive when you try to think about components as dependencies Depends on your mental model. Polylith talks a lot about "bricks" and assembling projects from those "bricks". At work, we have some components that have multiple implementations and the project decides which implementation is used (at build time). The development project can be run with any combination of those implementations but, because it is a build-time decision, you use aliases (`:+default`, etc) to tell the Clojure CLI which implementations to use for building the classpath, that is then used to run your project. Not having the (implementation) dependencies hard-coded in the bases or components is the key to making that possible. For example, we have an i18n component that has two completely distinct implementations: one uses a database, the other uses a local JSON file. We have one project that selects the JSON-backed version at build time to produce an app that can be run locally by our UI/UX folks so they can build out new HTML templates and provide the text strings in a local JSON file. That same component (interface) means that all the code that interacts with translations can work identically with both implementations, which improves reuse. We also have two implementations of our http-client component: one using hato that depends on JDK 11+, one using clj-http that can be run on JDK 8 -- because we have a single project that represents a legacy application that cannot run on JDK 9+. To give a sense of scale, we have 23 bases, 22 projects (plus development), and 162 component implementations, with 145k lines of Clojure altogether.

šŸ‘ 2
1
Vladimir Bokov11:04:26

thanks for detailed reply and usecases Sean, I got it, brick != dependency and not self-contained, basically you're also talking about namespace-level-DI as well let me generalize the use-cases: ā€¢ for i18n making and passing a "translator" interface would be too invasive? ā€¢ for http-client we cannot get a usual DI because there's no interface and classpath for projects and repls should be different anyway

seancorfield17:04:46

The specifics for i18n is to be able to create an app that has no JDBC or database-related dependencies in it at build time so it produces a minimal JAR file.

šŸ‘ 1
seancorfield17:04:20

And for http-client, the hato-based version will not even load on JDK 8 -- but the clj-http version will load on both JDK 21 (11+) and JDK 8 so we can test both implementations on the more recent JDK if we wish -- but we do not generally want clj-http on the classpath because of its assumed dependencies -- so both implementation choices are build time by necessity.

šŸ‘ 1
seancorfield16:04:12

Since you @'d me... > but if I comment it out this single line, I'm still able to run clojure -M:dev -m usermanager.web.main and the app works Because :dev is how you specify the development project and it has "all" the dependencies defined in it. You could also run it via cd projects/usermanager && clojure -M -m usermanager.web.main > imo it's a bit error-prone and non-intuitive when you try to think about components as dependencies Depends on your mental model. Polylith talks a lot about "bricks" and assembling projects from those "bricks". At work, we have some components that have multiple implementations and the project decides which implementation is used (at build time). The development project can be run with any combination of those implementations but, because it is a build-time decision, you use aliases (`:+default`, etc) to tell the Clojure CLI which implementations to use for building the classpath, that is then used to run your project. Not having the (implementation) dependencies hard-coded in the bases or components is the key to making that possible. For example, we have an i18n component that has two completely distinct implementations: one uses a database, the other uses a local JSON file. We have one project that selects the JSON-backed version at build time to produce an app that can be run locally by our UI/UX folks so they can build out new HTML templates and provide the text strings in a local JSON file. That same component (interface) means that all the code that interacts with translations can work identically with both implementations, which improves reuse. We also have two implementations of our http-client component: one using hato that depends on JDK 11+, one using clj-http that can be run on JDK 8 -- because we have a single project that represents a legacy application that cannot run on JDK 9+. To give a sense of scale, we have 23 bases, 22 projects (plus development), and 162 component implementations, with 145k lines of Clojure altogether.

šŸ‘ 2
1