jqwik-team / jqwik2-api-discussion

Let's discuss and figure out some aspects of the jqwik2 API
2 stars 0 forks source link

API Discussion: Package naming scheme #5

Open jlink opened 4 hours ago

jlink commented 4 hours ago

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:

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 and internal subpackages as well as a subpackage core 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:

net.jqwik2.core.api
net.jqwik2.core.api.generation
net.jqwik2.core.api.arbitraries
...
net.jqwik2.core.internal.p1
net.jqwik2.core.internal.p2
...

JUnit platform module:

net.jqwik2.junit.api
net.jqwik2.junit.api.annotations
net.jqwik2.junit.internal

JUnit Jupiter module:

net.jqwik2.junit.jupiter.api
net.jqwik2.junit.jupiter.extensions.api
net.jqwik2.junit.jupiter.internal
odrotbohm commented 2 hours ago

For the Spring Modulith project (the project itself), we've resorted to a slightly different approach:

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

jlink commented 1 hour ago

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.

jlink commented 1 hour ago

@odrotbohm BTW, the "Project URL" and "Source Control" links in Maven Central point to the wrong places.

odrotbohm commented 55 minutes ago

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.