Configuring module dependency

Think of a module being a walled garden by default. By default, any Java type in a module is accessible only to the types inside the same module. Previously, the SortUtil class was in the packt.addressbook module and was thus accessible to other types in that module. Move it to a different module however, and it is not accessible to types in the original module anymore.

Given two modules, A and B, for any type in module A to access a type in module B, two conditions need to be satisfied:

  • Module A needs to declare its dependency on module B
  • Module B needs to declare that it's okay with that type being accessed externally by other modules

If either of these conditions isn't met, the type being accessed in module B is said to be not readable by module A. We'll cover the topics of readability and accessibility in sufficient detail in Chapter 6, Module Resolution, Accessibility, and Readability, but, for now, note that these are two important requirements. Let's apply these to the packt.addressbook and packt.sortutil modules.

First, we need to have the packt.addressbook module declare that it is dependent on the packt.sortutil module. It is using a class from that module, and there's no way you can compile or run the module without it. The way to declare a dependency on a module is by using the requires keyword:

    module packt.addressbook { 
      requires packt.sortutil; 
    } 

Following the requires keyword is the name of the module that is required. And yes, you can only require other modules, and not packages or classes. This line is enough for the compiler and runtime to look up types from the module that is being required any time those types are being used in the module. A module could depend on multiple other modules. So, it's actually common to have multiple such requires lines of code in a module declaration.

When a compiler tries to compile a module, imagine that it looks at this list of modules specified with the requires clause and says "Okay, I understand that this module requires all these other modules. While I compile this module, anytime I see a type being used in the code of this module that belongs to another module that is required, I'll go look at the module that contains the type and make sure it exports the type being used." Since every dependent module could potentially require other modules, this is a recursive process.

Now what does a module exporting a type mean? This is where we get to the second of the two preceding conditions. Every module needs to specifically declare what types in its module are okay for use outside the module. In our case, the packt.sortutil module needs to declare that the SortUtil class is allowed to be used outside its own module. This is done in the sortutil module's declaration file, using the exports keyword followed by the package you want to export. When a module exports a package, all the types belonging to that package are allowed access outside the module:

    module packt.sortutil { 
      exports packt.util; 
    } 

In our case, we want to export the packt.util.SortUtil class. So, we export the package packt.util thereby exporting the class within it.

Again, note that you require modules and export packages. There are several reasons why the language designers decided to use the exports syntax with packages rather than having developers export individual types. The most obvious reason, of course, is that it is much more convenient than the tedium of having to export each class at an individual type level.

We've introduced a handful of new keywords so far--module, requires, and exports. You might be wondering if they are reserved words in the Java language as of Java 9. Are you in trouble if you've been using these as variable names all over your code today? The answer is no! These and other module related keywords we'll be learning in this book are what are called restricted keywords. You can still continue to use them in your code, but only when they are used in the context of a module descriptor, the compiler knows what they mean and it treats the keywords accordingly.

With both the conditions for cross-module access of types now satisfied, let's compile the code again from one directory above the module path:

$ javac -d out --module-source-path src --module packt.addressbook,packt.sortutil

Again, the --module-source-path parameter specifies where the compiler can find all the Java modules that are required to do its job compiling your code. And, the --module indicates the two modules to be compiled--packt.addressbook and packt.sortutil. The compiler finds both the module root folders in the module source path and compiles them into their respective classes.

The compilation should quietly succeed. Just like last time, you'll notice that the out folder has a .class file corresponding to every .java file in each of the two modules, including the module descriptors, the two module-info.java files.

You can execute the Main class in the packt.addressbook module like before. For illustration, I'll use the terser  -m option (instead of --module) to specify the module and class to start execution. They both mean the same and can be used interchangeably.

$ java --module-path out -m packt.addressbook/packt.addressbook.Main
[Babbage 123-456-7890, Lovelace 234-567-8901, Dijkstra 345-678-9012, Turing 456-789-0123, Berners-Lee 456-789-0123]

Here as well, the --module-path option specifies where the Java runtime needs to look to find the modules required to execute the code. The runtime detects that the class it needs to start execution with (Main) is a part of the module packt.addressbook and since that module has a dependency on another module (packt.sortutil), it searches the location specified by the module-path option (the out directory here) to find the depended module. Thanks to our compile step placing the compiled module in the same location, the runtime finds it and the execution proceeds.

You should see the contacts successfully sorted by last name. Note that, to run this code, you just specified the packt.addressbook module and the Main class, and we didn't have to provide any information to the runtime about the dependent packt.sortutil module. This is because the Java runtime is reading off the same module descriptor (the module-info.class file this time) to know that the packt.sortutil module is required and so leverages the right class files from both modules as and when necessary.

Here is a diagram explaining the behavior of the two modules:

What we've done is isolated the sorting functionality into its own module so that it can be used by other Java 9 modular applications that need sorting. All that any Java 9 module has to do in order to use packt.sortutil is:

  1. Add the requires packt.sortutil line in the definition of the module that needs the sorting functionality.
  2. Import the packt.util.SortUtil class and call the sortList() method to sort any List of objects as long as the objects are Comparable.

This is great, but before we share our new packt.sortutil library for the world to use, let's think about the library's API for a bit.

What I mean by a library's API is the code required to be used by the consumers of your library in order to access it. In the case of the simplistic packt.sortutil library, for example, the API is a single method sortList() on the class packt.util.SortUtil. More functional libraries obviously have multiple classes and methods that could potentially be called by their consumers.

When you create a library, before you allow others to use it, you have to very carefully define and finalize the library's API. The reason being that, once others start using your library, it becomes harder to make changes to the library's public API. Any changes to the API in future versions of your library would mean requiring all the consumers of your library update their code to work with the new API.

Right now, the packt.sortutil module contains just one class. We will be evolving the module in the next chapters, but for now, one change I'd like to do is to make the SortUtil class as lightweight as possible. That class acts as the programming interface to the packt.sortutil library, so making sure the class is as simple as it can be with fewer lines of code makes it less susceptible to possible changes in the future.

One way to achieve this is by moving the actual sorting functionality to an implementation class and have SortUtil just delegate the sorting logic to that class.

Let's assume we create a BubbleSortUtil class that has the same structure as the SortUtil class has so far:

    public class BubbleSortUtilImpl { 
      public <T extends Comparable> List<T> sortList(List<T> list) { 
        ... 
      } 
      private <T> void swap(List<T>list, int inner) { 
        ... 
      } 
    } 

We can then update the SortUtil to just call BubbleSortUtilImpl class's sortList method to delegate:

    public class SortUtil { 
      private BubbleSortUtilImpl sortImpl = new BubbleSortUtilImpl(); 
      public <T extends Comparable> List<T> sortList(List<T> list) { 
        return this.sortImpl.sortList(list); 
      } 
    }  

This is much better, because if you'd like to change the structure of the library or the sorting logic, as long as you keep the structure of SortUtil unchanged, the consumers of your library don't have to change their code. The SortUtil class is still tightly coupled to BubbleSortUtil in that it is directly instantiating it, but we'll be improving this is in a subsequent chapter, so let's live with it for now.

Running the code at this stage should result in the same output as last time. Now, before we announce to the world that our packt.sortutil module is ready for consumption, think back to the problem Jack faced in Chapter 1, Introducing Java 9 Modularity. He had created a sorting library JAR file with pretty much the same design as far as classes are concerned--an internal BubbleSortUtilImpl and an external SortUtil. The problem he ran into was the fact that a certain class is either internal or external was only a matter of convention and wasn't enforceable without Jack lumping his code into a single package just to leverage the package-private mechanisms. Developers using his library started to use the BubbleSortUtilImpl class that they weren't supposed to use. Let's see if we have the same problem with our sortutil module, and if so, if there are better tools to protect certain classes using the module system in Java 9.

The answer is simple. Yes, we have the same problem that Jack ran into. Any consumer of the sortutil module could easily use BubbleSortUtilImpl directly. That's because the class is in a package that's exported from the module. We'd like to avoid that by encapsulating the class and prevent its usage outside the module. How do we do that? Simple! Just move the class to another package! Like we've already seen, the Java Platform Module System expects us to specify what packages are visible outside the module. If any type is accessible outside any module, it's only because it belongs to an exported package in that module. Which is why refactoring the type into a new package is a potential solution. As long as the new package doesn't show up with the exports clause in the module definition file of the module, the classes in the package are effectively hidden from outside use, like we've already seen:

Here's a diagram of the modules, revisited with our newest change:

Remember that packages in Java are not hierarchical. In this example, the packages packt.util and packt.util.impl are two separate packages that are not related in any way. Just because you've exported the package packt.util, it doesn't mean that you are automatically exporting packt.util.impl also. Nor does it mean that packt.util.impl is somehow within packt.util. That's not how packages work in Java. These are two entirely different packages, and totally unrelated as far as package semantics are concerned.

The state of the code at this time is in the bundled source code. Compiling and executing the code as before should give us the same results. However, we have solved a major problem related to class encapsulation that we discussed in Chapter 1, Introducing Java 9 Modularity.

Think about the potential impact of this encapsulation on a library developer. Before Java 9, every public type that was shipped in a library could have been used and accessed by the consumers of the library. Now, with Java 9 modules, a library developer has full control over what classes can be used and what are just internal. So, the library developer can refactor the internal library code without having to worry about them potentially being used. Thanks to the module contract, they are inaccessible and hence guaranteed to be unused in code outside the module.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.213.238