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.