Fundamentals of Object-Oriented Design
CHAPTER 13

Organizing operations

Meilir Page-Jones

© Copyright 1998, Wayland Systems Inc. All rights reserved.

This chapter is about organizing the attributes and operations that comprise a class’s interface.

The chapter’s first section covers a very useful object-oriented design technique: the use of mix-in classes to add capabilities to a class without compromising the class’s cohesion. Mix-in classes also enhance the reusability in other applications of classes developed for one particular application. In this section, I use two examples, one from business and one from graphics, to illustrate the concept of mix-ins.

The second section discusses how you can organize operations into concentric rings to create “an interface within an interface” and to further strengthen encapsulation, object orientation’s central column. This chapter applies several design principles that we covered in previous chapters and uses samples of object-oriented code to illuminate new design concepts.

13.1 Mix-in classes

13.1.1 A business example

In order to show what mix-in classes are and how they can be useful, let me describe an object-oriented design problem from an accounts-receivable application at Grabbitt and Runne Enterprises, Inc. (The business could actually be any business with a simple invoice that comprises invoice items.) The aggregate class Invoice, and its constituent class InvoiceItem, appear in Figure 13.1.

Figure 13.1: An Invoice object is an aggregate of InvoiceItem objects

The requirement is this: Messers Grabbitt and Runne want to send each customer’s 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 doesn’t 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 Invoice’s 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 we’ve 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 Invoice’s 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 customer’s choice of communication medium) and eAddress (a customer’s e-mail address).

That’s all well and good, but the design of Figure 13.2 still limits the reusability of the fax operation, which we’ve 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, we’ve “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; it’s a general class, capable of faxing or e-mailing any document. So now let’s 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 — let’s 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:

  1. We invoke sendableInv.createDoct. This operation creates a standard text document (comprising pages and lines) that can be faxed, e-mailed or printed out. However, the attribute that represents this text document (and the variable that implements it) is defined on SendableDocument, not on SendableInvoice.1 The operations that build the document (clearDoct, appendTextToDoct) are again defined on SendableDocument. The pseudocode for SendableInvoice.createDoct goes something like this:2
    public operation createDoct
    begin
      self.clearDoct;
                      // set text area to empty - SD
      get the invoice header;
                      // - I
      convert it to the text form headerText;
      self.appendTextToDoct (headerText)
                      // - SD
       
      repeat
          get the next invoice line;
                      // - I
      until no more invoice lines
          convert it to the text form lineText;
          self.appendTextToDoct (lineText)
                      // - SD
      endrepeat
    end createDoct;
  2. Now that we’ve filled doct with the invoice information, we need to send it, using the operation SendableInvoice.sendToCustomer, whose pseudocode looks something like this:
    public operation sendToCustomer
    begin
      cust:Customer := self. responsibleCust;
                      // - I
       
      case cust. prefCommMedium
           "MAIL":    self.mailDoct (cust.name, cust.address);
                      // - SD
           "E-MAIL":  self.eMailDoct (cust.name, cust.eAddress);
                      // - SD
           "FAX":     self.faxDoct (cust.name, cust.faxNumber);
                      // - SD
      else            // error! ;
      endcase;
       
    end sendToCustomer;

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 doesn’t 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 don’t instantiate objects from mix-in classes; that’s why SendableDocument is marked as {abstract}. Instead, other classes (like SendableInvoice in this example) inherit a mix-in class’s 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.

13.1.2 A graphics example

In case you hate business examples of mix-in classes, I’ve included this next example just for you.

Figure 13.4 depicts a rectangle that’s 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. It’s 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 we’ll 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 rectangle’s 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. That’s 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, let’s look at the code of the three classes, Rectangle, ShapeInFrame and RectangleInFrame.
class Rectangle;
   
  var center: Point;
  var height, width: Length;
  var orient: Angle;
      ...
  public read center, height, width, orient;
      ...
  public operation v1 ( ): Point;
                      // an attribute that returns a vertex
  begin
      var vertex: Point := Point.New;
      vertex.x := center.x +
          (height * sin (orient) + width * cos (orient)) / 2;
      vertex.y := center.y +
          (height * cos (orient) + width * sin (orient)) / 2;
      return (vertex);
  end v1;
      ...
   
  public operation top ( ): Length;
                      // an attribute that returns the top
  begin
      return (max (self.v1.y, self.v2.y, self.v3.y, self.v4.y));
  end top;
      ...
   
  public operation move (moveIncr: 2DVector);
                      // an operation that moves the rectangle
  begin
      center.x plus moveIncr.x;
                      // plus increments the lefthand variable
      center.y plus moveIncr.y;
  end move;
      ...
endclass Rectangle;

The internal representation of Rectangle objects rests on four variables:

  • center records the center-point of a rectangle.
  • height and width are self-explanatory.
  • orientation records how much a rectangle is tilted (counter-clockwise from the horizontal).

These are the core representational variables of the class; they’re 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 that’s to enclose the shape, and a Boolean switch recording whether the frame is active (constraining the rectangle) or not.
class ShapeInFrame;
   
      var enclFrame: Frame;
                      // assume for simplicity frame is always horizontal
      var isActive: Boolean
      ...
      public read, update enclFrame;
                      // the frame doing the enclosing can be changed
      ...
endclass ShapeInFrame;

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 isn’t usually an issue with true mix-in classes. This is because a mix-in class, say M, doesn’t have instantiated objects of its own. Therefore, this question doesn’t 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.

13.2 Rings of operations

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. Here’s the code for one of its operations, moveWithinFrame:
class RectangleInFrame;
  inherits from ShapeInFrame, Rectangle;
      ...
  public operation moveWithinFrame (moveIncr: 2DVector);
  begin
      var allwdMoveIncr: 2DVector := 2DVector.New;
                      // will hold the actual allowed move
      if self.enclFrame.isActive
                      // enclFrame is inherited from ShapeInFrame
      then
          if moveIncr.x > 0
                      // to the right in this convention
          then allwdMoveIncr.x :=
              min (moveIncr.x, self.enclFrame.right - self.right);
          else allwdMoveIncr.x :=
              max (moveIncr.x, self.enclFrame.left - self.left);
          endif;
   
          if moveIncr.y > 0
                      // upward in this convention
          then allwdMoveIncr.y :=
              min (moveIncr.y, self.enclFrame.top - self.top);
          else allwdMoveIncr.y :=
              max (moveIncr.y, self.enclFrame.bottom - self.bottom);
          endif;
      else allwdMoveIncr := moveIncr;
                      // there’s no active frame at present
      endif;
   
      self.move (allwdMoveIncr);
                      // move is the operation inherited from Rectangle
  end moveWithinFrame;
      ...
endclass RectangleInFrame;

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 doesn’t go outside its enclosing frame when it’s 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 didn’t 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, it’s 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:

  1. It may avoid duplicating code in the two operations.

  2. It limits the knowledge of some variables’ representations to fewer operations.

  3. If one of the operations is in a subclass, then sending a message, rather than directly manipulating the superclass’s variables, decreases the connascence between the two classes. (For example, the subclass has to know fewer of the superclass’s variable names.)
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 doesn’t mean public and inner doesn’t mean private. operationD provides an example of this fact, because operationD is publicly accessible although it’s 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 designer’s 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 didn’t 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.
class Rectangle;      // the new, improved-speed version!
 
  var center: Point;
  var height, width: Length;
  var orient: Angle;  // these are the core representational variables
  var v1, v2, v3, v4: Point;
                      // the four vertices of the rectangle,
                      // held redundantly for efficiency
      ...
  public read center, height, width, top, bottom,
      left, right, v1, v2, v3, v4, orient;
      ...
  public operation move (moveIncr: 2DVector);
  begin
      center.x plus moveIncr.x; center.y plus moveIncr.y;
      v1.x plus moveIncr.x; v1.y plus moveIncr.y;
                      // move the vertices with the center
      v2.x plus moveIncr.x; v2.y plus moveIncr.y;
      v3.x plus moveIncr.x; v3.y plus moveIncr.y;
      v4.x plus moveIncr.x; v4.y plus moveIncr.y;
  end move;
      ...
endclass Rectangle;

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 Rectangle’s operations in rings.

Summary

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 don’t 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 class’s 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 class’s 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.


1. You can use the read-only attribute doct to access this text document.

2. Key: The comment — SD means “via inheritance from SendableDocument”; the comment — I means “via inheritance from Invoice”.