Modularity Patterns with JPMS: Abstractions

While developing software in Java and other languages, we often create abstractions and separate them from the implementation. This practice has many benefits that we would like to take advantage of while designing modules. The Abstract Module and Separate Abstraction patterns from [Kno12] and API Modules from [Sand&Bak17] deal with abstractions in the context of modules.

Sample code for this post can be found on GitHub.

Abstract Module

The Abstract Module pattern states that users of modules should depend on abstractions, not implementations. When we depend on interfaces or abstract classes the implementation can change without affecting us. It may even be replaced completely by the maintainer of the module. Systems composed of modules that depend on abstractions of each other are flexible and easier to extend.

Sample

You may have come across the sample of payment methods in one form or another already. Let’s see how it looks like with Java 9 modules. If we have a webshop when we check out and complete a sale, there are multiple ways to pay. A first version of the checkout module depends directly on a credit card module and PayPal module as shown below.

As you probably imagine if we add new payment methods the checkout module needs to depend on them too. These dependencies introduce high coupling which we can avoid.

The Refactoring

To prevent coupling the checkout module to every payment method implementation we introduce a payment method abstraction. Now the modules containing payment methods depend on the checkout module.

To make the payment methods usable from within the checkout module, we design the payment method abstraction as JPMS service with the interface PaymentMethod. The payment method modules provide this service with their implementation. Finally the opus.checkout module declares that it will use the payment method services as can be seen in the simplified module descriptors below.

If we need to add more payment options, they can now easily be used by the checkout system without affecting its dependencies. It is also possible for the different implementers of payment methods to change without affecting (rebuild, deploy) the checkout module.

API Modules

The API Modules pattern [Sand&Bak17] states that we should design the API of a module carefully and create dedicated API modules. The API of a module consists of all parts that are exported from it. We should make it as small as possible to gain resilient modules.

Another Refactoring

To apply the API Modules pattern we extend the previous sample with a separate API module called opus.paymentmethod. It contains only the interfaces for using the different payment methods and exports them.

By using the JPMS service infrastructure, we don’t even need to export anything from the implementers. They have zero API and therefore no incoming dependency which allows us to change them without affecting any other module.

The API Modules pattern also states that most parts of the API should be abstractions. This statement aligns well with the goal of the Abstract Module pattern (depend on abstractions) discussed earlier. The API Modules pattern, therefore, can be seen as the inversion of the Abstract Module pattern. If modules only export abstractions other modules can depend only on them and not the implementations.

Separate Abstractions

The third pattern I would like to introduce is Separate Abstractions [Kno12]. It states that we should place abstractions and implementations in separate modules. If they live in the same module, they cannot change independently. Also, an implementation may require additional modules to provide its functionality which are not relevant for the abstraction.

Sample

Suppose our first design introduced an abstraction but located implementations in the same module. This module needs to depend on everything the implementations need like the opus.visaconnector. By refactoring the credit card implementation into its own module, we could remove this dependency from the module with the abstraction (opus.checkout). If there are many different implementations with multiple dependencies, extracting those implementations, helps to keep the modules clean an cohesive.

Where should abstractions live then? If there is only one user of the abstraction, we could place it in the same module. Otherwise, we create a separate module on which multiple users can depend.

The implementations live in their own module. This module needs to depend on the abstraction which could be problematic when it is located in the same module as the user of it. To resolve this problem, we could create a separate abstract module even if there is only one user of it. We applied this already in the second refactoring of the payment method sample.

Wrap Up / Final Thoughts

Abstractions are an integral part of software development. With modules, multiple questions arise regarding when to create them and where they should live. The patterns described in this post should help you answer those questions.

There is always a tension between (re-)usability and flexibility when designing modules. Modules that export abstractions and provide implementations too are easier to use. API only modules that separate abstractions from implementation are more flexible and have fewer dependencies but can be harder to use.

[Kno12] K. Knoernschield: Java Application Architecture – Modularity Patterns with Examples Using OSGi (homepage)
[Sand&Bak17] Sander Mak & Paul Bakker: Java 9 Modularity – Patterns and practices for developing maintainable applications (homepage)

Leave a Reply

Your email address will not be published. Required fields are marked *