December 1996
Hide your Data and Make Objects Responsible for Their Own User Interfaces, Part II
Allen Holub
Allen Holub is a programmer, consultant, and trainer specializing in C++, object-oriented design, and Microsoft operating systems. He can be reached at allen@holub.com or http://www.holub.com.
The central idea of object-oriented (OO) design is data hiding-the contents of one class should be unknown to all other classes. How can you initialize, modify, or display an object if you have no idea what's inside it? An MFC CDialog, for example, violates the data-hiding rule right and left because, by nature, the CDialog must export data to a separate object. The main problem is maintenance. Changes to one class definition shouldn't ripple to other class definitions. A change should be localized; it should be possible to make radical changes to a class definition, and as long as the public interface is unaffected, the rest of the program should be unaffected. Designing a public interface that works in this manner is difficult, I might add, but is the central theme of all OO design methodologies.
This article is the second of a three-part series that began with "Hide your Data and Make Objects Responsible for Their Own User Interface" (MSJ, August 1996). That article discussed the problems of using CDialog derivatives to display an Employee object's attributes. Changes to the Employee class affect every CDialog derivative that's used to transfer information from the user to an Employee or vice versa, and there could easily be twenty or thirty such CDialog derivatives with definitions scattered all over the program. Moreover, these same CDialog derivatives might also be used to initialize or display more than one high-level object, and there might be complex interactions between the high-level objects. (An HR_Department object might be used to validate the data in the Employee object, for example.) Consequently, a change to the original Employee could easily affect all the other objects that are indirectly coupled to it via the shared CDialog. When you consider all the coupled classes, the dust blown up by making a seemingly simple change to the Employee class could take weeks to settle. The technical term that describes this architecture is Unmaintainable Mess.
The solution is to give each object control over its own user interface. You shouldn't get data from somewhere (like a CDialog) and then put it somewhere else (like an Employee); rather, you should tell the Employee to initialize itself interactively from the user or to display itself on the screen. A very simple object, such as the Text class I developed in Part I, can have an interface just that simple because the Text class has only one attribute to display or load. An Employee class is another matter, though. The problem, of course, is that an object with several attributes such as an Employee might need to be displayed in different ways at different times, and the information that comprises an Employee might not be available all at once on a single input screen.
Though the simple Text class interface from Part I doesn't solve the general problem, it does provide a basic building block for the general solution. Text implemented a generic user interface by deriving from an abstract User_interface class. User_interface defined three pure virtual methods: hide, display, and interact. The latter two are passed a pointer to a window on which to display themselves and a rectangle that specifies the position inside the window. (The position was defined by simple pixel-level-MM_TEXT-coordinates, but that's easy enough to change.) A User_interface object, when passed a "display" message, just displays itself on the screen. An "interact" message causes it to display itself in a way that permits the user to input directly to the object. A Text object, for example, creates an edit control and automatically transfers all characters typed into the edit control directly to the Text object's internal buffer as they are typed. Finally, the "hide" message causes the object to temporarily disappear. (The actual Windows¨ UI objects that display the User_interface object are probably destroyed as a side effect of a hide request, but they could just as well be hidden without being destroyed.)
The point of abstracting the three methods just described into a base class, rather than just implementing them in the Text class itself, is that pretty much any class that has only one externally visible attribute can be handled using that same simple interface. The actual UI can get quite elaborate. The interact method for the Date object, for example, might draw an entire calendar and let you pick a date by clicking on the face of the calendar. Derivation can also be used to provide several different display methodologies. A simple Date might use a simple edit control-based UI, while a Calendar class that derives from Date could override the display and interact virtual methods to use the more elaborate calendar-style interface.
An abstract User_interface base class sets things up so that you can write general-purpose functions that can display (or initialize) any object that implements a User_interface without having to know exactly what sort of object that you're dealing with. You could, for example, define a Form object that could display any number of fields-objects that implemented a User_interface-without having to know anything about the actual types of the fields. A simple Form could just keep a linked list of such objects and display them in sequence, for example. This is exactly where I'm headed in this part of the series. I'll use a more real-world architecture than a simple linked list, however.
A Note on TerminologyBefore jumping into the technical details of the design, a word of clarification is necessary. For those of you who aren't familiar with the terminology, an OO "message" has absolutely nothing to do with a Windows message like WM_CHAR (though a Windows message does indeed exhibit some message-like behavior). In standard OOPese, objects communicate with one another by sending messages. Message passing is implemented in different ways in different languages. In C++, for example, "sending a message" to an object involves calling a member function of either the receiving object's class or one of its base classes. When you send a message to an object, you effectively invoke the associated message-handler function in the receiving object. Other languages do it in different ways. I've used terms like "send a message" because the design I'll be discussing is really language (and to a large extent, operating system) independent, so using accepted non-Microsoft terminology seems sensible. Just bear in mind that the Windows SendMessage API has nothing to do with OO message passing. I'll make it clear when I mean a Windows message, so assume that generally a message is an OO thing, not a Windows thing.
FormsA form provides a surface on which an object can initialize or display its own attributes. This notion of attributes isn't well understood, so let's start there. An attribute is an essential characteristic of a class of objects-something that distinguishes that class from other classes. For example, a name is an essential attribute of a person-all people have names. A salary distinguishes a particular subclass of persons (employees) from other persons, so it is an attribute of the Employee subclass.
An attribute is not a field of any class. In some cases, there might be a one-to-one mapping from an attribute to a field-a Name attribute might be stored internally in a single Text object called Name-but that's just an implementation detail. You should never expose that field as part of the public interface because you want to be able to change the way the attribute is stored internally without affecting the users of a class. The attributes should always be modified by sending a message to the object or by calling a member function. There might not even be an obvious mapping from an attribute to a set of fields; the attribute could be synthesized from information in several fields, and changing an attribute might modify all of these fields. Moreover, the attributes don't necessarily represent state information in an object. One of the main attributes of any class is the set of public member functions: the public interface exposed by the class. The state-related attributes (a window's color or an Employee's salary, for example) are just as much a part of the class's "contract" as the public-member function definitions. In other words, it's reasonable to know that all windows have a color associated with them. It's unreasonable to know how the color is represented inside the window.
So how does all this relate to forms? It's perfectly reasonable to ask an object to represent a particular attribute on a given form, but it's not acceptable to ask an object to represent one of its fields on the form because the field's type could change during maintenance. The attributes of a class are external knowledge-they're part of the fundamental definition of the class. The fields are not; the way that the attributes are represented is (or should be) immaterial. The main difficulty, then, is to separate the attribute from the internal representation. I've done this in the code by naming the attributes (with a String) and asking the objects for proxies that represent the attribute whose name is represented in the String. The attribute names are part of the public definition of the object. The object itself creates the proxy and decides how the proxy looks and behaves, but all proxies have a common interface which is-surprise-a User_interface.
Getting back to the notion that a Form is a surface on which an object's attributes are placed, imagine that you need to initialize an Employee whose Name attribute was stored internally in a Text field. The Text object, when passed an "interact" message, causes an edit control to pop up; any data typed into that edit control goes straight to the underlying Text object. If the Name attribute is stored internally as Text, you load the Employee's Name attribute essentially by telling the Employee to put the user interface associated with the Name directly on the surface of some form. In this example, the Text object serves as a proxy for the Name attribute-it represents the name attribute to the user. (A Proxy class will be used for this purpose in the actual implementation. Proxies will be discussed in more detail later.) The form literally provides nothing but a place for an Employee object to put this proxy. Information typed by the user goes directly from the user's fingers into the proxy-the Employee's Name string-with no intermediaries involved.
This is a very different architecture from your run-of-the-mill MFC CDialog. A CDialog might hook up a CEdit object to a CString member of the CDialog derivative, but you'd have to get the data from that CString into an Employee somehow. Here, the data goes directly from the person typing into the Employee object.
The Form, on the other hand, knows nothing about the objects displayed on it other than the names of the attributes that need to be displayed or modified and where the proxies should be placed on the window. The proxy itself is provided by the object that's using the Form (the Employee) and all proxies have exactly the same interface. Since the Form is completely decoupled from the object, you can make massive changes to the way that the attributes are represented inside the object without affecting the Form one bit. Similarly, the objects don't know where on the Form their attributes are being displayed (or even which attributes will be displayed), so changes to the Form don't affect the objects either. The only time you'll need to change a Form is when the set of attributes for a given class changes, which shouldn't be very often if you came up with the design before you started madly typing away.
Implementing FormsFigure 1 shows the architecture of the Form package. This is a Booch-style class diagram; the notation was discussed in Part I of this article. I've moved in the direction of the Unified Booch/OMT notation by pulling nested classes outside the container and identifying the nesting with a base_class:: in the name.
Figure 1 Form Class Architecture
Note that the Form "is displayed on" a window, but this is a "uses," not an "is" (derives from) relationship. This is one place where MFC invariably gets it wrong, primarily because the designers were thinking in terms of the underlying operating system rather than in true abstractions. Derivation is used properly in only two situations, which can be characterized as "extends" and "implements" relationships. An extends relationship adds properties to an existing class, the "is-a" test discussed in most books on OO. (An employee "is-a" person since employees have all the characteristics of people-names, and so forth-but add additional characteristics such as salary.) An implements relationship is used primarily in multiple-inheritance scenarios when you want to mix a capability into a class. In Figure 1, a Form implements a User_interface by deriving from it. This way a Form can be passed to any function that takes a User_interface argument without difficulty. This sort of derivation fails the is-a test (a Form certainly is not a user interface) but is nonetheless a legitimate use of the mechanism.
Both kinds of derivation have a common characteristic since conversion to the base class is commonplace. Here's the rule of thumb: derivation is appropriate only when a derived-class object is occasionally treated as a base-class object. Looked at another way, don't use derivation if you never pass a derived-class object a message whose handler is defined in the base class. A Form is not a window-it could be displayed on paper rather than on the screen, for example. Forms and windows are different things with different properties. More to the point, Forms are never passed to functions that expect to deal with windows. A Form simply uses a window to display itself in some situations, so derivation is inappropriate here. You could really apply the same logic to MFC's CDialog class, which derives from MFC's version of a window: CWnd. I've never passed a CDialog object a message defined in CWnd, and I've never passed a CDialog to a function that expected a CWnd argument. I can imagine occasional rare situations where I might want to do the above, but I haven't yet. I can also imagine winning the lottery, but that hasn't happened either. Consequently, deriving CDialog from CWnd seems inappropriate.
The Form is made up of several Field objects, each of which knows the name of the attribute and its position on the Form; once the Form is activated at runtime, the Field keeps track of the associated proxy. There are two sorts of Field objects: a normal Field represents an attribute of some object, the name attribute of some Employee, for example. A static Field is an attribute of the Form itself, such as a label next to another Field or a decorative icon on the Form.
A normal Field is created using the first constructor shown in the Form::Field cloud in Figure 1. You give the constructor the name of the class whose attribute is going to be displayed in the Field along with an attribute identifier. (Different classes could have attributes that happened to have the same name, of course.) The identifier could be as simple as the attribute's name. (You might just use "name" as an identifier for an Employee's name attribute, for example.) You could also use a more elaborate attribute name if that makes sense. A Drawing object, for example, could be asked to display a small part of a big drawing by encoding the desired display rectangle into the attribute identifier. The third constructor argument is a Form-relative rectangle that shows the size and position of the Field on the Form. The final argument establishes the behavior of the attribute proxy-an object that represents the display mechanics-on the Form. Possible values are Field::input, Field::output, and Field::input_output (the default). The input_output mode is provided primarily for documentation-it behaves identically to an input Field. Output Fields always send display messages to their proxies, even when the containing Field is sent an "interact" message. Input and input-_output Fields are sent either display or interact messages, as appropriate.
Note that a normal Field as yet knows nothing about the actual object that it will display. The Form definition would normally be persistent-it might be stored on the disk or as a const global variable, for example. (I haven't implemented persistence in the current article to make things a bit simpler, but it's easy enough to do. See a previous article of mine, "Roll Your Own Persistence Implementations to go Beyond the MFC Frontier", MSJ, June 1996, for information.) The Field is connected to the actual object that's using the Form for I/O at runtime.
A static Field is created with the second constructor shown in Figure 1. It's used for icons, labels, and so forth. A static Field differs from a normal Field primarily in that it knows exactly what it's going to display at definition time. (Normal fields get proxies at runtime.) In addition to the position rectangle, the Field constructor is passed a pointer to an object that implements a User_interface, and that object is always displayed in the static Field. (The User_interface object is never sent an interact message, only a display message.) Since static Fields are always output only, there's no need to pass a Field::Behavior into the static-Field constructor.
Again, for simplicity's sake, I haven't implemented persistence, but User_interface objects used for static Fields should obviously be persistent too because they will have to be stored on the disk along with the rest of the Form.
The Field objects must be allocated dynamically via new. (Most persistence implementations, including my own and the one implemented in MFC, work this way.) The Form can be populated with Fields in two ways. The constructor can be passed a variable-length list of pointers to Field objects, or you can call add_field to add Fields to a previously constructed form. The Form passes all of its Fields to delete when it is destroyed. The main concept here is ownership. You own the Field when you create it, but once it's passed to the Form, the Form owns it. It's up to the owner to delete the object.
A Form is created like this:
Form *the_form =
new Form( "form_name", new Field( Text("Name:"),
// This next line is Static text
Rect(0, 0, 20, 10)),
// This next line is the Name Attribute
new Field("Employee", "Name", Rect(20,10, 100, 10)),
NULL
);
The Form is named "form_name." It contains a static Field that holds the label "Name." The second Field will eventually be used to display the Name attribute of the Employee class.
Automatic deletion of a Field by a Form is, of course, a potential maintenance problem because there's no way to stop someone from allocating a Field as a local variable and then passing a pointer to it to add_field. A sufficiently robust implementation of operator new and delete would detect this problem at runtime, but there's no guarantee that an idiot-proof memory manager will be used. In any event, the alternatives aren't much better. I could, for example, provide an overload of add_field that took a Field reference (rather than a pointer) for an argument, and then mark Fields that were added with the reference-version of add_field as "not to be passed to delete." All this overload does is introduce another possible bug, though: I could accidentally allocate a Field from new and then pass it to add_field using *p. The other possibility that comes to mind is passing the Fields by value, and actually putting copies of the Field into the Form. This is probably the safest path, but it also has the highest overhead. Passing by value can work if I use the same reference-counting strategy that I used in Part I's String class, but at the cost of considerable extra complexity. Requiring all Fields to be consistently allocated from new seems like a reasonable compromise.
Fill 'er UpA Form that's populated with Fields is only that-a Form. To display it you must first fill it in by attaching proxies for the attributes to the Form. Again, think of the Form as a surface on which objects can display themselves.
The process of attaching Fields in the Form to actual runtime objects (and then activating them) is shown in Figure 2. (I didn't use a diagram like this one in Part I because the model was so simple that an extra diagram wasn't necessary. Here it adds some clarity, though.) This diagram shows you three scenarios that demonstrate the interaction between objects in the runtime system while certain tasks are being performed. (The tasks are sometimes called use cases.) I've used a Booch-style message-trace diagram for the scenarios. The vertical lines represent actual runtime objects (not classes, which are compile-time things). The horizontal arrows represent messages being passed between objects. The message-trace diagram shows you what's actually happening in the system at runtime in temporal order. Time gets later as you move downward, so the "put yourself on this form" message is sent before the "put me on you" message. The vertical lines widen to become boxes to indicate that the object is active. This is a little different from simply indicating that you're in a function body, though it means that too. For example, the Element's put_yourself_on_this_form method sends a put_me_on_you message to the associated Form. The initialization process is not complete until the Form sends one or more give_me_ a_proxy_for(id) messages back to the Element, so the Element stays active until these messages are processed.
Figure 2 The Dynamic Model
So here you are at runtime with a Form of some sort and a bunch of objects that need to use the Form to interact with a user. You start populating the Form with proxies by asking the Form to display various Form::Element objects, using the Form::put_me_on_you message (which is passed a pointer to the element and a string holding the class name). For example, you could cause an Employee to attach itself to the Form created earlier and then activate the Form like this:
class Employee : public Form::Element {/*...*/};
Employee homer;
the_form->put_me_on_you("Employee", homer);
the_form->interact();
All objects that want to use a form for I/O must derive from Form::Element. The Form now asks its Fields whether they represent an attribute of that class by passing them an if_in_class_return_id message. The message handler returns a reference to a String that contains the attribute ID if the field indeed represents an attribute of the indicated class, otherwise it returns NULL. Note that the message takes the form "if you are a member of this class, do something," not "give me your class name so I can decide what to do." This organization is another example of what I mean by a class hiding the representation details.
If a Field indicates that it's associated with the requesting Element, the Form turns around and asks the Element for a Form::Proxy to use for the Field. The user interacts directly with this Proxy; the Field just provides a place for the Proxy to live. The Proxy class is an example of what's known in the OO world as the Decorator pattern-a class that extends the functionality of another class using containment rather than derivation. A Proxy is essentially a User_interface object that also implements the release message (discussed below). I didn't want to implement a Proxy by deriving from User_interface because I wanted any generic User_interface object to be useable as a Proxy. I didn't want to require the user of my forms package to derive from Proxy rather than directly from User_interface just so the interface object could be used on a Form. The Proxy then eats a User_interface object and exposes all the contained-object's functionality by exposing a member function for each public member function of the contained User_interface object. The need to provide a container function for every public containee function is one of the main disadvantages to the Decorator pattern (as anybody who's implemented an OLE object knows-OLE uses the Decorator as a poor substitute for true derivation). The overhead of relaying the messages is also a problem; the decorator's versions of the functions can't be inline because they're virtual overrides.
Since I also want a Proxy to behave as a User_interface, I've derived it from User_interface and provided virtual overrides of the User_interface virtual members that do nothing but call the equivalent functions in the contained User_interface object. This way a Proxy can effectively be used just as if it had derived from User_interface, even though the user of the forms package doesn't need to derive from Proxy.
A Proxy is manufactured by the Element using new because it will usually be destroyed by the Field, not the Element. The Proxy constructor (referring back up to Figure 1) is passed an object that implements a User_interface and either a pointer to the Element that created the Proxy or NULL. This last parameter solves a potentially nasty memory-management problem. The problem is that a Proxy could reference an actual Field in an object, or it could reference an object that the Element creates solely for display on the Form. For example, the Employee's Name attribute can be represented internally by a simple Text object, which implements a User_interface. If the Employee object just passes a pointer to its name field to the Proxy constructor, the user will effectively be typing a name directly into the associated Field of the Employee. The User_interface object can also be created by the Form when it manufactures the Proxy, however. For example, the Proxy used for entering the name might be a single string, but the name might be stored in the Employee object as first_name, middle_name, and last_name fields. The Employee can handle this situation by manufacturing a single Text object for data-entry purposes, passing it to the Proxy, and once the user finishes with it, parsing the Text into the three fields that actually represent the attribute internally.
There are two problems. First, how does the Form notify the Element that it's time to parse the Proxy? Second, how should the Proxy's destructor work? If you're dealing with a synthesized attribute that was being used for some sort of display purpose, no interaction with the Element is required when the Form shuts down, and it would be convenient if the Proxy just passed the User_interface object to delete. On the other hand, the Proxy couldn't very well do this if the User_interface object was actually a field in the Element whose address was passed to the Proxy constructor.
The problem is solve with the notify_me_rather_than_destroy_proxy argument to the Proxy constructor. If the argument is NULL, the Proxy deletes its User_interface object when the Proxy is released by the Field. If the argument isn't NULL, the Proxy calls the original Element's release_proxy function, passing a pointer to itself as an argument. Here's the sequence from the "release proxies" scenario in Figure 2. You either pass a release_proxies message to a Form or the Form goes out of scope or is passed to delete. (The Form's destructor calls release_proxies.) The Form then sends a release_proxy message to each of its Fields, which in turn send release messages to their proxies. This message does one of two things: if the Proxy constructor was originally passed a NULL notify_me_rather_than_destroy_proxy argument, the Proxy object first passes its User_interface pointer to delete, and then calls
delete this;
effectively freeing the Proxy too; if the argument wasn't NULL, Proxy::release passes a release_proxy(this) message to the Element that originally created the Proxy. The Element can then decide what to do with the Proxy and its contained User_interface object. In the case of the Employee object passing the address of one of its Fields to the original Proxy constructor, the Employee would just pass the Proxy to delete in response to receiving a release_proxy message. The Proxy destructor doesn't do anything, so that's all that needs doing.
Just in case the Element wants to derive a class from Proxy to do something complicated, I've made the Proxy's pointer to the User_interface object a protected, rather than private, member of the class. I usually think that protected-member data isn't a great idea, but in the current case, the Proxy gets the User_interface object from the same class that would be creating the derived class, so the maintenance problems are relatively minor.
Getting back to the Form-loading process in Figure 2, the Element passes the Proxy back to the Form when asked, and the Form attaches the Proxy to the associated Field. The other objects that use the same Form for I/O are then attached to the Form in the same way as the first such object, by calling put_yourself_on_this_form.
Now it's time either to display the Form or to use it for user input. Looking back at Figure 1, you'll notice that the Form itself implements a User_interface (it derives from User_interface), so you make the Form visible on the screen just as you would any other User_interface object-by passing it a display or interact message. The Window and Rect arguments to display and interact can specify a window on which to draw the Form (and the window size). You can also pass a NULL window pointer, in which case a pop-up window is created to hold the Form and the window's size is exactly large enough to hold all the Fields. The rect argument will be ignored in this case. Once the parent window is created, the Form relays the display or interact message all the way down to the User_interface elements associated with the Proxies. The User_interfaces then display themselves as subwindows to the Form's window. Since the Field's size is implicit, the Field's implementation of the display and interact handlers ignores the Rect argument.
There are two more loose ends. First, since a Form implements a User_interface, it can be encapsulated in a Proxy. That is, an Element on a Form can use another Form to represent itself. Second, it's never an error to ask a Form to display itself, even if Proxies are not attached to all the Fields. The Fields without proxies are just not displayed. This lets you do things like have two different objects display themselves at the same position on the same Form, provided that only one of them attaches itself to the Proxy at any given moment. The Element::remove_me_from_you message is provided to make this behavior possible-it works essentially like put_me_on_you except that it releases all Proxies.
ConclusionSo that's the high-level design. It achieves my primary purpose-decoupling the layout of a form from the classes that are displayed on the form (pretty elegantly if I do say so myself). You can radically change the layout of a Form and the objects displayed on it are completely unaffected. They don't even know that the Form has been changed. The flip side is also true; you can radically change the implementation of some class, and the Forms on which objects of that class are displayed are equally unaffected. The result is much easier maintenance.
In an upcoming issue, I'll continue this article by looking at the lower-level design details of the Forms package and the actual implementation as well, so stay tuned.
From the December 1996 issue of Microsoft Systems Journal.