Featured Post

Applying Email Validation to a JavaFX TextField Using Binding

This example uses the same controller as in a previous post but adds a use case to support email validation.  A Commons Validator object is ...

Sunday, February 15, 2015

Dynamic FX Application Modules - Part 2 - Foundation - Google Guice

This is the second part in a six part series on creating dynamic Java FX application modules.  This post presents the Google Guice dependency injection framework as a way to wire up dynamic modules and to provide those modules with access to core services.


Guice

Google Guice, like Spring or the CDI in Java EE, is a dependency injection framework that lets you configure an application from a central location.  The means that the bulk of your application replaces code like this

ServiceObject1 service = new ServiceObjectImpl1()

or

ServiceObject1 service = ServiceObject1Factory.getInstance()

with

@Inject ServiceObject1 service;

While this may seem like a modest syntactic savings, it becomes more useful the deeper the object graphs go.  A single "new" statement may eventually lead to a cascading set of "news" as each child object's dependencies are created.  This can lead to classes that are difficult to test and to repurpose into other areas because you can't surgically adjust the dependencies.

Also, Google Guice is very lightweight.  This post describes how you can hook it up to your project: JavaFX Dependency Injection with Google Guice.   The configuration?  Add classes that extend Guice's AbstractModule like this.

public class CoreModule extends AbstractModule {
    @Override    protected void configure() {
        bind( ServiceObject1.class ).to( ServiceObjectImpl1.class ).asEagerSingleton();
    }
}

Create something called a Guice Injector, make reference to it in an FX factory, and you're all set.  Here is the code of the other AbstractModule in the project.  The annotation @SubApp will be discussed in the next post.

@SubApp(
        buttonId = "B",
        buttonTitle = "Show B",
        stageTitle = "Screen B",
        fxmlFile = "subapp_b/screenB.fxml")
public class B_Module extends AbstractModule {

    @Override    protected void configure() {
        bind( ServiceObject2.class ).to( ServiceObjectImpl2.class ).asEagerSingleton();
    }
}

Services

This class model shows the demo application calling ServiceObjects from Java FX Controllers.  Part 1 of this series described how the application is divided into SubApps.  The SubApp structure defines the Controller, ServiceObject, Module arrangement.

Modules Bind ServiceObjects Used by Controllers

Under the terms of this design, SubApps can bring their own service objects.  For example, ScreenDController uses ServiceObject4 as defined in D_Module.  All of that code -- ScreenDController,ServiceObject4, D_Module -- is kept together.  Moreover, ServiceObject4, while available to Google Guice, will not be available to the other SubApps.

SubApps can share in the kernel.  CoreModule loads ServiceObject1 which is used by both SubApp A and SubApp B. SubApp B also brings its own ServiceObject2.

From the perspective of the Controller developer, the source of the ServiceObject isn't important.  Take a look a SubApp B's controller which uses both a Core and a SubApp B ServiceObject.

public class ScreenBController {

    private Logger logger = LoggerFactory.getLogger(ScreenBController.class);

    @Inject    private ServiceObject2 service;

    @Inject    private ServiceObject1 service1;

    public ScreenBController() {
        if( logger.isDebugEnabled() ) {
            logger.debug("[CONSTRUCTOR}");
        }
    }
    @FXML    public void doSomethingOnB() {

        service.doSomething2();

        service1.doSomething();
    }

}

ServiceObject2 is an interface with a single method.

public interface ServiceObject2 {
    void doSomething2();
}

ServiceObject2Impl prints out a debug message.

public class ServiceObjectImpl2 implements ServiceObject2 {

    private Logger logger = LoggerFactory.getLogger(ServiceObjectImpl2.class);

    @Override    public void doSomething2() {

        if( logger.isDebugEnabled() ) {
            logger.debug("[DO SOMETHING 2] object id={}", this.hashCode());
        }
    }
}

Note that ServiceObject2 and ServiceObject2Impl don't make any reference to Google Guice.  The calling Controller uses the standard (non-Google Guice) annotation "Inject".

When I start Screen B and press the "Do Something" button, I get

[JavaFX Application Thread] DEBUG fms.app_core.HomeScreenController - Starting a screen B
[JavaFX Application Thread] INFO fms.subapp_b.B_Module - Configuring fms.subapp_b.B_Module
[JavaFX Application Thread] DEBUG fms.app_core.HomeScreenController - [CREATE STAGE] fxmlFile=subapp_b/screenB.fxml, using loader from class=fms.subapp_b.B_Module
[JavaFX Application Thread] DEBUG fms.subapp_b.ScreenBController - [CONSTRUCTOR}
[JavaFX Application Thread] DEBUG fms.subapp_b.ServiceObjectImpl2 - [DO SOMETHING 2] object id=1698669582
[JavaFX Application Thread] DEBUG fms.app_core.ServiceObjectImpl1 - [DO SOMETHING] object id=1890026086


Note although I can compile ScreenAController with ServiceObject2, I get this error at runtime when I try to show the screen.

Caused by: com.google.inject.ConfigurationException: Guice configuration errors:
1) No implementation for fms.subapp_b.ServiceObject2 was bound.
  while locating fms.subapp_b.ServiceObject2


This speaks to the benefits of developing SubApps independently.  ScreenAController doesn't have ServiceObject4 from fms-multistage-subapp-d on it's compiler classpath, so this mistake won't occur with that ServiceObject.

Initialization

HomeScreenController starts initializing Google Guice in an @FXML initialize method.

private Injector injector;
private BuilderFactory builderFactory;
private Callback<Class<?>, Object> coreGuiceControllerFactory;
@FXML
public void initialize() { CoreModule module = new CoreModule(); injector = Guice.createInjector(module); builderFactory = new JavaFXBuilderFactory(); coreGuiceControllerFactory = clazz -> injector.getInstance(clazz); }
(Notice the nice Java 8 Lambda for the coreGuiceControllerFactory object.)  CoreModule is a Google Guice AbstractModule subclass and is created with the new operator.  The JavaFXBuilderFactory is the default value, but I need to provide it as an argument so that I can provide the more important Controller Factory as an argument.  coreGuiceControllerFactory links the FX object creation mechanism to Google Guice such that Guice knows of each object and can provide depedendency injection.

When FX creates a Controller class, it's dependencies (ServiceObjects) are wired in because injector.getInstance() is called.

It's also worth mentioning that my most important class in the kernel HomeScreenController is not created by Google Guice.

Eggs Meet Basket

This application makes use of child Google Guice Injectors, one per SubApp.  Each SubApp will share in the main Injector which will provide common services like ServiceObject1.  However, each SubApp's specific Injector will limit the availability of objects in other areas.  The code to create the child Injector is made on the main Injector and takes a single AbstractModule object which is the AbstractModule of the subapp. 

AbstractModule m = (AbstractModule)sa.newInstance();
Injector inj = injector.createChildInjector( m );
gcf = clazz -> inj.getInstance(clazz);
gcfCache.put( evt.getSource(), gcf );
I'm caching the child Injector-created Controller Factories to enforce a singleton pattern on the SubApps.  "sa" is an annotation that I've placed in the code that I'll discuss in the next section.

The child Injector scheme is important so that we don't have a huge main Injector responsible for each and every AbstractModule.  In addition to the information hiding, this will help support a bootstrap mode.

I've been a big fan of dependency injection since I worked on a large project that had what can only be described as object creation "timebombs" throughout the code.  Not only was the code impossible to test, but masses of logic started to permeate through the code to sidestep object creation ("new" calls) or to provide a different object.  This lead to a ton of side effects where test results would lurch from one working decision path to the other.

For the demo app, dependency injection provides the mechanism for HomeScreenController to wire dependencies in a SubApp without explicit knowledge of the dependencies.  HomeScreenController learns of a SubApp's AbstractModule and builds an Injector around it, but the specific objects involved remain hidden in the local AbstractModule.

The next part of the series will discuss the annotation-driven discovery function and the metadata associated with a SubApp.

Click Dynamic FX Application Modules - Part 3 - Foundation - Reflections for the next part of the series.




No comments:

Post a Comment