1

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?

Hidde
  • 170
  • 7

1 Answers1

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.

just me
  • 628
  • 4
  • 9