StrataCode components

StrataCode has several features to make Java better at building 'components'. This is an overloaded term, but here we use it to mean wired together graphs of objects with the goal of mostly declarative, statically typed applications.

Most Java programmers have encountered component frameworks that implement an inversion of control pattern to wire together a graph of objects for later customization (e.g. Spring, Guice). With StrataCode, these features are all implemented using code-pre-processing, so that a similar version of the original Java file is generated, compiled and run. There's no hidden code injected using byte-code enhancement, runtime binding, startup overhead, or libraries required. The new features are:

  • data binding (covered here)
  • object operator - create outer and inner object instances
  • @Component annotation - add multi-step initialization sequence for more flexible init
  • 'scope' annotation - add lifecycle constraints/management for instances during code gen
  • modify operation - layered components (covered here)

While the syntax changes are small, framework developers can use carefully design hooks to customize the generated code to insert framework specific code. For example, a framework developer can customize the code generated for an object with a specific base class - like javax.swing.JComponent - so that inner objects automatically add themselves as children.

The same syntax is supported in the dynamic runtime by implementing the IDynChildManager and IDynObjManager interfaces. With a small amount of code, frameworks support both compiled and dynamic modes with refresh, plus all the features of layers for customization.

Simplifying component oriented design

Traditional inversion of control frameworks (IOCs) require the developer to design the "configuration interface", that sits on top API. Should a feature be configurable in XML, annotations or just a plain old Java feature? Often this choice is related to whether you want to configure this feature at compile time or runtime, and the role the configuration will play long term, and who changes it: developers, QA, design, devops, marketing, etc.

The IOC framework tends to be large and don't run on the client very well. They contain a lot of code that directs the application that may hide problems due to the size and complexity. In part, that's because they replicate a lot of features of static typed languages at runtime, and that's a lot of logic.

Ideally we'd like to make every system configurable, but with IOC adding "configurability" comes with a non-trivial cost that spreads throughout the code. What if it were a simple, incremental process where you can create configuration layers as needed that included just the right properties? You should be able to change a property from not-configurable to configurable without breaking existing configuration by providing a default. If you change a property from being configurable to not-configurable, it should give compile time errors for programs that have configured it. In an IDE, all uses should be discoverable from a 'find usages' operation, fixable quickly in the editor before they become a problem down the road. Any program should be compilable into a standalone jar file with no configuration, or deployed with any subset of its configuration exposed but using static-typing to identify incompatibilities and contain no library dependencies for no overhead.

StrataCode components offer these options. You write your initial code using simple classes, fields or properties in plain old Java. You can use the object keyword in place of class for declarative instances - basically any instance which is a singleton in that context (maybe global, per-session, or per-parent instance for inner objects). You can set the lifecycle of the object declaratively or inherit it from the outer instance.

How do you separate code from configuration at all then? All your code may start out in one layer for prototyping because it's so easy to split layers and how convenient to have all files in one directory when you are writing code for a new project. Because of the static typing model presented by a layer, you can combine or separate intermediate layers as long as you preserve the results expected by the layers you publish. When following this approach, your initial solution is simpler and easier to maintain and evolves to meet needs big or small later. You can split out layers of configuration as needed for any audience.

The layered approach to component assembly provides customization without upfront planning, a solution that improves code reusability.

One of the more important hidden features provided by IOC is the ability to wire together graphs of components, even when those graphs contain cycles - i.e. recursive references. To support this case in StrataCode just add the @Component annotation to the class. StrataCode changes the way the code is expressed in the generated class to support recursive references and a multi-step initialization phase.

It's not entirely seamless to move from regular Java classes to using StrataCode's @Component annotation, but it's a graceful transition when you find you need it in a design. Many basic classes require no code changes when you add the @Component annotation. Under the hood, processed code that's compiled, any code and any non-trivial references from the constructor or instance initializers are moved to a preInit method. This method is called after the object is constructed and available via it's own getX method. For more complex classes, the developer may need to move code into an 'init()' or 'start()' method, so that dependent references are all initializes to the necessary level (preInit, init, or start).

Since your original code is transformed in this process, maintaining the structure, and comments, it's relatively easy to figure out what's going on in the debugger as you can step through the entire construction process using either the original or generate code.

Creating object instances

StrataCode adds to the Java syntax a new keyword: "object" that works a lot like Scala's object keyword. It defines a new variable and creates an instance for that variable automatically when it's accessed or through custom rules defined at code-generation time. A framework developer has control over how the 'object' keyword is converted to Java by setting annotations and attaching custom code-templates to a type or base type. Annotation layers let framework developers customize even compile classes this way. Each layer becomes a sandbox designed for customizing a particular type of asset.

Here are a couple of simple examples that show typical code that's generated.

If in "myInstance.sc" you have:

object myInstance {
}
the generated code looks like:
class myInstance {
    private static myInstance myInstance;
    static myInstance getMyInstance() {
        if (myInstance == null)
            myInstance = new myInstance();
        return myInstance;
    }
}

StrataCode creates a class for the object with a static property which lazily creates the default instance. In your code, you can refer to the object with an ordinary variable e.g. 'myInstance'. At code-generation time, StrataCode will transform that variable into a method call: myInstance.getMyInstance(). We don't synchronize here around the instance creation because framework patterns typically used in StrataCode will synchronize at a higher level. If you need a thread-safe object though, it's a simple change to the code template which you can then configure for a given base-class, for the objects in a specific layer, or globally for all applications using a particular layer.

If you define an inner object, it works like an inner class in Java:

class MyClass {
   object innerObject {
   }
}

This defines an inner object, with a single instance created for each instance of MyClass. At code generation time, StrataCode generates an instance variable to hold the instance and a similar getInnerObject method inside of MyClass to lazily create and retrieve the instance. As an optimization, StrataCode may not generate a class for each inner object. Many inner objects are just configured instances of some other type and this makes the implementation more efficient.

Framework layers can use inner objects for the parent/child relationship by adding the "setParent" call or constructor parameters during code-generation.

Properties

Java programmers have been traditionally advised to avoid exposing fields directly in APIs. Instead the convention used in Java is to define two methods called getX and setX to implement a property called "x". Programmers have to then call the getX or setX method instead of directly manipulating the field. Later, the implementation of the property can change without affecting the calling code. The pattern works well enough, but it's a lot more code to write and makes code less readable.

StrataCode formalizes the Java convention by selectively automating the getX/setX conversion - handling both sides: converting fields to getX and setX methods as needed and converting expressions using fields to use getX and setX method calls as needed. If you define your code with explicit setX/getX methods, you can still just use them as you would Java fields and those references are converted. Your code looks simpler, cleaner and for those cases where getX/setX conversion is not required, you can use fields to avoid those extra method calls. You can use fields knowing that you can eventually change them to getX and setX methods, or have them generated for you later without breaking code.

Frameworks can customize the getX and setX code generated for properties to interact with framework code. You can add logging, tracing, and validation hooks to groups of properties. And unlike byte-code enhancement, you can debug those extensions easily.

Properties with data binding

The data binding system detects when properties are used in data binding expressions and generates getX and setX methods with the proper code to implement the binding. The setX method will trigger a change event typically, or the getX method may force the lazy-evaluation of the binding (if that's ever needed). The details of how this happens are managed in framework layers so application programmers and business users only see properties that may be attached to data binding rules. They are provided efficient and powerful abstractions over properties which can adapt from framework to framework.

In most cases, the default is what you need but there are ways to override the automatic getX/setX conversion. You can:

  • use @GetSet on a field to force getX/setX method for that property (or @Bindable to force get/set and add the data binding event)
  • use @ManualGetSet on a method to disable the automatic get/set conversion for expressions in that method that would ordinarily be converted.
  • refer to a field with the "this." prefix to avoid a getX conversion for a single expression.

Additionally, the conversion is disabled automatically for a field in it's own getX and setX methods.

Recursive references in java

Due to the design of the Java language, when you initialize a field with an expression, that expression can not refer to fields whose values are not yet defined - so called "forward references", or "recursive references". If the fields in your object in any way are initialized to values which refer back to the owner object, your code will either not compile or just not work. Component frameworks support recursive references by using a multi-step initialization process. Objects are created and registered in the name space, then references are resolved, then initialization code can run that requires the object to be fully initialized.

Programmers try to avoid recursive references during design for good reasons. They create less modular code, and the multi-step init process is more complex at runtime. But when you need to add one in an evolved design, it can be very hard to refactor code to keep all references going in the same direction. In the real world, data sometimes must flow upstreamand to arrange for the proper API hooks to allow this can be awkward.

That's probably why declarative languages like HTML support recursive references built-in to their syntax. It's more intuitive and less error prone to wire together your components when you can cut and paste around in a file without worrying about the ordering.

This split between "desired syntax" and "language behavior" in Java is thus lacking. We need a great path for evolving code efficiently using one core syntax, that gives the Java developer better tools for supporting components with a more compatible syntax. Today in Java's existing component frameworks, you move configuration back and forth between Java and XML, or at least pick a design that's more configuration heavy than optimal to avoid changing things down the road. That's because most changes will affect published APIs which we want to avoid for most efficient development.

Recursive references in StrataCode using @Component

StrataCode offers a nice solution for the Java programmer. You can add the @Component annotation to your StrataCode class to change the generated Java code to allow that class to use recursive references. StrataCode transforms your Java code to use a multi-step initialization sequence for such classes. First it creates all instances in the reference graph and sets their member variables so the getX methods work. Then the instance variables are assigned. Then initialization code is run for any component classes reachable in the graph from this object.

For simple cases, there are no code changes required once you add @Component. But for complex cases, you may need to move code from your constructor into an init() or start() method that's called alter, once all dependencies are satisfied. Because these methods are all called automatically, you can just define an init() or start() method and put the code there. If your framework already has an "init()" or "start()" method that conflicts, use IAltComponent and override _init() or _start().

Constructor properties

Normally a property assignment is initialized as part of the init code for the instance, or in the preInit method for an @Component. There is no guarantee these properties will be initialized before binding expressions run and so the value might be null, even when initialized. For important properties, where null checks become awkward, use constructor properties. This is set via an annotation: @CompilerSettings(constructorProperties="name1,name2"). The initialization value for a constructor property cannot refer to other properties of the instance since they will be run before the instance is created and passed to a generated constructor. This generated constructor is automatically propagated to all sub-objects that might inherit from a base object.

Issues and warnings

Note: When using @Component, the constructor code is run before reference properties are assigned unless they are constructor properties. It would be nice if by the time constructor code is run, all instance variables are at least assigned unless a recursive reference prevents it.

It would be nice to be able to configure the list of stages required for a given component class (e.g. init, start, validate, etc.)

Scopes: customizing object lifecycle

A declarative framework that only supports static objects only goes so far. StrataCode makes the object lifecycle a customization "hinge point" so the same object operator can be used to create applications deployed where instances live with different lifecycles. For example, deploy an application which runs with separate state for each window, versus a shared state keeping all windows for the same session in sync, or all windows sharing the same collaboration session in sync.

Adding a new lifecycle gives a framework developer a powerful new way to reuse declarative applications. To add a new lifecycle, you add code templates to customize the generated getX method for accessing that instance, how fields are synchronized, add init-code for creating a new instance or destroying an instance.

The framework developer can structure layers that provide defaults to make it easy.

Framework layers can control the lifecycle of the page objects by using the scope operator or the @Scope annotation.

For example, the HTML framework provides these scopes:

  • global: like static instances on the server
  • app-global: per separate application id. For a web-application, by default the page's type-name is used as the application-id.
  • session: per browser session
  • app-session: per application specific browser session
  • window: per browser window
  • request: per request

Scopes are arranged in parent-child relationships, where a scope such as "app-session" can be a child of both "app-global" and "session".

When you use data binding with scopes, you can annotate certain bindings as "cross scope" bindings with @Bindable(crossScope=true). Events for these bindings are queued up before being delivered to the other scope. They would typically be delivered in a different thread, which was operating in the context of that scope. For example, if objects in a session are receiving events from a global object through a data binding, the binding would be marked 'cross scope'.

What this means is that you can build declarative, realtime, and collaborative applications which are thread-safe and efficient.

Note on crossScope: Today, you need to set the crossScope annotation manually but it seems likely in the future we can detect when it should be set automatically. Or at least generate a warning which propagates changes 'up scope' without a crossScope annotation. Also, the cost of crossScope is just another thread-local call to check if the scope has changed, plus the overhead for queuing the change when it has so it's possible we could even make this the default or at least warn when in a non-production environment.