The Controller-Object Relationship

To design both automation objects and controllers well, you need to understand the optimal relationship between them. Knowing that, you can reduce redundant programming as much as possible, which is, after all, the point of any object-oriented relationship.

In general, the design process entails finding the answers to who, what, why, when, where, and how. Of course, who is the question we're trying to answer in looking at the object-controller relationship. The answer to how and where is the object's domain, what it does is expressed in its interfaces, and when and why those capabilities are used are the controller's domain. Therefore, the common medium between object and controller is what the object can actually do because that will generally determine where it happens and when and why. So as with any design project, you have to start with what you want to accomplish and then decide the best ways of making it happen.

As an example, let's suppose that what we want to accomplish is the capability to draw and view from any angle a square that is transcribed within the equatorial plane of a sphere of a certain radius. It follows that we want to be able to specify rotation and declination of the sphere, which simulates our moving around it, and we want to specify the radius of the sphere, which simulates moving closer to or farther away from it.

The most difficult part of this problem is the transformation of a few three-dimensional points on a sphere into two-dimensional points on a screen. This sounds like a great place to encapsulate that functionality in an object of some kind. We could easily create one function to do this and export the function from a DLL. The function would take a radius, declination, and rotation and pump out four POINT structures. Hardly a reason to create an object—but this design would require the client of the object to manage all the variables. Besides that inconvenience, only programs that knew about our DLL would be able to use this function.

The next best solution would be to make the transformation functionality part of an object in a DLL or an EXE (because we can do EXE-based objects in OLE) and create an interface for it. Depending on our target clients, we might make a custom interface, a dispinterface, or a dual interface. For the purposes of this discussion, let's assume we want a dispinterface. With a dispinterface, we can express functionality and information—that is, methods and properties. A square object, as we might call it, can thus manage all the information necessary to draw itself. This is great because it relieves a client of having to maintain a bunch of related variables separately—by making them part of an object, we enable the client to treat those variables as a related group. So we can give a square object the properties Radius, Declination, and Rotation, each of which could be of type double for a high degree of accuracy. The object could also have other properties through which the client could retrieve each point on the basis of the object's current state, or the object could have a method that would return the entire point set at once. Because all the points form a cohesive unit, allowing them to be read in bulk rather than individually is a better design. This eliminates the possibility of the client getting one point, changing a parameter, and then getting another point that is now completely unrelated to the first.

So we've taken care of the problem of computation. We have a design that allows us to specify parameters and to retrieve the calculated points, and the points and parameters together form one object that manages all that information as a single entity. We now have to decide how to turn those points into a visual rendering on the screen. We have two choices as to where on the screen this will happen: in a location of the client's choice (inside its own window) or in the location of the object's choice (inside another window that the object creates). We also have to decide who will actually generate the GDI calls to draw the necessary lines. Here are the possible choices:

It should be obvious that the second option is brain-dead because it breaks any notion of object orientation: the object would have certain expectations about the client using it. Bad bad bad. If the object allows the client to retrieve the set of calculated points in bulk, the first option is entirely the client's choice and doesn't affect the design of the relationship we're working on.

That leaves the third and fourth options, and deciding between the two is another major choice. Before we explore these, however, pause a moment to think of when drawing should actually take place in either situation. This should ultimately be the choice of the client—the controller—meaning that the object should provide a method named Draw that gives the client a way to tell the object when drawing should happen. This is a better design, I think, than overloading the semantics of the object's Radius, Declination, and Rotation properties to mean "change variable and repaint" instead of simply "change variable." Doing so allows us to change all three variables before drawing actually happens. There are, of course, other ways to accomplish this, for example overloading the properties and providing another Boolean property named RepaintOnChange. Either way works.

With a Draw method, however, we can specify where the drawing should take place, which allows control over options three and four. Such a method can take an optional hWnd argument (which can be shared across process boundaries); its absence means "draw into your own window with whatever scaling you choose," and its presence means "draw into the client area of the specified window." A second optional Boolean argument can control whether the drawing is scaled. (Otherwise, the Radius property wouldn't do anything if scaling were always on.) In addition, we'd probably want to specify how the drawing is centered in the window in which it's being drawn—which might require additional method arguments for a center point and an additional CenterPoint property.

Besides providing for the first option—the client draws the lines in its own space—it is best that the object can perform the drawing step as well because then it can perform optimizations if it wants, or provide extra features such as shading one side differently from the other so you'll see when the square flips over. Additional properties can let the client manipulate the colors used for the shading, or for drawing the background or the lines.

Are we done now? Possibly. We have an object that does what we set out to do: draw different views of a square inscribed in a sphere. But, of course, creeping featurism, featuritis, or whatever you want to call it, inevitably infects us, and we start to think of additional features. For example, we could make this object support animation; we could tell it to change its rotation and declination at a certain rate every so often and then tell it to start and stop the animation. We'd also want dynamic control over the radius so we could make this rotating and pulsating square appear somewhere on the screen.

We must stop here and ask whether this object is deviating from its original purpose. Adding animation support might sound cool, but adding features creates a bigger, bulkier, and slower object. This doesn't sound like what we want. As far as this simple object is concerned, animation is a feature that spans a single computation or a single rendering, and because of that, the object requires some knowledge of the big picture, some knowledge of why and when—but these are the realm of a controller. Therefore, we should stop the design of this object right here and now and keep it small. Let the controller determine how often and how much to change the parameters; let the object simply manage the current state and draw itself on request. This is the best separation of responsibilities.

You see, what we naturally and very easily slipped into was changing the original specification of the problem we wanted to solve. This is creeping featurism at its finest: stealthy guerrilla warfare. When we thought of adding animation, we had to either change the specification, which means going back to the very beginning of the design process, or state a new problem, write a new specification, and start the process over. In object-oriented design, the latter is the much better choice because we already have a component that knows how to draw the square we want. Our new specification can state the problem as one of creating an animation object that drives a calculation and rendering object that already exists. Trying to modify the original specification would hopelessly intermingle the act of controlling the animation and the acts of drawing and calculating. The problem statement is much easier when we rely on an object we've already built—that is, reusability—than when we decide to go back and redesign everything.

So the end design involves a simple calculation and rendering object (which itself could be broken into two pieces, mind you) that can be driven from any controller. We also have a value-added animation object that is itself a controller for the calculation and rendering object but that can also be driven by another controller. End users can choose which object to use depending on what they want to accomplish: if they want static views, the smaller object is best. If they want animation, the larger object is best. In each case, the user gets to choose the right object for the job, depending on the interfaces through which those objects expose their features. The larger the object, the more likely that it will have a higher-level set of interfaces that will be more appropriate for less skilled end users.

As a demonstration of the ideas discussed in this section, you can play around with the SphereSquare sample object (CHAP15\SQUARE), which implements the features described here, and use the DispTest/Visual Basic controller script (CHAP15\VBSQUARE) to drive that object. We won't look at SphereSquare in this chapter in any more detail—I merely include it for your own exploration.

The point of this exercise is to show that the design of objects and controllers is not haphazard and that objects that expose functionality and content —through OLE Automation, for example—can themselves be controllers of other objects. How you design a controller depends greatly on who you believe will use that tool and what sort of components will be most applicable for those users to integrate with that tool. So let's look at the sorts of features that controllers generally implement, and then we can look at the technical details that make it all work.