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.
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.
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:
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.
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.
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.
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.