0

Let's say I have a library, which provides two independent plugin interfaces, 2 implementations per plugin, and one parent POM file.
There are also some abstract tests in the "core", which the plugins have to implement and pass to be considered compliant.

From code perspective, the plugin interfaces don't depend on core at all.
Only core depends on plugins.

My assumptions are:

  • the core abstractions go into the "core" artifact and its tests are packaged in a test-jar.
  • each implementation of a "plugin" goes into a separate artifact (4 artifacts in this example).
  • the parent POM also goes into a separate artifact.

I have considered several options about how to structure the dependencies between each artifact, which can be boiled down to these 2:

  1. Leave it at just 6 artifacts. Every "plugin" depends on "core". Every "plugin" and "core" all depend on parent artifact.
    all depend on core
    This makes it possible for the library users to only specify 2 artifacts in their pom.xml/build.gradle, because "core" is a transitive dependency.
    BUT, given I have some changes to the "core" which cause a version bump, I would have to update every plugin implementaton to bump the dependency version. Also, if users didn't specify the core explicitly, they now depend on outdated core.

  2. Extract plugin interfaces into separate artifacts, so that implementations no longer depend on core. Which now creates 8 artifacts.
    plugin interfaces extracted
    Now, unlike the previous approach, library users can no longer skip the "core" dependency in their pom.xml/build.gradle - it is now a given that it has to be specified. Which means, they would have to depend on 3 artifacts.
    But, overall, any update of the core no longer forces a cascading update of the plugins. The plugin implementations need version bumps only if their respective interface updates.
    The downside is probably that I now have 2 more artifacts to maintain.

My questions are:

  • Which approach is the more correct one? Does it depend on project size or some other factors?
  • Are there other approaches?
  • Is it bad that users have to depend on plugins & "core" explicitly, even if plugins transitively bring "core" in the first approach?
  • Anything that is intrinsic to the problem and cannot be solved? (like, is it a given that 8 artifacts are to be maintained, with no way to minimize that?)
  • Is it correct to provide abstract tests in the "test-jar", if I want to make sure that all plugin implementations comply with the interface contracts? Or do I have to copy-paste the tests in each plugin implementation?

Reply to @vokail

Generally, If you release a new version of the core, you must release a new version of the plugin, right?

Currently the code is structured in such a way, that plugin have no dependencies on the core. With 1st scheme, if core updates, plugins must update. With 2nd scheme - if core updates, plugins don't care.

I think it's possible to have more than two plugins implementations
for plugin developers they need to use only this as dependency directly

True & true

plugin-api need only core-api

Currently, I cannot invert the dependency in such a way. Plugins know nothing about the core, except the plugins API.
As a note, there are 2 plugin APIs. Their code doesn't depend on core and their code doesn't depend on each other.
With 1st scheme, all plugin APIs are inside a single core artifact.
With 2nd scheme all plugin APIs are in separate artifacts (so it's 1 core artifact, and 2 separate API artifacts = 3 artifacts in total).

core-api can be implemented by more than one core-impl ( in the future )

Mhm... Don't see it in the future.

It's better to depend for my plugin implementation from an interface only, not from a core one

To clarify, this is what I meant.
From library user perspective, 1st scheme looks like this:

// Implementaton of "A" api, variant 1
implementation 'library:plugin-a1-impl:1.0.0'
// Implementaton of "B" api, variant 2
implementation 'library:plugin-b2-impl:1.0.0'
// Both plugins transitively bring in "library:core:1.0.0".
// But if for example core:1.1.0 is released, it has to be included explicitly  

2nd scheme looks like this:

// Implementaton of "A" api, variant 1
// Transitively brings in "library:plugin-a-api" - a new artifact
implementation 'library:plugin-a1-impl:1.0.0'
// Implementaton of "B" api, variant 2
// Transitively brings in "library:plugin-b-api" - a new artifact
implementation 'library:plugin-b2-impl:1.0.0'
// Core has to be explicitly specified, nobody depends on it, only core depends on plugins
implementation 'library:core:1.0.0'

just do one artifact and let people depend on that only ( as example to minimize that ).

Currently there are separate projects that depend on the library, and they use different plugin implementations. Users pick between different implementation of the same APIs depending on shared dependencies.
For example, there's A, and there are 2 implementations: A-oranges, A-apples. If the project already uses oranges, it imports A-oranges. If it already uses apples, it imports A-apples.
In other words, the plugins are more like adapters between the library and external projects.


Another depiction of the differences between 2 options:

Squares represent ".jar" artifacts. Circles inside a square represent interfaces/classes and their dependencies on each other.

comparison of options

It could be said, that the code is DIP compliant - both core and plugin implementations depend on abstractions.
It's only a question of artifact structuring - is it worth extracting abstractions into separate artifacts as well?

iwat0qs
  • 146
  • 3
  • 10

2 Answers2

1

I suppose there is an issue on how and how much often do you release a new version of the core and the plugin. Generally, If you release a new version of the core, you must release a new version of the plugin, right? If not so, please specify this.

I'm for solution 2, but with a little difference, as the following example:

artifact dependencies

As you can see I've introduced a plugin-api artifact, with only interfaces used by plugins, because:

  • I think it's possible to have more than two plugins implementations
  • for plugin developers they need to use only this as dependency directly
  • plugin-api need only core-api
  • core-api can be implemented by more than one core-impl ( in the future )

Following this approach you focus will be to design plugin-api better you can, stabilize it and then let plugin developers do the job.

What if:

  • core-impl change ? For example a bugfix or new release. Ask yourself: do I need to change core-api ? For example to provide a new feautre to plugin-api ? If so, release a new core-api and then release a new plugin-api
  • core-api change? Like before
  • plugin-api ? if plugin-api change, you need to change only plugin-impls

To answer on your questions:

Which approach is the more correct one? Does it depend on project size or some other factors?

There is no "correct one", depends for sure on project size ( just count how many feature/methos/interfaces you have in core-api and plugin-api ), how many developers works on it and how your release process works

Are there other approaches?

See one answer before, you can search from some big project like apache or eclipse foundation one to learn their patterns, but depends heavily on the subject and can be an huge task.

Is it bad that users have to depend on plugins & "core" explicitly, even if plugins transitively bring "core" in the first approach?

For my understanding, yes. It's better to depend for my plugin implementation from an interface only, not from a core one

Anything that is intrinsic to the problem and cannot be solved? (like, is it a given that 8 artifacts are to be maintained, with no way to minimize that?)

Well, If you are alone, this is an open source project used only by yourself, don't overengineering this, just do one artifact and let people depend on that only ( as example to minimize that ).

Is it correct to provide abstract tests in the "test-jar", if I want to make sure that all plugin implementations comply with the interface contracts? Or do I have to copy-paste the tests in each plugin implementation?

For me it's better to have a plugin-api and let plugin implementation declare only that, it's more clear and concise. For tests, I'm not sure if you plan to do tests on implementations by yourself, of "ask" to plugin developers to do the test. For sure copy-paste is not the right choice, you can use a command pattern or similar to make these tests, see here


After updated question, I'm still for solution 2, event if there are two separated plugin-api, is better to have different plugin-api.

It's only a question of artifact structuring - is it worth extracting abstractions into separate artifacts as well?

I think yes, in the long run. If you separate in different artifacts, you can change them independently, for example change something in plugin-apiA and this doesn't affect plugin-apiB. If you change the core, yes of course.

Note: for my diagram above I think can still be working, can't you make an abstract set of interfaces for plugin-api and have a common artifact for them ?

Vokail
  • 630
  • 8
  • 27
  • Upvoted for the interest and edited my question to clarify and reploy to some of the questions. Would like to hear if you have anything to add :) . If not, will mark the question as the solution anyway – iwat0qs Jun 01 '21 at 17:09
  • Added a clarification about the direction of dependency between "core" & "plugins" in the beginning, and another pic in the end – iwat0qs Jun 01 '21 at 17:49
1

If plugin A and B are two distinct type of plugin, then the option 2 is a better pick however:

  1. If A depends on core@v1
  2. If B depends on core@v2

Then core@v2 have to be binary compatible with core@v1, otherwise it will fail when for example someone depends on an implementation of A and an implementation of B: there always have to upgrade the plugin version in any case.

You could probably use Java Module do hide the details (eg: only provide an interface that is likely to never changes) which will makes the solution 2 of Vokail useless in some sense: you don't need a core-impl because Java module will ensure you that, apart from your core module, no one access the details (the impl). This also allow you to reuse the same package.

If A and B interface are in core, then the likeness of a binary incompatibility fall down.

NoDataFound
  • 11,381
  • 33
  • 59
  • I was considering 2nd option specifically because uf the `core@v2` vs `core@v1` case. Also, I would like to clarify that the plugin code doesn't depend on core code. So this problem arises only for 1st option. – iwat0qs Jun 01 '21 at 17:42
  • Added a clarification about the direction of dependency between "core" & "plugins" in the beginning, and another pic in the end – iwat0qs Jun 01 '21 at 17:49