This is the fourth part of A Journey to Java 9 modules. See the table underneath for links to the other parts.
Part 1: Modularizing an existing codebase | |
Part 2: Using service loaders | |
Part 3: Selecting services based on quality aspects | |
Part 4: Using a default service provider |
What if there is no module on the module path that provides a suitable implementation of Matcher
? What if there is no implementation of that interface at all? In this case, it is possible to provide a default implementation alongside the public API. The benefit is, that once the API module is distributed, it can definitely be put to use, even if the default implementation does not exhibit the quality aspects that we aimed for. Providing a default implementation is quite simple. Let us have a look again at the module graph of our application.
Fig. 1: The current state of affairs for our little string matching application.
We currently have two distinct implementations of the public API as exposed via module matchers.api
. One of these implementations features an elaborate and fast algorithm designed by Knuth, Morris and Pratt. The other one is a rather naive implementation that follows a brute-force technique. Deliberately selecting the brute-force algorithm over the more sophisticated Knuth-Morris-Pratt matcher is probably not going to happen considering its performance characteristics. However, it is a stable solution and solves the given problem. We could use it as a fallback mechanism in case there is no implementation that satisfies the quality constraints or in case there is no additional module at all which could provide an implementation.
Co-locating the BruteForceMatcher
along with the public API will give us a self-contained solution that will work out of the box.
To achieve this, we will move class BruteForceMatcher
as well as its JUnit tests to module matchers.api
while retaining the original package layout. Thus, the source tree for module will look similar to this.
matchers-api
├── pom.xml
└── src
├── main
│ └── java
│ ├── module-info.java
│ └── net
│ └── mguenther
│ └── matchers
│ ├── naive
│ │ └── BruteForceMatcher.java
│ ├── Experimental.java
│ ├── Fast.java
│ ├── MatcherCharacteristics.java
│ ├── Matcher.java
│ └── Stable.java
└── test
└── java
└── net
└── mguenther
└── matchers
└── naive
└── BruteForceMatcherTest.java
Module matchers.naive
is no longer required and can be safely removed from the project altogether with its corresponding Maven module matchers-naive
. We end up with a module graph like the following.
Fig. 2: The final module graph after removing the obsolete module matchers.naive
.
The API module does not have to export the default implementation - it will remain an implementation detail of the module. But it has to provide the same service that the original module matchers.naive
provided as well. This is easily done by a small adjustment to the module descriptor of module matchers.api
1.
module matchers.api {
exports net.mguenther.matchers;
provides net.mguenther.matchers.Matcher
with net.mguenther.matchers.naive.BruteForceMatcher;
}
How does a service consumer switch to a default implementation? Recall that we used the Service Loader API to retrieve all implementations of interface Matcher
and filtered it using the Streams API to select the one which best suited our needs.
Optional<Matcher> optionalMatcher = ServiceLoader
.load(Matcher.class)
.stream()
.filter(provider -> MatcherCharacteristics.isStable().test(provider.type()))
.findFirst()
.map(ServiceLoader.Provider::get);
The final map
will result in Optional.empty()
if a suitable implementation was not found. Let's say that MatcherCharacteristics
provides not only predicates for different quality aspects, but also a method useDefault()
that is a factory method for creating instances of the default BruteForceMatcher
. We can easily fallback to the default using Optional.orElseGet
like the underneath listing demonstrates.
Matcher matcher = ServiceLoader
.load(Matcher.class)
.stream()
.filter(provider -> isStable().and(isFast()).test(provider.type()))
.findFirst()
.map(ServiceLoader.Provider::get)
.orElseGet(MatcherCharacteristics::useDefault);
Summary
Integrating this last piece of code into MatchersCli
finally satisfies our initial mission statement. We have not only modularized the entire Java application, but also put the JPMS to good in use. Through the Service Loader API we achieved a very loosely coupled system of modules that exhibit strong encapsulation and information hiding while at the same time not introducing any unsatisfactory dependencies between any pair of modules. Using annotations to attribute implementations with quality aspects allows us to choose the implementation that fits the current situation best. And finally, by co-locating a default implementation, albeit undesirable with regard to its quality aspects, we guarantee that the API remains usable even though no specific implementation is currently present on the module path.
- `import` statements are omitted.