Fundamentals of Object-Oriented Design
|
| Figure 13.1: | An Invoice object is an aggregate of InvoiceItem objects |
The requirement is this: Messers Grabbitt and Runne want to send each customers invoice to that customer in the manner that the customer prefers. Some customers like their invoices faxed and others e-mailed, while the more nostalgic like to see their invoices coming down the road in a bouncy little mail van.
At first, this requirement doesnt seem to be a design problem at all. We might add, for example, a fax operation to Invoice that allows an invoice to fax itself to the appropriate customer. But that design would reduce the cohesion of Invoice to mixed-domain cohesion (because it would probably encumber Invoice with at least some details of faxing protocol, which belongs in the architectural domain). Practically speaking, the design would limit Invoices reusability and, worse, possibly limit the reusability of the fax operation.
| Figure 13.2: | The class SendableInvoice preserves the cohesion of Invoice |
Confronted by the problems of the previous paragraph, we could create the design of Figure 13.2. Here weve factored out operations such as eMailInvoice and faxInvoice into their own class, SendableInvoice, which inherits from the original Invoice. So now Invoice can revert to its pristine form, with ideal cohesion. To create a new invoice, we instantiate a SendableInvoice object, rather than an Invoice object. The operation faxInvoice will know all how to run the fax-modem and will also (via inheritance) have all of Invoices information available for it to fax.
Incidentally, I should say a word or two about the class Customer. Customer is related via a Responsibility association to the class Invoice. (This association records which customer is responsible for which invoice(s).) The attributes defined on Customer are, for example: prefCommMedium (which records a customers choice of communication medium) and eAddress (a customers e-mail address).
Thats all well and good, but the design of Figure 13.2 still limits the reusability of the fax operation, which weve even named faxInvoice. What a shame to have all that fax-modem expertise tucked away and unavailable to us when we want to fax things other than invoices: acknowledgments, greetings, threats and so on.
This is where a mix-in class comes to the rescue, as Figure 13.3 shows.
| Figure 13.3: | SendableInvoice now inherits multiply from Invoice and the mix-in class SendableDocument |
Although Figure 13.3 is only subtly different from Figure 13.2, that subtle difference is important. In this design, weve factored up a mix-in class, SendableDocument, which has all the smarts to carry out faxing and e-mailing. Importantly, however, SendableDocument has no knowledge about invoices; its a general class, capable of faxing or e-mailing any document. So now lets follow, blow by blow, how the whole design works.
When we want to create an object to represent a new invoice, we invoke SendableInvoice.New. We initialize this object lets refer to it as sendableInv by giving it its invoice items (and any header information) and by linking it up with the responsible Customer object. All of the above happens via inheritance using the machinery of Invoice, since sendableInv belongs to a subclass of Invoice.
sendableInv also has available to it (via inheritance again) the communication capabilities of SendableDocument. So when we want to send the invoice represented by sendableInv, we do it in two steps:
|
|
|
|
SendableDocument is an example of a mix-in class. A mix-in class typically supports an abstraction or mechanism that could be useful in several other classes, but which doesnt belong in any particular one of those classes. Parceling away distinct abstractions and mechanisms into mix-in classes enhances the reusability of those abstractions and mechanisms.
Normally, you dont instantiate objects from mix-in classes; thats why SendableDocument is marked as {abstract}. Instead, other classes (like SendableInvoice in this example) inherit a mix-in classs capabilities. SendableInvoice also inherits from the class Invoice, from which an object of class SendableInvoice gets specific information with which to carry out its business capabilities. So, since a mix-in class needs to inherit from (at least) two superclasses, mix-in classes work best when your language supports multiple inheritance.
In case you hate business examples of mix-in classes, Ive included this next example just for you.
Figure 13.4 depicts a rectangle thats free to move and rotate so long as it remains within its enclosing frame. (I indicate the limits of its current extent on the screen with lines marked top, bottom, left, right.)
| Figure 13.4: | A rectangle within a frame |
Figure 13.5 shows part of the design of RectangleInFrame, which inherits from the two classes Rectangle and ShapeInFrame.
| Figure 13.5: | The inheritance hierarchy for RectangleInFrame |
The class Rectangle is the ordinary class that supports the manipulation (such as the moving, rotating or stretching) of rectangles. Its a class you may purchase as part of a class library. ShapeInFrame is a less conventional class, which records the relationship between a rectangle and its enclosing frame. ShapeInFrame is another example of a mix-in class.
In the rectangle example shown in Figure 13.4, the mix-in class ShapeInFrame offers a design solution to a problem presented by the rectangle and frame: our needing to record the frame in which a given rectangle is enclosed. If we modify the class Rectangle by giving it a variable to hold this information, then well reduce the reusability of Rectangle in other applications. (To use the terms that I presented in Chapter 9, we would encumber Rectangle with Frame and give it mixed-role cohesion.) Anyway, the vendor of Rectangle might not give us the source code to modify!
A more reasonable place to record a rectangles relationship with its frame is the class RectangleInFrame, which is all about rectangles and frames. That would be fine, except that EllipseInFrame and TriangleInFrame also need access to the same kind of machinery. Thats why the mix-in class ShapeInFrame is so useful. ShapeInFrame can be mixed in with Rectangle to yield RectangleInFrame. In another part of the system, it could be mixed in with Ellipse to form EllipseInFrame, and so on.
Now, lets look at the code of the three classes, Rectangle, ShapeInFrame and RectangleInFrame.
|
|
The internal representation of Rectangle objects rests on four variables:
These are the core representational variables of the class; theyre the pillars that internally support the external abstraction of a rectangle. (Since the information that these variables provide is also part of the abstraction that Rectangle supports, the variables are also public attributes of Rectangle.)
The class ShapeInFrame, like many mix-ins, is simple. It contains little beyond a pointer to the frame thats to enclose the shape, and a Boolean switch recording whether the frame is active (constraining the rectangle) or not.
|
|
Notice that RectangleInFrame in some sense conforms to ShapeInFrame. That is, a rectangle in a frame is a shape in a frame. However, type conformance isnt usually an issue with true mix-in classes. This is because a mix-in class, say M, doesnt have instantiated objects of its own. Therefore, this question doesnt arise: Can I provide an object of class SuchAndSuch in the context that an object of class M is expected?
Although a mix-in class rarely has objects of its own, it does capture some aspect of the world that offers a particular capability. Through multiple inheritance, a designer may then combine these aspects from mix-in classes into one class, from which objects will be instantiated.
In this section, we investigate the structure of operations within a single class, and see how to make the most of encapsulation by designing operations in inner and outer rings. As an example, I again pick RectangleInFrame (as shown in Figure 13.5), the class that both creates rectangles within frames and defines the behavior that keeps a rectangle within its enclosing frame. Heres the code for one of its operations, moveWithinFrame:
|
|
moveWithinFrame is one of several operations that this class could contain. (Another would be rotateWithinFrame.) The chief job of moveWithinFrame is to make sure that the rectangle doesnt go outside its enclosing frame when its moved in some direction. To do this job, the operation computes the allowed move for the rectangle, which is the smaller of the requested move and the distance to the frame border (in each of the x and y dimensions), and then sends a message to self. This message invokes the operation move, as inherited from the class Rectangle.
Notice how the designer uses a message to invoke move, rather than directly tweaking the value of the variable center. But why didnt the designer just tweak center directly by coding, for example,
center.x plus allwdMoveIncr.x;
center.y plus allwdMoveIncr.y;
instead of invoking move? After all, that would do exactly the same thing and would probably be more efficient. And, although the variable center is declared within Rectangle, its also available to RectangleInFrame, which is a subclass of Rectangle.
The answer is encapsulation or, more specifically, implementation hiding.
Invoking another operation (typically a get operation) of the same object, rather than simply grabbing a variable directly, is beneficial for three reasons:
| Figure 13.6: | Inner and outer rings of operations |
Figure 13.6 shows how the operation structure might appear when you use this approach of operations invoking operations within the same object. Operations appear in two rings. The outer ring comprises operations that use other operations of the same object. operationB and operationC belong in the outer ring, because they send messages that invoke operationD, operationE and operationF. Notice, however, that the methods of many outer operations access at least one variable directly, as does operationA.
Inner rings comprise operations used by other operations methods. For example, operationF lives in the inner ring and is invoked by the method of operationC through the message self.operationF (..., out ...) to read and update variables.
Other objects may use operations both in the outer ring and the inner ring; in other words, outer doesnt mean public and inner doesnt mean private. operationD provides an example of this fact, because operationD is publicly accessible although its in the inner ring and is used by operationA and operationB.
The class Rectangle offers an example of operations organized in rings. The get operation top invokes the get operations v1, v2, v3 and v4 instead of doing all its calculations directly from the core variables (center, height, width and orientation). The designers reasons were both to save code and to localize the knowledge of representation of variables. (These are the first two of the three reasons mentioned above.)
We now also have a fuller answer to the earlier question: Why didnt the designer of the operation moveWithinFrame (in the class RectangleInFrame) update the variable center directly? The reason is the danger of having operations of the subclass RectangleInFrame messing around with variables of the superclass Rectangle. (This is the third reason mentioned above.)
Consider what would have happened if the designer of the operation moveWithinFrame did directly manipulate the center of the rectangle, and if our class vendor had then sent us a new version of the class Rectangle, a version that stores (rather than computes) the four vertices of the rectangle, as shown in the code below.
|
|
Notice that the operation move is now more complicated, because it must maintain the redundant information held by v1, v2, v3 and v4. (The information is redundant because the four vertices can be computed from the core representational variables, center, height, width and orientation.) If the system had simply been recompiled and relinked, then the operation moveWithinFrame would exhibit a defect: It would divorce the corners of a rectangle from its center.
The fix would be to rewrite the corner-moving code. Better yet, we should reinstate the first design by invoking the operation move, defined on Rectangle. In other words, we should arrange Rectangles operations in rings.
This chapter covered the placement and design of operations. The first design approach that we explored addressed using mix-in classes to free other classes from abstractions that dont belong in their interfaces. We saw that a mix-in class is a relatively simple component that is normally abstract. Instead, a designer uses the abstraction or mechanism that the mix-in class embodies, via inheritance, to create a new combination class. This new class, with its multiple avenues of inheritance may then possess both general (say, business-domain) abstractions and more special (say, architecture-domain) abstractions.
By relocating restrictive abstractions from a business class into a mix-in class, a designer enhances the business classs cohesion, encumbrance and reusability. Since the same mix-in class may be useful in several design situations, superfluous code can be eliminated from applications and class libraries. The reusability of the mix-in classs capabilities is also improved.
The second design approach in this chapter addressed the organization of operations into rings to create layers of encapsulation within a single class. This approach uses information and implementation hiding by inner operations to shield outer operations from unnecessary knowledge the way variables are designed. Then, if the designer should change, say, the names, classes or other details of certain variables, fewer operations will need to be rewritten.
2. Key: The comment SD means via inheritance from SendableDocument; the comment I means via inheritance from Invoice.
![]()