Automate dependency tracking, Part 1

Design an information model for automatically discovering dependencies in interactive object-oriented applications

In any interactive application, dependency tracking is one of the most significant problems you must solve. Usually, you apply some manual mechanism to update user interface (UI) components when the information model changes. For example, you might use the Observer pattern to notify registered objects of changes to a given subject. Or you might use the Publish/Subscribe pattern to broadcast changes to all listeners. Or you can route update messages with appropriate hints to all views. All these mechanisms work, but they all require effort by a person -- and that person will be you, the overworked programmer.

An automatic dependency tracking mechanism can handle most of that work for you. Using the automatic mechanism resembles programming with a spreadsheet; you simply enter formulas, and the computer figures out when to calculate them. The major difference is that the automatic mechanism is object-oriented and speaks Java.

The choice to track dependencies automatically is not based on laziness -- well, not entirely. By relieving the programmer from manually tracking dependencies, an automatic dependency mechanism removes a potential bug source. It also reduces the work needed to add features to the program. With a manual dependency mechanism, a program addition usually requires you to revisit prior dependencies to keep the user interface in sync with the model. However, when dependency is tracked automatically, the program discovers new dependencies on its own at runtime, with no programmer intervention. Of course, setting up such a mechanism takes some preparation.

In this series, you will learn what it takes to create an interactive application using automatic dependency tracking. Automatic dependency tracking separates the information model (IM) from the UI so that the UI discovers dependencies upon the IM and automatically updates itself. In this first installment, you will find out how to construct the IM for a dependency-tracking application. In Part 2, you will see how to build the UI on top of the IM. In Part 3, you will use dependency to solve some common application problems. Part 1 lays the groundwork for automatic dependency tracking. You won't actually see automatic dependency tracking in action until Part 2, but your patience will be rewarded.

Throughout the series we will develop Nebula, a drag-and-drop application for visually designing computer networks. With this application, network designers can model a physical network, determine addressing, and run simulations to discover problems. Admittedly, this is a complex example, but as you will see by the end of the series, automatic dependency tracking makes even complex problems quite manageable.

Read the whole series on automatic dependency tracking:

Let's start at the beginning: a properly designed information model.

Design the information model

An information model is a system of software objects that models a set of problems in a given domain. Because our problem domain is network design, our IM consists of objects such as computers, routers, cables, and hubs. An IM's client is a person or software system that understands the problem domain. The IM interface (public class methods) should be designed specifically for someone knowledgeable in that domain. In other words, a network designer with the Nebula IM, a Java compiler, and a working knowledge of programming should be able to construct a network simulation straight from the source code. The UML diagram in Figure 1 depicts the information model for Nebula.

Figure 1. The Nebula information model

Like any good software system, an information model is designed to accomplish specific tasks. Particularly, an IM must provide means for performing three activities:

  1. Build: The client must be able to construct a model to the required degree of complexity to represent the problem.
  2. Traverse: The client must be able to browse or search a given model to identify objects of interest, discover their information, and make required changes.
  3. Apply: The client must be able to solve problems using the model, usually by evoking its predictive abilities. Nebula, for example, can predict the path a packet will take through a network, identifying every device it reaches along the way.

Notice that an information model is not specifically designed for any given user interface. An IM has no presentation knowledge and doesn't create its own UI components such as visual proxies. This decision reduces coupling between the IM and UI, making the application design more maintainable; IM changes have little impact on UI code, just as UI changes have little impact on the IM.

When designing the information model for an automatic dependency tracking application, observe three guidelines:

  1. Clearly define ownership
  2. Rely on object relationships to store information
  3. Favor abstract data types over native types

Clearly define object ownership

Every object has exactly one owner, and ownership is not transferable. An object's owner is responsible for its creation and destruction. With clearly defined ownership, each object's responsibilities become more apparent. The owner provides means for building and traversing the objects under its control. (The owned objects themselves provide the third activity -- apply.)

In Nebula, the Network object forms the root of the ownership tree. The Network owns all the Devices as well as the Cables that connect them. Devices in turn own Ports. Different kinds of devices -- Hubs, Computers, and Routers -- own different kinds of ports. The Network provides a method for creating each device and methods for traversing the collection of devices. Whether port, device, or cable, each object has exactly one owner; at the top of the ownership hierarchy is the Network.

Object relationships store information

The second guideline to follow when designing the IM is to rely on object relationships to store information. The alternative, storing identifying attributes, leads to unnecessary searching and potential referential errors. Object relationships offer a much richer traversal mechanism than searches, perform better, and ensure data integrity. They also greatly simplify the UI's job, as you will see in Part 2.

In Nebula, a network interface card (NIC) has a default gateway, which tells it where to forward packets destined for addresses outside the local subnet. When configuring a single computer, you identify the default gateway by IP address. An IP address represents identity in the real network; it uniquely identifies a network node. However, in Nebula we can take advantage of true object identity. The Nebula information model already includes an object -- Hop -- that represents a packet's target. Therefore, a reference to a Hop object, not an IP address, identifies the default gateway.

There are many advantages to such a representation. For instance, we know for certain that a reference identifies an existing Hop object. If we relied upon an IP address, we would have to perform a search to validate the information. Second, each IP address is stored in only one place, the Hop object that it identifies. The network designer can easily change the address strategy without compromising the information's integrity. Third, we have direct access to the object that we wish to identify. We can use object references to traverse the network and immediately make requests of its various devices -- for example, to accept a packet.

Favor abstract data types

The IM's goal, remember, is to permit a client knowledgeable in the problem domain to access the information. To that end, you must expose information from the IM with access and mutate methods. The task for which it was designed demands it: build implies mutate; traverse implies access. However, you should not expose your classes' inner workings, as that would violate one of the fundamental rules of object-oriented design: information hiding. The trick is to hide the classes' inner workings while exposing information meaningful in the problem domain. Abstract data types (ADTs) do that well.

An ADT provides a set of values and the operations that can be performed over that value set. An ADT might be restrictive, represented only by a small set of values (such as an enumeration), or it may be quite general, with a large or infinite set of values (like a stack). An ADT represents its values and operations in the most appropriate form for the desired application. Most likely the set of values and operations defined by int or double don't exactly match your desired problem domain. An ADT lets you be more precise.

The ADT IP represents both IP addresses and subnets in Nebula. The IP ADT defines the set of 32-bit IP addresses with subnet masks. It includes a few utility operations, such as conversion to and from a string, and a few application-specific operations, such as testing for an address's inclusion in a given subnet. The information appropriate to the problem domain is available, as the client can obtain any Hop's IP address. But information is also hidden, in that you cannot see from the interface how an IP is actually represented.

Clearly defined ownership, reliance on object relationships, and preference for ADTs govern the IM's design. Committing that design to code requires an additional set of disciplines.

Code the information model

When constructing the information model for an automatic dependency tracking application, apply three coding patterns:

  1. Distinguish between definitive and dynamic state
  2. Accept visitors to discover derived class type
  3. Treat object relationships as identities and ADTs as values

Definitive versus dynamic state

Dynamic state is state that can change. It is typically modified in a mutate method and retrieved by an access method. Definitive state, on the other hand, does not change at any point during an object's lifetime. It is typically passed in as a constructor parameter and either used internally or exposed via an access method.

For automatic dependency tracking to work, you must tell the dependency system when a dynamic attribute is accessed or mutated. To accomplish that, create a dynamic sentry, an object of type Dynamic that rides alongside the actual dynamic attribute. Whenever the dynamic attribute is accessed, you must invoke the dynamic sentry's onGet() method. Whenever the dynamic attribute mutates, invoke the dynamic sentry's onSet() method.

However, definitive state requires no such sentry. Because definitive state does not change during an object's lifetime, it will never mutate. Dependence upon definitive state resembles dependence upon a constant: since you know it will never change, you don't need to track it. Therefore, you don't waste any effort tracking dependencies on definitive state.

In Nebula, a Hop's IP address is dynamic. The Hop class has a mutate method setAddress(), which changes the address attribute, and an access method getAddress(), which retrieves it. Therefore, each method invokes a dynamic sentry. The Hop class appears below, complete with access and mutate methods:

abstract public class Hop extends Port
{
    public Hop()
    {
    }
    public IP getAddress()
    {
        m_dynAddress.onGet();
        return m_address;
    }
    public void setAddress( IP address )
    {
        if ( !m_address.equals(address) )
        {
            m_dynAddress.onSet();
            m_address = address;
        }
    }
    // Dynamic data
    private IP m_address = new IP();
    // Dynamic sentries
    private Dynamic m_dynAddress = new Dynamic();
}

Note that the above mutate method performs some redundancy checking. It only invokes onSet() if the attribute actually changes. This optimization saves some update time if an attribute's value is simply reasserted.

Collections can also be dynamic. For instance, the device collection in the Network is dynamic because the collection itself can change. The methods that add and remove devices are mutators, while the one that retrieves the device iterator is an accessor. These methods, too, invoke a dynamic sentry, as shown below:

public class Network
{
    public Network()
    {
    }
    // Device access
    public Hub createHub()
    {
        // Add a new hub to the device collection.
        m_dynDevices.onSet();
        Hub pNew = new Hub();
        m_lDevices.add( pNew );
        return pNew;
    }
    ...
    public void deleteDevice( Device victim )
    {
        // Remove a device from the device collection.
        if ( m_lDevices.remove(victim) )
        {
            m_dynDevices.onSet();
        }
    }
    public Device.ConstantIterator getDeviceIterator()
    {
        // Traverse the device collection.
        m_dynDevices.onGet();
        return new Device.ConstantIterator( m_lDevices.iterator() );
    }
    // Data values
    private List m_lDevices = new LinkedList(); // of Devices
    ...
    // Dynamic sentries
    private Dynamic m_dynDevices = new Dynamic();
    ...
}
1 2 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more