Automatic dependency tracking discovers dependencies at runtime and updates the user interface Rube Goldberg, an award-winning cartoonist, drew incredibly complex contraptions that performed simple tasks. He described his machines as “symbols of man’s capacity for exerting maximum effort to accomplish minimal results.” Indeed, when we maintain software, we often feel trapped in one of his creations, tracing the path of the gloved hand to the egg-laying hen, to the shooting rocket, to the swinging clock pendulum. With automatic dependency tracking, you can break the complex chain of events so that the software updates itself. In Part 1 of this series, I laid the groundwork for an application based on automatic dependency tracking. The first step was to design and build the information model (IM). The IM records all the system’s logic; it is designed such that a client knowledgeable in the problem domain can build, traverse, and apply a solution. In this installment, I will construct a user interface (UI) that automatically discovers dependencies upon the IM and updates itself.Read the whole series on automatic dependency tracking: Part 1: Design an information model for automatically discovering dependencies in interactive object-oriented applicationsPart 2: Automatic dependency tracking discovers dependencies at runtime and updates the user interfacePart 3: Create a rich, interactive experience for your userThe application that we started in Part 1 is Nebula, a network design tool. Nebula’s information model records the connections among various network devices. While building the IM, we inserted dynamic sentries to monitor access and modification of dynamic data. While constructing the UI, we will insert dependent sentries to react to changes in the IM.We use automatic dependency tracking to simplify application development and maintenance. When we work with a program that discovers dependencies and updates attributes automatically, we can focus on application logic, not housekeeping. The program can also respond to changes in object dependency as we update the program, meaning that as the problem changes, the solution changes. We don’t have to go back and revisit old dependencies as we add new features.To achieve these worthwhile benefits, we must adopt a few disciplines. The following six guidelines will help us construct a user interface based on automatic dependency tracking: Specify the update method for each dependent attribute in an anonymous inner classCall a dependent sentry’s onGet() method to ensure that a dependent attribute is up to dateUse intermediate layers to project UI-specific information onto the IMRecycle objects in dependent collectionsModel actions that don’t cause changes as dependent stateModel actions that do cause changes as eventsSpecify an update methodTo use automatic dependency tracking, you simply express the calculation of each dependent attribute. The sentries determine when each calculation should occur. To express a dependent attribute’s calculation, supply a dependent sentry with an update method in the form of an anonymous inner class. The update method gathers all required information, performs the necessary calculations, and records the dependent attribute’s new value. As the application developer, you never call the update method directly. The dependent sentry calls the update methods when the dependent attribute needs updating. You pass the update methods into the dependent sentry’s constructor as an implementation of the IUpdate interface.Take, for example, the update method for a graphic hop’s bounding rectangle. A graphic hop, represented by the GraphicHop class, displays the IP address of a network interface card or router port near the attached device. To do its job, it must calculate the position on the screen where the text will appear. The text’s bounding rectangle is a dependent attribute, as it is calculated based on several pieces of information: the IP address text itself, the associated device’s location, and the cable’s direction. To calculate the bounding rectangle, we find the attached device’s location, go a fixed distance along the cable toward the remote device, and anchor one corner of the rectangle surrounding the text. The update method below performs these calculations: private Dependent m_depBounds = new Dependent( new IUpdate() { public void onUpdate() { IGraphicsInfo info = getGraphicsInfo(); // Get the size of the text. String strAddress = m_hop.getAddress().toString(); Dimension dimAddress = info.getStringSize( strAddress, g_font ); info.dispose(); // Find the location of the device and the angle of the cable. Point ptAnchor; Point ptRemote; if ( m_hop.getDevice() == m_locatedCable.getFrom().getDevice() ) { ptAnchor = m_locatedCable.getFrom().getLocation(); ptRemote = m_locatedCable.getTo().getLocation(); } else { ptAnchor = m_locatedCable.getTo().getLocation(); ptRemote = m_locatedCable.getFrom().getLocation(); } Dimension dimCable = new Dimension( ptRemote.x - ptAnchor.x, ptRemote.y - ptAnchor.y ); if ( dimCable.width == 0 && dimCable.height == 0 ) dimCable.height = -1; // Find the point a fixed distance from the device along the cable. double dFactor = 20.0 / Math.sqrt( dimCable.width*dimCable.width + dimCable.height*dimCable.height ); ptAnchor.x += (double)dimCable.width * dFactor; ptAnchor.y += (double)dimCable.height * dFactor; // Put one corner of the rectangle on that point. m_rectBounds.setLocation( ptAnchor ); m_rectBounds.setSize( dimAddress ); if ( dimCable.width < 0 ) m_rectBounds.translate( -m_rectBounds.width, 0 ); if ( dimCable.height > 0 ) m_rectBounds.translate( 0, -m_rectBounds.height ); } } ); Each GraphicHop object creates a Dependent sentry, m_depBounds, and gives it an IUpdate interface implementation. The anonymous inner class implements one method, onUpdate(), which gathers all the required information, calculates the bounding rectangle, and stores the result in m_rectBounds. Ensure up-to-date dependent attributesThe Dependent sentry m_depBounds invokes the update method whenever the dependent attribute m_rectBounds needs recalculating. Any code that accesses the attribute must therefore call m_depBounds.onGet() before obtaining m_rectBounds‘s value. Since the update method only invokes if the attribute is out of date, onGet() ensures that the attribute is up to date.To guarantee that onGet() is called prior to each access of m_rectBounds, the GraphicHop class defines the private method getBounds(), which appears below. Any method that requires the bounding rectangle calls getBounds() rather than accessing m_rectBounds directly, thus ensuring that the dynamic sentry is notified: private Rectangle getBounds() { m_depBounds.onGet(); return m_rectBounds; } You might wonder how a dependent sentry discovers the attributes upon which it depends. Notice that the update method for m_depBounds takes no parameters. Because it is not given any information to work with, the method must gather the information it requires. As it gathers information, the update method in the UI will call access methods in the IM. Recall from Part 1 that each access method calls a dynamic sentry’s onGet(). Quite simply, the onGet() method looks at the top of a stack to find the dependent sentry that is currently being updated. The dynamic sentry forms a link between itself and the dependent sentry so that when the dynamic attribute changes, the dependent attribute can be marked as out of date. Insert location informationThe information model, as I mentioned last month, is designed for a client knowledgeable in the problem domain. It is not designed specifically for a user interface. The Nebula IM in particular lacks location information for its devices, since such information is foreign to the problem itself. The Nebula UI does need to store location information, however, so we apply a new layer specifically for that purpose.The LocatedNetwork class is the location layer’s root. It contains a dependent attribute: m_mDevices, a collection of LocatedDevice objects. A LocatedDevice takes as definitive data the Device object that it represents. In addition, each LocatedDevice contains a dynamic Point: the device’s location. In this way, the location layer adds UI-specific information to the UI-ignorant information model.Recycle objects in dependent collectionsAs mentioned above, the m_mDevices attribute is dependent: it depends upon the collection of Device objects in the Network. One LocatedDevice object is created for each Device object in the information model. Like any dependent attribute, this one features an update method. But we must be careful; if the update method recreates the entire collection each time a network device is added or removed, the location of pre-existing devices would be lost. Instead, the update method must recycle former LocatedDevice objects as it updates the dependent collection. Recall from Part 1 that an object can have definitive state — state that is assigned upon construction and does not change throughout the object’s lifetime. While updating a dependent collection such as m_mDevices, we recycle objects based on their class and definitive state, since these two pieces of information are needed to construct an object. A LocatedDevice object, therefore, is recycled based upon its device type and the Device object that it represents.To achieve object recycling, the update method for LocatedNetwork‘s m_mDevices attribute places each LocatedDevice object in the dependent collection into a recycle bin. For each object that the update method tries to move back into the dependent collection, it passes a prototype through the recycle bin. If the bin contains a match in terms of both class and definitive state, the match is added to the dependent collection. If the bin contains no match, the prototype itself is used. When the update method is complete, all objects remaining in the recycle bin are thrown out. The update method for m_mDevices below uses a bin to recycle LocatedDevice objects: private Dependent m_depDevices = new Dependent( new IUpdate() { public void onUpdate() { // Dump all objects into the recycle bin. final RecycleBin bin = new RecycleBin(); Iterator it = m_mDevices.values().iterator(); while ( it.hasNext() ) { bin.insert( (IRecyclableObject)it.next() ); } IDeviceVisitor visitor = new IDeviceVisitor() { public void visitHub( Hub hub ) { // Pass a prototype LocatedHub through the recycle bin. m_mDevices.put( hub, bin.extract(new LocatedHub( hub )) ); } public void visitComputer( Computer computer ) { // Pass a prototype LocatedComputer through the recycle bin. m_mDevices.put( computer, bin.extract(new LocatedComputer( computer )) ); } public void visitRouter( Router router ) { // Pass a prototype LocatedRouter through the recycle bin. m_mDevices.put( router, bin.extract(new LocatedRouter( router )) ); } }; // Populate the dependent collection, extracting // from the bin when possible. m_mDevices.clear(); Device.ConstantIterator itDevices = m_network.getDeviceIterator(); while ( itDevices.hasNext() ) { itDevices.next().accept( visitor ); } // Throw away what's left. bin.dispose(); } } ); To support object recycling, the LocatedDevice class implements the IRecyclableObject interface; each type of LocatedDevice overrides the matches() method to compare definitive state. For example, a LocatedComputer object’s definitive data is the Computer object to which it is attached. The Computer object is therefore passed into the LocatedComputer‘s constructor and compared in its matches() method, as seen below: public LocatedComputer( Computer computer ) { m_computer = computer; } ... public boolean matches( IRecyclableObject obj ) { LocatedComputer that = (LocatedComputer)obj; return that.m_computer == m_computer; } Since the update method recycles LocatedDevice objects instead of recreating them, it preserves location information between updates. It successfully injects location information into the information model. With this UI-specific information in its proper place, we are now ready to build the graphics.Model some actions as dependent stateThe Abstract Windows Toolkit (AWT) and Swing both provide the same drawing mechanism. The Component class — and by inheritance the JComponent class — defines the paint() method specifically for that purpose. A derived class overrides this method to perform its own custom painting. Both AWT and Swing treat drawing as an action.However, when using automatic dependency tracking, we must view drawing differently. When a component draws itself, it doesn’t change its own state; it simply reflects its current state. So drawing is not an action, but is rather an observation. The way a component appears is dependent state. The collection of items on the screen is a component’s dependent attribute. The GraphicComponent class represents a JComponent with a dependent collection of items. It defines the onUpdateGraphicItems() method, which derived classes override to populate that collection. Each item in the collection is derived from GraphicItem, which represents a single image on the component.Two dependent attributes determine the appearance of a single GraphicItem: a Glyph and a Point. An abstract data type that represents the item’s image, a Glyph is a collection of strokes — that is, the geometric primitives: rectangles, ellipses, lines, and text. All glyph strokes are defined within a local coordinate system. The GraphicItem applies the draw offset to the draw glyph so the image can render anywhere on the screen. The derived class overrides the onUpdateDrawGlyph() method to construct a draw glyph and the onUpdateDrawOffset() method to determine its location.Using Glyph and Point, the GraphicComponent class responds to the paint() method by observing the UI’s current state. It tells each GraphicItem in its dependent collection to draw itself on the screen. The GraphicItem class paints its current glyph at its current location. Since both AWT and Swing treat drawing as an action, they require that applications inform them when an area of the screen needs to be redrawn. However, in an automatic dependency tracking application, the sentries figure out when a dependent attribute needs recalculating. The sentries call the attribute’s update method at the appropriate time, which makes the update method the ideal place to invalidate the component.The update for a GraphicItem‘s draw glyph is defined in an anonymous inner class; it is passed into the dependent sentry m_depDrawImage upon its construction. Before it calculates the new draw glyph and draw offset, the update method invalidates the area occupied by the old one. After the new glyph and offset are updated, it invalidates the new area. This causes Swing to call paint() as soon as it can so that the new glyph and offset appear to the user. The update method for drawing the image appears below: private Dependent m_depDrawImage = new Dependent( new IUpdate() { public void onUpdate() { // Invalidate the old image. m_drawGlyph.invalidate( m_container, m_drawOffset ); // Update the image. m_depDrawGlyph.onGet(); m_depDrawOffset.onGet(); // Invalidate the new image. m_drawGlyph.invalidate( m_container, m_drawOffset ); } }); The Glyph class represents the way a graphic item appears on the screen. It encapsulates an object’s image in terms of state, not actions such as paint or invalidate. The GraphicComponent and GraphicItem classes abstract the translation from Glyph to paint() and invalidate(), hiding such actions from the application. Application-specific UI classes specialize these two base classes, overriding their methods to provide a Glyph based upon the state of the IM and the location layer. Model other actions as eventsAlthough drawing a component does not alter its state, other actions do. Clicking or dragging a graphical item might cause changes to the system, whether to the UI, the location layer, or even to the information model itself. Actions that have the potential to change state are properly modeled as events.AWT and Swing provide a mechanism for responding to events such as mouse motion, mouse clicks, and keystrokes. The GraphicComponent class registers to receive those events and forwards them to the appropriate GraphicItem. To determine a mouse event’s target, the GraphicComponent identifies the item under the mouse. Hit testing, as this process is sometimes called, does not alter an object’s state; it merely identifies an event’s target. Therefore, hit testing, like drawing, is not an event but an observation. As for drawing, a GraphicItem uses the draw glyph and draw offset to test for a hit.When the GraphicComponent receives the MOUSE_PRESSED event, it hit tests to find the affected GraphicItem. Only after it receives the MOUSE_RELEASED event does it forward the onLClick() or onRClick() event to the GraphicItem. The event occurs only after the user completes the gesture. (Next month, we will see how to add feedback to the process.) The GraphicComponent‘s processMouseEvent() method appears below: public PopupMenu onUpdateContextMenu() { PopupMenu pm = new PopupMenu(); MenuItem item = new MenuItem("Delete"); item.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { deleteComputer(); } } ); pm.add( item ); if ( m_locatedComputer.getComputer().getKind() == Computer.SERVER ) item = new MenuItem("Change to workstation"); else item = new MenuItem("Change to server"); item.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { toggleKind(); } } ); pm.add( item ); return pm; } When the user selects either “Delete” or “Change to workstationserver,” the events operate directly on the IM to achieve the desired change. You will notice in the code below that the events do not attempt to update the UI. They simply call mutate methods on the Network or Computer object to which the GraphicComputer is attached, and let the automatic dependency tracking system update the UI. Thanks to the magic of the sentries, this happens with no programmer intervention. Here’s the code for the menu events: private void deleteComputer() { Computer computer = m_locatedComputer.getComputer(); computer.getNetwork().deleteDevice( computer ); } private void toggleKind() { if ( m_locatedComputer.getComputer().getKind() == Computer.SERVER ) m_locatedComputer.getComputer().setKind( Computer.WORKSTATION ); else m_locatedComputer.getComputer().setKind( Computer.SERVER ); } A similar interaction occurs when the user drags a GraphicItem. The GraphicComponent uses each item’s draw glyph and draw offset to identify the event’s target. When the user releases the mouse button, the GraphicComponent forwards the onLDrag() or onRDrag() event to the affected GraphicItem. Shown below, the GraphicComputer‘s onLDrag() event alters the location to which the LocatedComputer is attached: public void onLDrag( Point ptFrom, Point ptTo ) { // Get the original location. Point ptLocation = m_locatedComputer.getLocation(); // Determine the offset by which it was dragged. ptLocation.translate( ptTo.x - ptFrom.x, ptTo.y - ptFrom.y ); // Make that the new location. m_locatedComputer.setLocation( ptLocation ); } Again, the event acts directly upon the affected information, in this case the location layer. The event does not need to update the user interface; the dynamic and dependent sentries will perform that task. The dependent sentries in the user interface automatically discover dependencies upon the dynamic sentries in the information model and the location layer. Nebula contains more examples of automatic dependency tracking at work. For instance, it contains a split bar that separates two views of the same network. When you open the second view, you can make a change in one window and see the results immediately reflected in the other. No special code makes that possible. In fact, neither of the two windows knows about the other. Because they both depend upon the same location layer and information model, they both update when a change occurs. Compile the program and give it a try.Achieve simple and flexible programsAutomatic dependency tracking makes interactive program development a breeze. You no longer need to manually route update messages or register for notification. You simply use dynamic and dependent sentries in the right places, provide update methods for dependent attributes, and recycle objects in dependent collections. When modeling the application, design the information model for any client knowledgeable in the problem domain, and place UI-specific information in a separate layer. Finally, use dependent attributes to represent observations of the system’s state, and events to represent changes to the system’s state. When you adhere to these disciplines, you will be rewarded with greater simplicity, flexibility, and maintainability.In Part 3 of this series, we’ll take advantage of this flexibility by adding new features to the Nebula user interface. We will enhance the application’s user interaction with item selection, drag echo, and new item placement. Of course, we will model such interaction with dynamic and dependent state, and implement these features with automatic dependency tracking. Michael L. Perry has been a professional Windows developer for more than seven years and maintains expertise in COM, Java, XML, SOAP, .Net, and other technologies. He formed Mallard Software Designs in 1998, where he applies the mathematical rigor of proof — establishing the correctness of a solution before implementing it — to software design. Michael applies a cohesive set of rules to all his software models, one of which — dependency — is the foundation for automatic dependency tracking. Software DevelopmentJava