Assume we have a single large JVM project (the example is in Kotlin), containing code. As part of a refactoring effort, we are decoupling pieces of the code by splitting the code into multiple modules, and using a build tool such as Gradle/Maven to build deploy applications with only a small set of the modules.
Some more details:
class SomeService(
private val SomeCallable: callable, // implemented by OtherService during runtime
) {
fun doSomething() {
callable.call()
}
}
interface SomeCallable {
fun call()
}
class OtherService: SomeCallable {
fun call() {}
}
We want to split this code into two modules:
// Module A
class SomeService(
private val OtherService: callable,
) {
fun doSomething() {
callable.call()
}
}
// Module B
class OtherService: SomeCallable {
fun call() {}
}
// Module ???
interface SomeCallable {
fun call()
}
The question is in which module the interface belongs. If it would be part of module A, then the implementation in module B cannot compile. If it would be part of module B, then the calling service cannot compile. It seems we need a third module C containing just the interface SomeCallable
, and that both modules A and B depend on.
A compilation dependency diagram:
A B
\ /
C
A runtime dependency diagram:
A
| \
B |
| /
C
Is this the best way to achieve the decoupling of code/modules, or are there better ways?
1 Answer 1
Your reasoning seems correct, though a simpler practical solution might be to combine B and C, depending on the details in your case.
In that design, A would depend on (B+C) at both compile time and runtime. This obviously couples A to the implementation of the service, which isn't conceptually ideal, but may be worthwhile in practical terms in order to simplify your module hierarchy.
A would require B in order to run, but from your runtime depencency diagram, that was the case anyway, so that hasn't changed. However, it does mean that it's more complicated to swap out B for another implementation of C, so if you were planning on doing that, C as a separate module makes more sense.
It does open the possibility that developers may misunderstand or work around the design, and introduce an unwanted compile time dependency from A onto B directly, in the absence of a hard module boundary preventing them. However, there are ways to prevent that, for example Java 9 modularity (JPMS). The (B+C) module could declare only the C packages as exports
, therefore hiding the B packages from anything outside of that module.
It's also worth noting that A would not be able to instantiate OtherService
from B directly, using your original design. That would need handling in some way, either by a dependency injection framework, an additional module D that depends on both A and B, ServiceLoader
with JPMS uses
and provides
, or something similar.
Explore related questions
See similar questions with these tags.