ET++ case study (pattern review)

ET++ is a user interface framework similar to MacApp. It contains every pattern we've done so far, so it makes great final case study. The first lecture will describe the overall structure of ET++ and identify the patterns we've seen before. The second lecture will introduce new patterns used in ET++.

What is a framework?

Technically, a framework is a special kind of class library which aspires to provide a ready-made architecture for communication and control flow. This is in contrast to a toolbox, which only provides components which can be assembled into an architecture. Think of a toolbox as the leaves of a call tree and a framework as the interior nodes. A framework tries to absorb as much of the control flow as possible, so that you just have to provide event-receiving objects. "Don't call us, we'll call you."

For example, in ET++, MacApp, and MFC you build an application by defining a Document class and its associated View class. The framework is in control: it initializes the windows, defines the default menus and dialogs like "would you like to save changes?", and instantiates your classes for receiving input events. After handling an event, your class returns control to the framework. OpenDoc takes this paradigm even further. You now only provide the components of a document; the document representation itself is handled by the framework.

A framework is a useful concept because it encapsulates an architectural pattern. This makes it easy to reuse the pattern; even compose it with other patterns. If encapsulated properly, you could even substitute another architecture without changing the application-specific classes.

How to use a framework

Frameworks are usually based on subclassing: you extend certain classes in the library to do application-specific things. Virtually all of the examples in this course have used object composition to create variabilities; frameworks demonstrate how inheritance can be used in similar ways. If you have an existing set of objects that you want to integrate into the framework, you can use the Adaptor pattern to make them look like framework objects.

Hook methods

The basic idea is to break the framework's algorithm into clearly defined parts which are expected to change. Such an algorithm is sometimes called a template, where the variable parts are called hooks. If the hooks are calls to virtual methods, then you can change the hooks by overriding the methods in a subclass. For example, ET++ and MacApp prefix all of their hook methods by "Do", e.g. DoObserve. The methods for the template can be non-virtual. Note the difference between using Strategy objects for the hooks: here the hooks are bound at compile-time.

Another way to think about inheritance is as a function call. A class plays the role of a function: non-virtual methods are the body, pure virtual methods are arguments, and bound virtual methods are optional arguments. Subclassing is filling in all or some of the arguments (called "currying" when done with functions). However, the arguments are filled by name, not position. You can even provide extra arguments, for extending the class. Thus we see that the virtual methods of a class are just as important in specifying variabilities as the arguments of a function. Probably more so, since a class has a larger role than a function.

You can also use this analogy to motivate parameterized classes, aka template classes in C++. Parameterized classes act just like functions, but on the program text level. Like regular functions, you specify arguments by position. Though they require more anticipation, parameterized classes are generally a safer mechanism and an easier concept to grasp than inheritance. Thus, the CLU language features parameterized classes instead of inheritance.

Factories

A common example of a hook method is to provide a factory: a method for instantiating a class. Factories are needed in C++ because the argument to new must be a compile-time constant: either a class name or a template parameter. Frameworks must instantiate your classes in order for your code to be used. The solution taken in ET++ is to encapsulate all instantiations in separate factory methods, prefixed by "DoMake", e.g. DoMakeDocument. To tell the framework to instantiate a different class, you override the factory method in a subclass.

A drawback to using hooks for factories is the chain-reaction of subclassing. For example, to tell ET++ to use a your new Button, you have to make a new View subclass to instantiate it, then a new Document subclass to instantiate that new View, and finally a new Application subclass to instantiate that new Document.

In other words, when you make a new subclass, all of its creators, as well as their creators, and so on, must be modified. Thus even simple applications like "Hello, World" become bloated with class definitions.

Fortunately, there are alternative ways to implement factories:

Parameterized classes
Make a template class parameterized by the class to instantiate. This is generally an alternative to any inheritance solution.

Strategy object
Call a Strategy object to do the instantiation. This avoids chain-reaction completely because the Strategy can be assigned and changed at run-time. Such a Strategy is called an Abstract Factory in Design Patterns. An Abstract Factory can have methods for instantiating several different classes, allowing you to encapsulate class families like the choice of look-and-feel, window system, or operating system.

Flexible instantiation
Don't use the new operator. For example, COM allows instantiation from a class ID, which is needed for loading objects and creating Proxies. By using COM, MFC avoids the factory issue altogether. Prototype-based languages, which do not distinguish classes from objects, also allow flexible instantiation.

A way to think about the factory problem in C++ is that the new operator creates coupling with the global scope. These techniques are general ways to reduce coupling. For example, an Abstract Factory is essentially encapsulating the relevant parts of the global scope into an object.

For more discussion of inheritance in frameworks, see Design Patterns for Object-Oriented Software Development, by Wolfgang Pree, Addison Wesley. 1995.

Pattern review

Dependencies

Almost every class in ET++ inherits from the Object class, which provides core functionality. Part of the Object interface are the methods AddObserver, RemoveObserver, Send, Changed and DoObserve, which allow any Object to be either a subject or observer as in the Observer pattern. This is very similar to Smalltalk.

Inspection

Each class in ET++ has an associated run-time data structure (a class descriptor) which describes its fields, including which fields are used for containment, acquaintance, or observation. This allows dynamic access to object state. For example, an "inspect click" on a visual object in any ET++ application will pop up an inspector window showing the object's data. You can even view the entire run-time object structure as a dynamically-changing network of containment, acquaintance, and observer links. This helps debugging and can be the basis of a visual application builder. We saw similar reflection capabilities in CORBA's Interface Description Language and OpenDoc's scripting mechanism.

Drag and drop

The uniform data transfer needed for drag and drop is performed by Data objects. These act as carriers for the dragged data, advertising the formats available. Data objects can perform several conversions on the fly. Recipients can ask for a particular class by using its class descriptor.

Screen management

Visual appearance in ET++ is described by a hierarchical composition of objects of class VObject. A VObject is lightweight, only storing by default a bounding box and a pointer to its parent. This permits a great deal of sharing. For example, the individual characters in a text document can be VObjects. The same graphic can be mirrored in separate windows, though perhaps at different offsets in the graphic, e.g. different pages of the same document.

Some VObjects are atoms while others are containers. For example, the TextItem VObject simply displays a line of text, while the Border VObject decorates its child with a border and title. The Clipper VObject defines a clipping region, translation, and scale for its child VObject. The Box VObject lays out multiple children in rows and columns. The Text VObject lays out its children according to word, sentence, and paragraph breaks. The child VObjects need not be characters but also embedded images, buttons, hyperlinks, and markers to link to. The actual layout algorithm is delegated to a Strategy object. As far as it is concerned, it is laying out ordinary characters.

Some VObjects are heavyweight, such as the View. It maintains a current selection and handles cut and paste operations. The View may contain or have an Observer relationship with its underlying data model. Like most user interface frameworks before OpenDoc, ET++ does not place nearly as much emphasis on structuring the data model as on structuring the visual appearance.

Because of its similarity to an Inventor scene graph, one would think that the VObjects implement the Interpreter pattern. However, closer inspection reveals a lazy, pull-style Stream much like ImageVision. One clue is that Clippers Decorate the VObjects that they affect, rather than simply appearing earlier in the traversal like Inventor's Transform nodes. Rendering in ET++ is started by calling Draw(rectangle) on the root VObject. A parent VObject may modify the rectangle, perhaps shrinking it, determine which of its children intersect the new rectangle, and then send the appropriate Draw calls to the relevant children ("source nodes"). As usual in a pull-style stream, unnecessary drawing is automatically pruned (since it is never asked for) and source nodes can operate in parallel. One difference with a conventional stream is that no data is returned; source nodes operate by side-effect on the screen.

Redraws are handled automatically. When a VObject changes, say by user interaction, it notifies its parent that a particular rectangle is now invalid. If the rectangle is not visible, the notification will be ignored by the parent. Otherwise, the rectangle will continue up the tree, possibly being modified by decorators, until it reaches a Window, which will then initiate a Draw on the invalid rectangle. Thus the invalidation process is the logical inverse of the drawing process. We recognize this as the Streams pattern again, this time using the push-style.

Character streams

ET++ also uses the Streams pattern for reading and writing character streams. Class StreamBuf simply has methods for reading a character and writing a character. Subclasses like LZWFilter contain the next StreamBuf in the chain. To read a character, they ask the next StreamBuf, do some processing, and return the result, as in a pull-style Stream. To write a character, they process it and send the result to the next StreamBuf, as in a push-style Stream. This symmetric processing model makes sure operations are performed in the correct order. The LZWFilter performs compression, while the ProgressFilter periodically invokes a callback, e.g. for visual monitoring. Filters can perform arbitrary amounts of buffering.

Event dispatch

ET++ uses a Chain of Responsibility to dispatch input events. The innermost VObject receives the event first and converts it into a method call on itself, e.g. DoLeftButtonDownCommand. We recognize this as a hook method because it starts with "Do". Event handlers are rebound in ET++ by subclassing. The default handler, though, simply calls the same method on the container VObject.


Thomas Minka
Last modified: Tue Feb 11 21:24:55 EST 1997