The Motivation for Layers

Layers are as much a design pattern as language feature. They introduce both a new hinge point to extend and reuse code and allow new ways for building and maintaining large, complex systems. Layers are a new twist to object oriented programming, letting you refine a class or instance without renaming.

More Flexible Than Modules

Like modules, they are a packaging mechanism with dependencies. We say that a layer may extend one or more other layers which ensures those other layers are below it in the stack.. Where you'd use a module before, you'll use a layer with StrataCode. Unlike modules though, layers can be merged. The process works like layers in Photoshop but where we merge by name, not pixel position.

Still Statically Typed

Layers preserve encapsulation and are statically typed, adhering to the SOLID principles of O/O design. They may replace or add to an object's interface or override but cannot remove a previously defined contract. They also cannot depend on a layer that comes after them in the current list of layers. Just like Java, you have traceable code paths, edit time errors, find usages, refactoring and more. They are similar to Delta oriented programming and can also be used for product feature-lines.

Modules and Circular References

As modules grow in size, they tend to develop circular dependencies. For example, Module A depends naturally on Module B but as the project grows, some minor aspect of B may develop a dependency on some minor aspect of A, possibly due to a poor modularization decision. Refactoring to eliminate the circular dependency would work but breaks compatibility and so may not be an option. Allowing the new circular dependency creates severe code management problems. Conceptually now A and B must be updated in lockstep. You lose many of the benefits of modularization in the first place. Changes in B may depend on changes in A and vice versa. It becomes impossible to test a new version of A with an old version of B and vice versa. That reduces your ability to detect when the interfaces of your modules change in an incompatible way.

Layers Eliminate Circular References

With layers, a downstream layer cannot refer to an upstream layer. Code dependencies are enforced at compilation time to ensure dependencies only go one way. This is essential to preserving modularity as systems grow.

Instead, when you would need to add a cyclic dependencies, due to the addition of that minor feature, this code is added in a separate layer that extends both of the previous A and B layers. It separates that minor dependency from the bulk of the code. You retain the independence of the rest of the code without breaking APIs or compatibility. If you want existing clients to see this new module, the new layer is named "A" and the old "A" is renamed. If you want to offer this as an opt-in feature, you give the new layer a new name and tell clients to point to that layer to get that feature.

Languages like OCaml do not allow cyclic references between modules. As your system grows over time though, some change may require refactoring to move code from one type to another to avoid an upstream dependency. That will inevitably break any code using the code that has to move, causing unnecessary work, headache, and overall friction for the consumers of technology. Layers give you the best of both worlds: enforcement of one-way dependencies in the core modular structure, and the ability to incrementally add cyclic relationships to types while keeping the code modular.

Here's as attempt to illustrate:

Dependencies between Modules and Layers

Here we need to add upstream dependencies to types C and E. With layers we can do so without creating a hairball by modifying C and E in a new layer. Also, we can move code between layers without changing the APIs seen by the supported layer. If we move code between modules, we break APIs.

Dealing with Complex Types

It's very difficult to keep two complex types in your system from developing a circular dependency between them. The most intuitive types are built from natural entities in the system and two complex entities may need to depend on each other to implement certain features. By splitting complex types into well organized layers, you can support these types of use cases beautifully, and make those large files easier to navigate as well.

Mechanics of Layers

Like modules, layers may extend one or more other layers which they depend upon. This exposes all of the types and instances in that layer to the extending layer. Layers also can pass along imports to subsequent layers. Each layer gets a nice sandbox of all of the types imported by the layers it extends. Downstream code does not depend on the exact package name of an imported type, allowing you to easily substitute variants by adding an intermediate layer.

You typically run one layer which pulls in dependent layers automatically. You might however specify a list of layers to run and all dependent layers are automatically sorted and included in the application. Ultimately each collection of layers - a layered system - maintains a single ordered list of layers at any given time which are merged to form the "runtime view" of the application.

Although layers can modify any type, in any package it's often convenient to specify a base package for the layer. When you do, files in the layer's directory use that package without the extra directories for each package name. This is particularly nice when layers are edited by non-programmers, or when you can infer categorization by context. Instead of a "one size fits all" project directory structure, framework developers can create simpler project types, customized for different groups of users: e.g. deployment-configuration, application-configuration, source code, localization, user-interface style, etc.

Certain layers are designed as "build layers". These layers are validated and compiled to produce Java classes or applications which can be run or tested. When you compile a stack of layers, you might have more than one build layer in the stack. In that case, the build is done incrementally - so only the changed types from one stack to the next are re-generated and recompiled. This makes it efficient to define incremental builds - so only the source you are changing gets recompiled from one build to the next.

Why Layers?

Why objects, why methods, etc. I think object-oriented design missed a key hinge point - the ability to refine types. Some languages add this, but in an ad-hoc way. Perhaps the lack of layers has fueled the growth for dynamically typed languages. It's hard to describe why you should adopt a new language paradigm.

The most important benefits of layers show up when maintaining large customized systems, like those commonly found in enterprise development. Layers help manage workflows for enterprise systems, separating design, administration, business rules, workflow, and code.

When combined with StrataCode's declarative features, layers provide a hinge point for customizing any property of any object. You can replace any DOM element of any HTML file. Append to or replace any component. Override any formula. Though layers add a new concept, the IDE and management UI frameworks make it easy to build powerful, customizable applications.

Pure Domain Models

Among software architects, the term "domain model" refers to the core business code, independent of it's dependencies on the user-interface, database, etc. It specifies the types of objects involved in the business, the properties of those objects, configuration of the objects, operations performed on them, and rules that should be applied or enforced. Ideally this model is mostly declarative and as independent from the rest of the system as it needs to be used where you are coding up a business process using that model. The speed you can evolve your domain model is a major contributor to your overall efficiency.

But when expressed as types in your programming languages, your domain model becomes rich types that collect code and dependencies rapidly. Each dependency limits your ability to reuse that code in other contexts. Layering helps you preserve an independent version of your domain model which is expressed in all of the contexts in which it's required. Runtime dependencies exist only in the generated code for your model classes - not in the domain model source. Using annotations, base layers, base classes, etc. you can write powerful framework features to manipulate the code of your domain model so it's properly serialized, stored in a database, etc. When necessary, you can modify your types using platform specific layers of your domain model.

Here's a simple domain model object:

class Quiz {
   String name;
   List<question> questions; 
}

Add JPA persistence with:

@Entity
@Table(name="quiz")
Quiz {
   // Primary key is the quiz name
   override @Id name;

   // Define a one to many relationship between a quiz and its
   // questions.  The questions will automatically be persisted when the
   // quiz is persisted.
   override @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER) questions;
}

We could have added these annotations using a framework layer automatically using sensible defaults but here you see how to add annotations to existing properties and methods from a layer. Just referring to a property in a layer adds that property to a list that's accessible to the framework. This lets you set properties for the layer that apply to all properties in the layer.

To use a quiz, you might also initialize it:

object ScienceQuiz extends Quiz {}

ScienceQuiz {
   name = "Science";

   questions {
      object question1 extends Question {
         question = "The galaxy we live in is called the Milky Way.  It is shaped\n" +
                    "approximately like:";
         answerChoices = { "A round ball", "A doughnut", "A pretzel", "A flat spiral" };
         answerIndex = 3;
         answerDetail = "The Milky Way has four spiral arms radiating out from a\n" +
                        "central cluster of stars (nucleus).  Our solar system is\n" +
                        "located on one of the spiral arms, quite far from the\n" +
                        "center.";
      }
   }
}

This is a nice example of a configuration layer you could hand-off to someone else.

To make that easy, each layer is stored in a separate directory with parallel file names and path structure which can be swapped in and out of the application for different purposes. You can map where source files in a layer end up in the build configuration by extending base layers which define the context.

So layers help partition assets among people: business analysts, designers etc. One person defines the data model, another manages persistence, a third manages business data and rules. Layers help separate assets along role boundaries for better workflows.

Just as layers separate code by their dependencies, so do they separate code by the individuals who manage them. These two are commonly related for essential reasons, so why not use an environment which supports that separation.

Styles and Design Elements

Style sheets separate configuration from code in a clean way and are very powerful but sometimes overkill and overly complicated. How do you find and isolate the effects of a code change? Style sheets do not have the equivalent of a "find usages" at development time. Fortunately good debuggers help us trace that down at runtime or we'd be lost.

Sometimes it's better to constrain things and not use the more flexible selector types in CSS. If you use a simpler model for style management with standard types, layers and multiple inheritance you retain the ability to find all usages and make changes using static typing for more reliability and control.

Layers by themselves provide separation between code and styles. The code exposes the customizable properties - on both classes and instances through the same name-space. Designers deal with a single tree of types and properties. All of this is toolable because of strong typing, and Java's visibility rules.

UnitConverter {
   foreground = Color.WHITE;
   background = Color.BLACK;
   errorLabel {
     foreground = Color.RED;
   }
}

StrataCode's multiple inheritance feature lets you apply properties across the class hierarchy as well. This gives you a strongly typed version of stylesheet's "class" selector. Object names are analagos to the "id" selector.

The design phase pratically requires immediate updates so you can experiment with different looks quickly and dynamic layers let you do that without compromising on runtime performance.

Testing/Monitoring

It's common to want to instrument your code in various ways to improve it's testability and monitorability. In some cases, there are hard tradeoffs to make for performance or readability. Aspect oriented programming for a while was viewed as a viable approach to this problem for inserting code involved in "cross-cutting" concerns: logging, assertions, timing, etc. While it did reduce the quantity of code you had to write, it did not use traceable or debuggable patterns and so never gained widespread traction.

Layers let you inject code from separate files so that you can add this code in a way that preserves these code paths. For example, you could modify the core 'process()' method with a method that does timing around that method, then called super.process(). When you include this layer, you'd have a monitored version of the application and when it was excluded you'd avoid that overhead and any dependencies it created to the monitoring package. The layer itself would be a directory tree that showed all monitoring hinge points. If you renamed the "process" method, this layer would be updated automatically... if you deleted it, you'd get an error reminding you that you need to update the monitoring hook.

Here's a simple example of inserting monitoring code before and after some servlet:

MainProductionServlet {
  void service(ServletRequest request, ServletResponse response) {
    SystemMonitor.monitorStart("ProductionServlet");
    boolean success = false;
    try {
       super.service(reuqest, response);
       success = true;
    }
    finally {
       SystemMonitor.monitorEnd("ProductionServlet", success);
    }
  }
}

Behind the scenes, StrataCode will create a new MainProductionServlet class when this testing layer is included. It will use that class instead of the default one. You can create layers which add much more testing and monitoring logic because it is isolated both from the core code and core runtime.

Localization

Localization is one of the most common and most important forms of application customization. Layers give you some new tools and benefits as with other forms of customization.

For programmers, the easiest way to develop code is to hardcode the strings so it's easiest to change them and understand the code. Localization typically involves putting a resource identifier into the code and moving the string into a separate file. Some systems let you use one language in the code and then provide a separate file that maps strings in that language to another based on string-name.

Both of these schemes have problems. Most of the time, you are debugging the program in one locale so the resource identifier schema adds overhead to something you do a lot - go from error message to source code. But that's just an annoyance. You also lose static typing for compile time errors. Instead you find out about missing resources at runtime... which means you have to run everything in each locale. Given the number of possible locales and the breadth of your test suite, that inevitably leads to error messages you fail to localize.

programmers define strings with static final/const variables and initialize them as they might normally. You simply override those strings in sub-layers, one for each locale. At compile time, a new class is generated which replaces the original strings with the new strings. You get a new efficient localized executable for a smaller download size. Or use a dynamic layer and apply it at runtime for a resource bundle approach.

MainAppPanel {
   welcomeMessage="Welcome {0}!";
}

Localization does not stop at changing strings. You might also need to adjust UI spacing for a given language, or re-arrange menu items to preserve case-sensitive order. With StrataCode, one layer can manage all of these aspects by providing one modularization structure that can modify code and configuration making it easy to manage and build localized systems.

MainAppPanel {
   welcomeMessage = "Willkommen {0}!";
   leftSideWidth := windowSize * 0.25;
}