LowerProfile
From APIDesign
(→More Granularity) |
(→More Granularity) |
||
Line 31: | Line 31: | ||
{{:HeavyWeight}} | {{:HeavyWeight}} | ||
+ | |||
+ | Very [[rational]] solution - however it requires thinking. | ||
==== Injecting UI & co. ==== | ==== Injecting UI & co. ==== |
Current revision
Profiles are the best thing in JDK8 (in spite they are not as famous as lambdas). Now when they are here with us, it is essential to know how to adapt our applications, frameworks and libraries to be ready for them. Here is few tricks that will help you refactor your code to be ready for JDK8 profiles.
Contents |
Being Ready for JDK Profiles
What does it mean: to be ready for profiles? Well, first and foremost, it means to be able to run on smaller profile than the whole JDK. In older JDKs, up to JDK7, one always had the whole rt.jar. As such it did not matter whether your code used pieces from Swing (like ChangeListener) - all the classes were always available.
The situation changes with profiles. The smallest JDK8 profile is compact 1 and be sure, it does not contain a single class from Swing (neither JNDI or XML parsers). But as an owner of a library or running system you may be interested in such small profile. Why? Because then your library can be used on small devices (phones, Raspberry PI) or in a headless environment (on servers). For example Grizzly was not ready to run on compact 2 up until we submitted a patch which made a dependency on JNDI conditional since Grizzly revision 2.3.2.
Lower Your Profile Patterns
The need to run on smaller profiles is clear. Let's look at patterns how to adapt your API to such limited environment.
Conditional Usage
This is the most trivial change one can make. Not really beautiful, but effective. A form of clueless solution that was used in case of Grizzly as well. If there is a call to an API not always available, surround it with a catch statement:
Object ref = null; try { ref = new javax.naming.InitialContext().lookup("x"); } catch (LinkageError err) { // well, too small of profile. Give up. }
Effective, especially if the amount of such calls is not huge (nobody wants to pollute own code base with linkage catch statements too much). Requires compilation against the whole JDK and as such it is a runtime-only solution and possibly can regress any time (unless one actually runs tests on the small profile). On the other hand, it does not require any changes to build infrastructure or use of JDK8 during the build. In short this is the most cluelessness solution.
More Granularity
On the other hand, there is a rationalistic solution. Your system is modular, isn't it? Just some of your modules may be too HeavyWeight.
How one defines a weight of a module? In general it is a size of its environment - which is usually expressed by the outgoing dependencies the module has.
If there is a module depending on Swing, it is more HeavyWeight than a module which depends only on ArrayList - because the first one requires whole JDK8 while the latter can run on JDK8 compact 1 profile (which is not just a matter of the name but also of about 30MB of download size and other resources).
The classic refactoring is to increase granularity of your modules - e.g. split them into (two) parts. Module A (shows a dialog, asks for a number and shows result) can be refactored into two modules: one library to compute the factorial and another JAR to show the dialogs and call into the library to get the right results.
Very rational solution - however it requires thinking.
Injecting UI & co.
There is (obviously) more clueless (and in some form safer way) of refactoring. We can abstract away calls into the UI by using APISeam. Let's leave the whole code of module A untouched and only instead of call to show the result replace it with:
Iterator<ShowUI> it = ServiceLoader.load(ShowUI.class).iterator(); ShowUI ui = it.next(); int n = ui.askForNumber(); int fact = Library.factorial(n); ui.showResult(fact); // of course there needs to be public interface ShowUI { int askForNumber(); void showResult(int res); }
This will create a module A-noUI which is capable to run with a restricted environment. The Swing specific part would end up in a A-UI module which has richer set of dependencies and also registers itself as a provider of ShowUI interface (via Lookup-like mechanism). When your application runs in original mode, it will behave the same. When executed in headless mode, one needs to provide different implementation of ShowUI interface and alter the behavior appropriately.
The whole transformation is so trivial, that there should be an automated refactoring for it. Call it for example level up package dependency!
The Doublepack Starter
The experience gained while putting NetBeans Platform under the knees of JDeveloper (and especially its window system part) shows that one does not need to be afraid of initially overweighted system. It is always possible to trim it down! I would even advocate starting with a system that works, and then incrementally reducing its size and duplication of some functionality.
Start with doublepack (like our combo of NetBeans and old JDeveloper) and identify the parts that most obviously stick out (like two main windows instead of one). Then work on eliminating the duplication in such area. Measure the reduced size of your application environment. Identify next part which duplicates some functionality, repeat the elimination.
The beauty of this process is that you always has a working system and can incrementally move towards fat-free one.
Unwanted Modules
Some modules in your system are just a big no-no. You know they are too big to fit into the restricted profile and also too complex to be split out into parts. Yet there are other modules that depend on such functionality that have to be ported. What can one do?
One can work on such depending modules one by one and apply the level up dependency via APISeams. Yet, if there are many commonalities between these modules, it may be better to abstract a least common denominator.
Again, we used this in our JDeveloper unification work - the calls from Matisse GUI Builder (which obviously depended a lot on Java infrastructure) where abstracted into newly created java-queries module. It was lightweight module (e.g. no outgoing dependencies) which could answer simple string-like queries. For example: here is a class (identified by a fully qualified name), tell me fully qualified name of the superclass, or tell me names of its properties, etc. In addition to queries the module also had some editor related methods like - jump to given class or its property definition.
While creating the module, a proper implementation module was created for each usecase by leveling up dependency pattern, so the code in NetBeans remained almost unchanged (except one layer of indirection). Similar implementation module was created in JDeveloper.
As a result the Matisse Builder as well as other modules like profiler could remain independent from the actual Java and editor infrastructure, yet work the same as they did before.
No Need to Hack
When bringing your application to new environment some restrictions that were previously applicable, may no longer apply. For example Swing requires all actions to be executed on dedicated event dispatch thread. If you decide to rewrite your UI to JavaFX or even use your application in headless mode, this restriction no longer applies. How can one release such restriction? If there is...
public void runAction() { assert java.awt.EventQueue.isDispatchThread() : "Run me only from Swing dispatch thread"; doTheAction(); }
one may be tempted to hack around such limitation by rescheduling own call:
EventQueue.invokeLater(() -> runAction(); );
but may only be a temporary solution as neither JavaFX, neither headless mode knows what EventQueue means. Such hack, should be replaced by something more meaningful. Because, there is no need to hack! First of all (if one owns all the sources), one can remove the assert completely and require (and fix) all such actions to be ready for invocation from any thread.
Even if one does not own all the actions, there is a BackwardCompatible way of releasing such restriction. One can define a marker interface:
interface ThreadSafeAction { } public void runAction() { if (!(this instanceof ThreadSafeAction)) { assert java.awt.EventQueue.isDispatchThread() : "Run me only from Swing dispatch thread"; } doTheAction(); }
Then the callers of such actions don't need to do any hacks anymore and the providers of actions that want to be executed in the restricted environment will make a conscious decision to implement the ThreadSafeAction. Those who are unaware of the new contract, or don't care about the restricted mode at all, need not do anything and may rest in piece without their thread safety being compromised.
In any case, there is no need to hack. There is always a solution.
Fake Emulation Classes
Sometimes (especially when it comes to API) it is harder to eliminate classes from richer profiles easily. In such case one can try the brutal force, think no more solution: Emulate such classes. This is particularly easy in case of simple interfaces or classes that can throw exceptions. For example get your API compact 1 profile ready and still reference Action, just copy the class and everything will work. Or to eliminate need of JNDI without changing a line on the call site, one can create a fake:
package javax.naming; public class InitialContext { public InitialContext() { throw new NamingException(); } }
which is exactly the workaround I used to get Grizzly working on JDK8 compact 2 profile, before it got fixed. One can use this trick whenever it comes to javax packages - those can be loaded by any classloader. The trick does not work for java packages, as those are sealed and can only be loaded by the system classloader.
Different Implementation
What if our restricted profile requires different implementation than the original one? Then one has to make the implementation pluggable (for example by creating an Injectable Singleton) and implement it twice. Sometimes it is possible to keep the original API and have just two different implementations. Sometimes the original API richness cannot be achieved in the restricted environment - in such case one may consider to abstract the shared functionality into new module with API and rewrite modules ready for using the restricted version to use it instead.
Recently I had a lot of fun with restricted environment called Bck2Brwsr and for example to encapsulate WebSocket communication I rather invented new API than tried to use one already existing in Java (see the javadoc). Then it was easy to provide one implementation using classical Tyrus library and one, really slim using in browser WebSocket object and make sure the best suitable is always plugged in.
Summary
JDK8 profiles are here with us and our libraries, frameworks and application need to acknowledge that. Luckily there are simple patterns (demonstrated on this page) that help us adopt to smaller JDK profiles and make our code more re-usable.