next up previous contents
Next: Packages Up: No Title Previous: Introduction

Technical Design

Principles and Design Rules

This section describes the system- and language- independent rules we tried to apply to the overall design of BISS AWT. This is a somewhat philosophical topic, but there are so many systems without any evidence of a pre-implementation design that it seems justified (at least to remind ourselves) to spend a few thoughts on this issue. If you are familiar with systems design and just want to know about how certain BISS AWT tools or packages can be used, you might skip this section.

The first rule to mention always should be the meta rule: these things don't exist per se, their only legitimation is to fulfill some purpose. If it comes to a conflict between different rules, decide on how to proceed not by the best-match on all incorporated rules. Decide on what these rules are used for in this context. Use them as a guideline, not as a corset.

Small Number of Components

This really looks too commonplace to mention. Of course, there should not be a plethora of classes, packages or concepts (since all have to be understood and maintained). But then there is evolution. More and more features are added to the framework and sooner or later it ends up violating exactly this simplest of all rules.

The consequence first seems uncomfortable: during this evolution, there has to be a constant struggle to make things smaller again. Features should just be added if they fit in or if overall complexity could be reduced otherwise. Fighting the evil entropy shall never be ceased.

Leveled Tree Structure

The term ``leveled tree structure'' summarizes three different aspects:

 figure89
Figure 2.1: leveled tree structure

What is behind this rule is simply the observation that the most easy and obvious way to organize a framework is by using a hierarchical structure (tree) for the elements (e.g. classes) which is overlayed by a sequential structure (level set) for element groups (e.g. packages).

Generalized Packages

There are many possible organizational units on top of simple classes (e.g. applications, modules, libraries etc.). Most of them don't map very well to implementation languages. Instead of introducing such non-formal constructs (which would have to be supported / enforced by a non-standard tool layer), BISS AWT utilizes ordinary Java packages for the following purposes:

Usage of Patterns

If the ``leveled tree'' is the right measure to ease understanding of the big picture, consistent usage of a set of commonly accepted ``patterns'' is the right choice to let you understand the details of a framework. Things just look familiar if you identify a pattern you already found in other parts of the system.

The term ``pattern'' is used with the meaning described by the well known pattern book. In short, it is a set of classes and instances with some common scheme of collaboration to fulfill a specific purpose.

Inheritance vs. Parameterization

If it comes to increasing reusability, most people today first think of class inheritance (due to the non-technical hype OO experienced during the last years).

Class inheritance first and foremost is a mechanism to factor out common things (states and behavior) of a set of partly equivalent types. It is the right measure to avoid redundancy in a set of known classes, not to increase general reusability of a class. If it is misused like that, the number of classes in the whole system will explode. Think of implementing application specific Button behavior in Java programs by deriving a Button class (with re-implemented handleEvent() methods) for every specific Button usage (Edit, Save, Open, Cancel, ...).

With respect to reusability, it is much better to strive for generic classes which utilize parameterization to adapt to a variety of different contexts. Parameters don't have to be restricted to static behavior (colors, size etc.),they may also include collaborators (like Observers registered within an Observable) or actions (e.g. objects representing commands). With its single rooted type system, its instanceof operator, and especially its interface construct, Java is very suitable for this kind of programming. We consider this as a very important mechanism for implementing reusable GUI frameworks. Its exploitation is just limited by how easy it is to understand such a parameterized class model (classes should turn into amorphous monsters, they still should be designed for a single, specific purpose).

Mechanisms and Patterns

While the last section was rather abstract, this one lists the specific Java mechanisms and pattern implementations we used within the BISS AWT.

First of all, we tried to exploit already existing Java classes / patterns for all general mechanisms that can be found in the biss framework. We strongly feel that Javas standard libraries should not be proliferated with non-orthogonal concepts and classes (which would make it harder to understand). It there is a corresponding mechanism within the standard java libraries, this one should be used even if it is not the most efficient one.

Lib Classes

So called lib classes are collections of static functions that can be applied to certain standard Java types (e.g. biss.StringLib for java.lang.String). The main purpose of lib classes is to implement general functions which are missing in the standard Java classes without the need to subclass them. Subclassing of basic java.lang.* types should be avoided as far as possible in order to ensure coexistence of different class libraries.

A typical example is a Date conversion function which is capable of dealing with European date formats (dd.mm.yy). This is implemented in a class biss.DateLib that contains just such missing general functions working on common java.util.Date objects. Lib classes are usually concentrated in the biss package

PackageProperties and Package Classes

Given all the variety of potential platforms where Java apps can be run, there is a generalized need to parameterize these applications. In order to ensure that these apps are runnable, it can never be assumed that such settings exist. But if they do, they should provide a flexible, yet general way to tailor the app to specific needs (ranging from display characteristics to special file system specifications).

In order to make this as portable and robust as possible, these settings should be specified in simple text files (analogous to Unix resources) rather than using mysterious (and notoriously broken) binary repositories.

The java.util.Property class was designed to fulfill these requirements and therefor forms the base on which package classes are implemented.

Basically, package classes provide some kind of package local properties (as opposed to the system properties of standard Java). Such properties may be used frequently from various different locations of an app. It therefor is appropriate to transform properties to their final representation (e.g. a Point instead of a String containing two integer tokens). This can also be found in the standard Java system (e.g. static File.separatorChar which is initialized from the system property file.separator). But rather than (sometimes redundantly) spreading these static fields over various classes, we have chosen a convention of having one specialized class in each package which is in need of such properties. This so called package class is named after the package it belongs to (e.g. Jde for package biss.jde). It consists just of public static fields which are initialized from standard Java property files by means of String conversions (with ensured initialization) which are supplied by class biss.PackageProperties. It may also include some static functions which are of general interest for the particular package (something like a package specific java.lang.System class).

The most obvious example of a package class is biss.awt.Awt:

0pt []class Awt {
  static PackageProperties Values =
                            new PackageProperties(biss.awt);
  ..

  public static Font SysFont;
  ..
  static {
    ..
    SysFont = Values.getFont( SysFont, Helvetica BOLD 12);
    ..
  }
}

Property files are simple text files which consist of key-value pairs and are read in via the standard java.util.Properties.load() method. They are currently looked up in the following order:

Keys in a Property file should be fully qualified (include the package prefix) in order to enable accumulation of several package properties in a single file (which is not yet supported). File biss.awt is an example of such a Property file:

0pt []# file ~/.java/biss.awt: standard biss.awt properties / parameters
..
biss.awt.SysFont=Dialog BOLD 14
..

Do not use backslashes (' tex2html_wrap1162') within path specifications (java.util.Property might translate it, e.g '..tex2html_wrap1162n..'). Input has to be conforming to the standard java.util.Properties (e.g. no spaces before or after the '='),

ObserverSockets

The ObserverSocket is a generalization of the standard Observer/Observable mechanism found in java.util. Within the biss framework, it is used for type-decoupling of various components (server components always should use as less type information as possible about their potential clients in order to increase reusability).

Despite of being a very useful pattern for GUI frameworks, the Observer / Observable mechanism has two deficiencies:

The plain Observer mechanism requires the observed objects class to be derived from Observable and therefor is not applicable for arbitrary type subtrees (e.g. the widget classes).

In addition, it is often useful to let the Observer know about what kind of change within the Observable caused the notification without introducing specialized state objects. While this could be done by passing the information via the Observable.notifyObservers( Object arg) parameter, this often is inconvenient because the parameter is required to pass additional information, the event might happen too frequently (performance penalty by creating parameter objects), or the usage of notifyObservers() might be too widespread (imposing a consistency problem).

0pt []class SomeServer extends Observable {
  int State = UNINITIALIZED;
  public int getState();
  ..
  void initialize (..) {
    ..
    State = INITIALIZED;
    notifyObservers(..)
  }
  ..
}

class SomeClient implements Observer {
  SomeServer Server = ..;
  ..
  public void update ( Observable obs, Object arg ) {
    ..
    if ( obs == server ) {
      switch ( server.State ) {
      ..
      }
    }
    ..
  }
}

Both can be achieved by adding observable fields to the observed class instead of deriving the whole thing from a particular base. Class biss.ObserverSocket (which in turn is an Observable ) is used for this purpose and just adds some additional capabilities to the plain vanilla Observable (extra parameter, category etc.).

0pt []class SomeServer extends tex2html_wrap1158whatevertex2html_wrap1159 {
  public ObserverSocket OsInitialized;
  ..
  void initialize(..) {
    ..
    OsInitialized.notifyObservers( arg)
    ..
  }
}

class SomeClient implements Observer {
  SomeServer Server = ..;
  ..
  public void update ( Observable obs, Object arg ) {
    ..
   if ( obs == Server.OsInitialized ) {
      ..
    }
    ..
  }
}

 figure138
Figure 2.2: ObserverSocket

ObserverSocket instances may have a few other attributes compared with ordinary Observables. They may have a symbolic name (or socket-class) which can be used to symbolically refer to a specific socket (or a set of sockets). There also is an additional argument placeholder in case the standard Observable argument is not sufficient.

With this generalization, the Observer mechanism is used consistently throughout the whole biss framework (not just for model components). Most classes can be thought of as finite-state-machines where each transition (change of state) that might be interesting for some client maps to such an ObserverSocket field. Just classes where all transitions at all times should be reported to all clients are derived directly from Observable (to avoid extra effort for keeping the Observer registration consistent).

It shouldn't be concealed that we don't consider the ObserverSocket mechanism being a grand new invention (neither is the Observer which has been around for several years implemented in many different languages). When we started with biss.awt, we first used some more sophisticated things like methodRefs (implemented by dynamically created reference classes). But we decided againt such complicated mechanisms because almost all of our goals could also be achieved with the much simpler Observable/Observer (and its beefed up ObserverSocket variant). We were not forced to introduce new concepts or change existing classes (like JDK 1.1) in order to get a more elaborate event handling. This one has equal power and runs on every Java platform.

MLP triad as an MVC replacement

MLP, which is short for Model, Logical view, Physical view, is an adaption of the classical Model, View, Controller pattern introduced by Smalltalk 80.

It is an approach to organize applications consisting of visible parts (views) and non-visible parts (models) in a way that strives for the best decoupling of these components. Ideally, a view should not know about the model which provides its logical contents, a model should neither know about who is displaying the provided information, nor should it know about how it is displayed. Models should also be capable of dealing with several views at the same time. Within the MVC, this was best described by the term pluggable views. The weak spot in MVC always has been the controller part, mainly because:

MLP uses the Observer / ObserverSocket approach for all three components.

Physical View

A physical view is the most simple part of it and consists just of the definition of the view (its components and layout). Usually, it holds just instance fields for its components and a default constructor to define the layout. In order to ease user interface prototyping, (pane specific) pop-up menus and simple layout related behavior (e.g. zooming of components) can be implemented in a physical view too. But otherwise the physical view does not care or even know about displayed contents or user input reactions. Physical views should be generated by some kind of a graphical interface editor (to support the tedious layout work) and are executable units (have a main func to ease prototyping). Physical views have to be derived from some kind of a Frame or Dialog.

Look at biss.jde.LibViewer as an example:

0pt []class LibViewer extends TopWindow .. {
  ..
  ObjectPane ItemPane = ..; // fields holding the widget components
  CheckButton FileBtn = ..;
  ..
  public LibViewer () {
    setLayout( ..);
    add( ItemPane, ..);
    ..
    pack();
    show();
  }
  ..
  public static void main ( String[] args ) {
    new LibViewer();
  }
}

Model

Models should be designed as FSMs with a suitable number of ObserverSockets. They should not care about the nature of potential clients (either physical or logical views). Usually, there is more than just a single Model class involved and there is some kind of an access- or master- model which manages various different low level models. Often these access-models are designed as singletons (to ease multiple synchronized views on the same data). Class biss.jde.SourceLibrary (managing lots of biss.jde.CompileUnit instances) may be used as an example. There is no need to derive model classes from a specific base.

0pt []class SourceLibrary { // access model for more primitive model objects
  ..
  static SourceLibrary defaultLib;
  Vector CU; // container for CompileUnits
  ObserverSocket OsScanReady;
  ..
  void scanSourceDirs (..) {
    CU = ..;
    ..
    OsScanReady.notifyObservers(..);
  }
  ..
  public static SourceLibrary getDefault () {
    if ( defaultLib == null )
      defaultLib = ..;
    return defaultLib;
  }
}

Models are the right components to gain reusability of a system. Much more than the logical or physical views, they can be structured in a way which is not specific to a single application (they should serve a whole set of equal-branched applications).

Logical View

The logical view brings in the dynamic behavior. It connects the physical view to the model by means of ObserverSocket entries. Its most important part therefor is the update() method. A logical view usually is the main entry point of an application (its main() method starts the application) and therefor usually (directly or indirectly) creates required physical view or model objects. There is a 1:1 relation between a physical and logical view which also has to manage the state (e.g. selections) of a view. Logical views have to implement the Observer interface, they don't have to be derived from a particular base. Class biss.jde.LibBrowser may serve as an example:

0pt []class LibBrowser implements Observer {
  LibViewer View = new LibViewer();
  SourceLibrary Lib = SourceLibrary.getDefaultLib();
  ..
  public LibBrowser () {
    ..
    Lib.OsScanReady.addObserver( this);
    View.FileBtn.OsAction.addObserver( this);
    ..
    if ( !Lib.isInitialized() )
      Lib.scanSourceDirs();
    ..
  }
  ..

  public void update ( Observable obs, Object arg ) {
    ..
    else if ( obs == Lib.OsScanReady ) { ..
      updateContents();
    }
    ..
    else if ( obs == View.FileBtn.OsAction ) { ..
      setFileMode(); ..
    }
    ..
  }
  ..

  void updateContents() {
    ..
    View.ItemPane.setContents( Lib.getDisplayItems());
  }
  ..
}

In addition to react on Observable notifications, another important role of the logical View is to remember the view state (e.g. selections, active related views/dialogs, expanded objects etc.).

In case of physical view components with configurable display behavior (e.g. biss.awt.Lists), the logocal view usually provides (or dispatches) the draw methods.

This part of the MLP should neither deal with data formats, data manipulation or view layout. It is meant to be the ``glue object''between its collaborators. Within the triad, the logical view usually shows the lowest reusability (is specific to a certain application).

Logical view programming can be augmented by specialized tools too. For example, the update() method could be created by a specialized editor which recursively traverses all visible fields in search for potential Observable / ObserverSocket objects.

figure166

We don't think of the MLP as the only way to design interactive applications (because there is no implication of special bases, a physical and logical view might be identical in simple cases). Neither do we enforce this pattern. But we definitely had it in mind when we designed the biss.awt package.

Type Decoupling Choices

Given the ObserverSocket mechanism described above, there is the following list of choices (with decreasing degrees of coupling) to invoke actions within a set of peer objects (not to be mixed with Java peers):

Explicit Call

This is a static (compile-time) binding between components. It is the most efficient one, but obviously causes the tightest coupling. Because it requires the incorporated types to know about each other, it may introduce a mutual reference problem (which sometimes just can be avoided by introducing additional interfaces).

Explicit ObserverSocket - Field Reference

This scheme already uses the ObserverSocket mechanism described above, i.e. only the client has to know about the servers type. By explicit, it is meant that the client directly references a particular ObserverSocket field within the server. This has the significant implication that a whole chain of fields (up to the specified socket) has to be accessible (visible) for the server type (e.g. PersonViewer.NameEntry.OsValidate ).

This has the obvious drawback of forcing a lot of public fields (which otherwise should be better hidden for information hiding purposes, e.g. the components of a logical view).

But it also has its benefits. Branching within an update() method can be done by means of identity checks

0pt []...
if ( obs == PersonViewer.NameEntry.OsValidate )
  ...

which could be significantly more efficient in case of frequent events. Of course, this should be used with special care to avoid NullPointerExceptions within the context of temporary objects (e.g. if the above PersonViewer might be just a transient dialog which has to be checked for existence before referring to one of its fields).

Using direct ObserverSocket field references also opens up the opportunity to build specialized editors for linkage specification. Because this tool would be based just upon field type analysis (which is done by other tools like the CUBrowser anyway), it would be much easier to implement than a scheme like the event delegation model of the JDK 1.1. It also requires much less classes and interfaces.

Symbolic ObserverSocket Reference

This is a slightly more decoupled version of an ObserverSocket based client - server object communication. Rather than using explicit field names, the client just uses the symbolic attribute of a socket to identify the server within its update() method:

0pt []if ( obs instanceof ObserverSocket ) {
  if ( ((ObserverSocket)obs).getCategory().equals( ``SaveAction'') )
    ...
}

Because of the increased costs and the need for a well known set of socket names, this scheme is used rather seldom.

Generic Command Parameters

The ObserverSocket mechanism can be exploited even further by means of the simple interface

0pt []package biss;
public interface Command {
  public boolean execute();
}

To make the client react on something which is not hardwired within the clients class, the server object could pass an update() parameter object which implements the Command interface. To catch all these cases in a generic way, the clients update() method then would have a prefix like:

0pt []...
if ( arg instanceof Command )
  ((Command)arg).execute();
...

This is some kind of a generic client utilization. Because the client still is the Observer, the command would be executed just within the clients context.

Stand-alone Command Objects

Within package biss, there is a class AutarkCommand which implements both the biss.Command and the java.util.Observer interfaces. Objects instantiated from this class can be used to trigger an action on a server event (notification) regardless of whoever registered this object within the ObserverSocket.

0pt []...
SomeServer.OsSomeEvent.addObserver( new MyCommand(..) );
...

This scheme can be used regardless of the state of the object which executed the above code. It actually might not even exist any more at the time the command is executed. This is definitely the mechanism with the best type decoupling, but it also comes with the highest costs (with respect to both speed and space).


next up previous contents
Next: Packages Up: No Title Previous: Introduction

James Hughes
Sat Jun 7 10:31:42 EDT 1997