Continuous Change Architecture

July 30, 1998 - Jack Harich - Document Map

Architecture is a system's most important partitions and their interactions. Architectural goals assure that subsequent decisions will result in a "good" system. Our most important goal is supporting the Continuous Change Process. Key subgoals are technology independence, separation of state from logic, conceptual simplicity and a more human interface.


Continuous Change System - Top Level Model

Our partions are carefully abstracted to make building huge systems via continuous change as easy as possible. We need lots of loose coupling so that change in one area has minimal impact on other areas.

Evolver - Our first cleavage is to separate "systems" from the tool that builds them. This allows different builder tools to create or modify the same system(s). It allows builder tool version migration and system version migration to proceed independently. Most importantly it encourages third party builder tools and "buildable" systems. We have decoupled "change" from what we are changing. We use the word "Evolver" it indicate that all systems evolve as changes occur, and to have a unique, well understood term. The word "Builder" can too easily mean the whole ball of wax, as indeed it does for Magic, Visual Basic, Access, Delphi, etc.

System - Next we decide how to best architect a "System". A good architect always works at the highest level of abstraction possible and learns from the experience of others.

There have been large productivity gains by separating business rules (aka parameters) from business behavior. SAP and PeopleSoft have built software empires with this key concept. So has Mother Nature, where DNA defines entire systems rather efficiently. XML has emerged as a document standard that can drive renderers and container workers, such as Java Applets. Many languages define GUIs with "resource files" such as Visual Basic and C++. The common pattern is to separate "what" from "how". This becomes one of our partitions, as seen in the System Defintion subsystem.

There have also been large productivity gains from reusable components. This was pioneered by Visual Basic and emulated by Delphi, PowerBuilder, etc, and is the emerging silver bullet. So we need a Reusable Components partition. Note most of these limit their components to visual ones, with a few serving a non-visible role. We anticipate that as component technology matures, most will be non-visual, since that's where most code is.

Now how are we going to run a system that's defined in one place and uses components in another place? We need a mediator, which is the System Engine. Its role is to, given a System Definition, initialize and start the system, getting components as needed. While the system is running the System Engine can provide various infrastructure such as component collaboration services.

Implementation - In our first iteration we have:



System Definition - A Hierarchical Tree

The objective of this partion is to define "what" to do, not "how" to do it. "What" is best expressed as pure declarative knowledge. We use the format of parameters in a text file or database element, which we call a Param in the model. Thus the design of this partion is centered around a large collection of Params. We edit these with the Parameter Editor.

To find a Param we use a Marker Class. This is a Java class that may or may not have any behavior. Given a Marker Class, we can call Class.getResourseAsStream(resourceName) to get the parameter text or any relative file. Each System Tree's root must have a Marker Class. System Trees are reused by other System Trees pointing toward their Marker Class. This key mechanism provides easy file management, system reuse, platform portability and internet deployment, because getResoruceAsStream() works even across the internet.

We provide the structure of a visual hierarchical.tree by using Baskets as our containers and Components as our leafs. A Basket may contain Baskets and/or Components. Each Basket has a parent, though the root has no parent. Since we have two kinds of tree items, we have two very different kinds of Params: one for Baskets and one for parameter driven Components. See Parameter Examples.

It's important to design the System Engine to not start the entire System Tree when the root is started. Instead we start Baskets when they receive an event or a priviledged Component starts an unstarted Basket. This allows huge systems to be started quickly.

A System Defintion is currently stored as a directory branch which reflects the System Tree structure. The root directory has a Marker Class that is also the BeanAsembler entry point. Each directory is a Basket on the System Tree. Params are stored in as text in files. Each directory contains the Basket's Param file, plus other Param files for Basket members. It may also contain custom Java classes, images, html, etc. This is all wonderfully intuitive - The visual System Tree is reflected in the file structure. Future versions may store some of this in databases, which while more powerful, are harder to manage. They are justified for larger systems.


Collaboration in a System Tree

How do Components collaborate? Remember we are shooting for low coupling and ultra-high reuse, and must support visual tools and conceptual simplicity. We have settled on "anonymous events" as the preferred inter-component mechanism, not method calls.

Anonymous events can be implemented by having collaborators all use the same event and listener interface, rather than unique events and listener interfaces as favored in the Java Bean Specification. We have tried both, and find a single reusable event far more productive than many unique events. In particular, it's hard to design reusable mediators (Baskets) if a resuable coupling mechanism is not used. It also hard to design reusable components if you don't know what listener or source type they must collaborate with. We have deliberately departed from the Bean spec to gain high reuse. This is similar to Message Oriented Middleware, but occurs at a smaller granularity.

In the Bean Assembler we use Message as the event, MessageListener as the event listener interface and MessageSource as the event source interface. We also have a MessageRouter for routing events. Each Message has a locally unique name, such as "LogonRequested" or "EditCustomer". Basket Params define which Components are listening for which Messages from which source Components. This is exactly what gobs of code does, but is much cleaner, more understandable and reusable.

In addition to a name, a Message has properties. This allows passing and receiving complex state. The properties can have any unique name and any value. Convenience methods are provided for popular types such as Object, String, int and boolean. Messages can be modified by a listener and the modified property(s) used by the source to "acquire" data. We minimize "acquire" events.

To promote loose coupling, a Message event does NOT have the event source. This is too often cast to a particular type(s) and a dependency is established. We stridently avoid such dependencies, which are bad practice since in most cases a listener should not know what the event source was. If source knowledge is needed (which is infrequent) then it should be explicited designed in, such as with a Message property, container service (see below) or a Component property set in a Basket. This is another departure from the Bean spec.

For understandability and ease of assembly, MessageListeners publish the Message names they are interested in, and MessageSources publish what Messages they fire and what they contain, with a description of each property. This is how the Messages tab on the Inspector gathers its data, and you can now see how powerful that tool is.

Note we do not use a single centralized MessageRouter. Instead we have as local a route as possible. This makes event paths (dependencies) more clear, reduces event namespace conflicts, and above all makes reusable subsystems embedded in larger systems independent of the larger systems.

We also provide ContainerServices for Component collaboration. Each Basket has a ContainerServices. Any Component implementing ContainerServicesUser will be provided with a reference to the Basket's ContainerServices when the Component is initialized. Each ContainerServices knows it's parent ContainerServices, which allows setting a named service high in a system hierarchy and allowing all below to use that service. Services can be shadowed. Examples of services are Persistence, DynamicParameterSettings, UserSecurity and Naming. This is the Service Architecture pattern. It is similar to CORBA services, but occurs at a smaller granularity and is much simpler.