Open jlink opened 1 month ago
For the Spring Modulith project (the project itself), we've resorted to a slightly different approach:
api
packagesThe reason we went with the dedicated artifact, is that it allows to only have a compile time dependency to the JAR containing the code, most users are supposed to refer to. There, of course, will be an audience (people trying to extend the implementation) who will also have to depend on other JARs, but we cater for the 80% use case. The approach allows preparing starter POMs that will create very clean dependency arrangements. See this one here, for example, which includes the API JAR in compile scope, the core one in runtime scope. For users, that allows carelessly depending on the starter and getting a very carefully crafted dependency arrangement.
Once you have the artifact separation in place, there's no real need for a dedicated API package anymore, as literally, everything in whatever base package(s) the artifact contains is API to folks coding against it. I, personally, like, that the code that we think people should primarily code against, does not carry any technical distractions. That said, having something like internal
acting as a negative signal to someone accidentally coding against internals is a thing worth having.
A dedicated API artifact
So that's similar to what we did in jqwik 1 with the exception that we had both an api artifact and an api
package.
I've tried to understand the example of "spring-modulith-starter". In the end it will create a compile time dependency on the "spring-modulith-api" jar. This jar does not have any dependency on implementation code specific to spring modulith. What if it had? For example, because you want to provide a factory entry point which will then create instances of types that are considered to be internal? In jqwik 1 (and also JUnit 5) this problem is paramount and we solved it by having facade interfaces in the api jar, which are then implemented in the implementation package and loaded through Java's service loader mechanism. That approach requires a lot of indirection code just for decoupling - that's why I tend to not duplicate it.
I'm probably missing something in your approach. I hope I do.
@odrotbohm BTW, the "Project URL" and "Source Control" links in Maven Central point to the wrong places.
I've tried to understand the example of "spring-modulith-starter". In the end it will create a compile time dependency on the "spring-modulith-api" jar. This jar does not have any dependency on implementation code specific to spring modulith. What if it had? For example, because you want to provide a factory entry point which will then create instances of types that are considered to be internal? In jqwik 1 (and also JUnit 5) this problem is paramount and we solved it by having facade interfaces in the api jar, which are then implemented in the implementation package and loaded through Java's service loader mechanism. That approach requires a lot of indirection code just for decoupling - that's why I tend to not duplicate it.
We don't have that as it would introduce a cyclic dependency. Not only on the artifact but also the package level. Not being able to create that connection does not impose too much of a problem for us, as the instances are created by the auto-configuration mechanism of Spring Boot. Folks that would want to instantiate those manually would need to depend on the core JAR in compile scope as well, but I'd consider those to be the 20% of the audience.
If there's not too much flexibility required, I assume a factory method using the service loader mechanism might be an alternative to consider. The implementation JAR would then advertise itself as an implementation provider for the APIs in the other JAR.
@odrotbohm BTW, the "Project URL" and "Source Control" links in Maven Central point to the wrong places.
Thanks for that. I can see one of the SCM URLs point to spring-projects-experimental, still. The project URL seems to be generated from the one explicitly declared in the root POM, augmented by the path to the nested build module. It's news to me that Maven does that and I'll have to investigate, why that's the case.
If there's not too much flexibility required, I assume a factory method using the service loader mechanism might be an alternative to consider. The implementation JAR would then advertise itself as an implementation provider for the APIs in the other JAR.
I found this approach quite painful in jqwik 1, that's why I want to move away from it. The most important point is that it makes refactoring in these areas a lot harder. Jqwik's API surface is (too?) large, requiring lots of connections to implementation code that's not part of the published API. I'll have to think about that...
Other than jqwik version 1, jqwik 2 will not split up the codebase in API modules and implementation modules. The reason is that doing that is a lot of effort since it requires the introduction of facades on the API side for every call into the implementation module. The benefit (limit visibility of implementation classes) however is IMO rather small.
To mitigate the problem that implementation classes are visible by users of the API, jqwik 2 plans to use two mechanisms:
Options
Looking at other Java libraries, a few main ideas seem to prevail:
api
internal
api
andinternal
One special case is the fundamental model, often called "the core" of a library. Should it get its own subpackage
core
or just reside in the projects main namespace?Suggestion
I suggest to use both
api
andinternal
subpackages as well as a subpackagecore
for the base module. This makes all choices explicit, but it leads to somewhat longer package names.Example
Let's assume a simple jqwik 2 structure with three modules: a "core" module, a "JUnit platform" module and a "JUnit Jupiter" module.
The package structure would look like:
Core module:
JUnit platform module:
JUnit Jupiter module: