Disentangling Concepts in Object-Oriented Systems

A class interface can be deceptively simple, which is why Stephen finds useful this object-oriented technique for "unpacking" the underlying concepts present in a given class.


July 30, 2008
URL:http://www.drdobbs.com/architecture-and-design/disentangling-concepts-in-object-oriente/209900542

Stephen is a software engineer who works primarily in UNIX-based C++ for a medical-devices company. He can be contacted at [email protected].


A class interface can be deceptively simple. Even though it may consist of only a few functions, those few functions may confound a number of distinct concepts. Consider, for instance, a class whose interface consists of only three functions: getStandingBlueBall(), getSittingRedDoor(), and getRunningYellowDog(). There are, in fact, three distinct concepts bound up in this interface: action (standing, sitting, or running), color (blue, red, or yellow), and object (ball, door, or dog). Although these three functions are named in accordance with their respective purposes, these single-token names do not clearly distinguish the three aforementioned concepts. Using object-oriented techniques, you can better "unpack" a class interface to distinguish the various concepts bound up in it. To illustrate these techniques, consider how best to implement the interface to a polar array class.

Overview of a Polar Array

A polar array is a representation of numeric measurements on a globe. Each measurement (altitude, for instance) is located at a unique intersection of a latitude and longitude. If a globe is subdivided into 8 latitudes and 8 longitudes, there are 8×8=64 unique measurements on the polar array, at each unique intersection of a latitude and longitude. In addition, there are measurements at the north and south poles. A set of arbitrary measurements on a globe with 8 latitudes and 8 longitudes might look something like Table 1.

You can also specify a location via a "great circle" coordinate. A great circle is a longitude that traverses the entire globe, rather than half of it. Imagine starting your traversal of the globe at the North Pole. Pick a longitude (such as #3). Follow the longitude south along the front of the globe until you reach the South Pole. Follow the longitude north along the backside of the globe. Technically, you are now traversing longitude #7 (in the geographic coordinate system). In the great circle coordinate system, you are still traversing the same longitude. Thus, the measurements on a globe with 8 latitudes and 8 longitudes are described by an 18-row-by-4-column matrix. A great circle representation of the globe just described would look like Table 2.

(North pole) 100
0 1 2 3 4 5 6 7
8 9 10 11 12 13 14 15
16 17 18 19 20 21 22 23
24 25 26 27 28 29 30 31
32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47
48 49 50 51 52 53 54 55
56 57 58 59 60 61 62 63
(South pole) 200

Table 1: Arbitrary measurements on a globe.

100 100 100 100
0 1 2 3
8 9 10 11
16 17 18 19
24 25 26 27
32 33 34 35
40 41 42 43
48 49 50 51
56 57 58 59
200 200 200 200
60 61 62 63
52 53 54 55
44 45 46 47
36 37 38 39
28 29 30 31
20 21 22 23
12 13 14 15
4 5 6 7

Table 2: A great circle representation of a globe.

To simplify the internal class implementation, the actual measurements on the globe will be represented by a vector of values, in which the geographic coordinates will be listed in row-major form first, followed by the values at the North and South Poles, respectively:


1 2 ... 63 64 100 200


That said, the requirements for a class representing a polar array are:

Now, consider a variety of approaches and refinements to the interface to such a class.

The Simplest Case

I start with a simple implementation of a PolarArray class that meets the requirements previously specified. I fixed the size of the globe to 8×8 (via const variables); see Listing One.

Not a bad start! This simple interface provides the following accessors:

Clearly, we have satisfied the requirements for this class, and could conceivably halt development on PolarArray at this point. But what kinds of refinement could we make to this class interface? Is there a more intuitive way it could be constructed?

===== PolarArray.h =====
static const int ROWS = 8;
static const int COLUMNS = 8;
class PolarArray
{
public:
  explicit PolarArray(double * data)
  { 
    memcpy(_data, data, ROWS * COLUMNS + 2); 
  }
  double getValueAtIndex(int index)
  { 
    return _data[index]; 
  }
  double getValueAtGeographicCoordinate(int latitude, int longitude)
  {
    return getValueAtIndex(latitude * ROWS + longitude);
  }
  double getValueAtNorthPole()
  {
    return getValueAtIndex(ROWS * COLUMNS);
  }
  double getValueAtSouthPole()
  {
    return getValueAtIndex(ROWS * COLUMNS + 1);
  }
  double getValueAtGreatCircleCoordinate(int latitude, int longitude)
  {
    // The north pole
    if(latitude == 0)
    {
      return getValueAtNorthPole();
    }
    // Any location on the "front" side of the globe
    else if(latitude > 0 && latitude <= ROWS)
    {
      return getValueAtGeographicCoordinate(latitude, longitude); 
    }
    // The south pole
    else if(latitude == ROWS + 1)
    {
      return getValueAtSouthPole();
    }
    // Any location on the "back" side of the globe
    else // latitude > ROWS + 1
    {
      int trueLatitude  = (ROWS * COLUMNS + 1) - latitude;
      int trueLongitude = longitude + COLUMNS / 2;
      return getValueAtGeographicCoordinate(trueLatitude, trueLongitude);
    }
  }
private:
  double _data[ROWS * COLUMNS + 2];
};
// Example client code:
PolarArray p(...);
p.getValueAtIndex(10);
p.getvalueAtGeographicCoordinate(2, 2);
p.getValueAtGreatCircleCoordinate(12, 2);
p.getValueAtNorthPole();
p.getValueAtSouthPole();
Listing One

In this first implementation, each accessor function actually combines three distinct concepts:

The function name specifies the first two concepts ("getValue" indicates we are requesting a value, "at<System>" denotes which coordinate system), and the function arguments specify the third (the coordinates within the system). Note that getNorthPole() and getSouthPole() actually specify the coordinate itself as part of the function name.

While it is not a sin to combine multiple concepts into a single token, it is conceptually clearer if the concepts are differentiated by more than simple capitalization. In the simple aforementioned example, the only delineation is the word "at," which separates the "getValue" portion of the function name from the coordinate system name.

Consider how you might use the various features of object-oriented design to better differentiate the two concepts in play. What if you refactor the index-translation portion of the class so that it is publicly available? You could force users to understand coordinate system and coordinates as distinct concepts from value lookup.

Listing Two (available online at www.ddj.com/code/) certainly differentiates the value lookup from the coordinate system. It is a bit unwieldy for the client, however, because any request for a PolarArray's value at a location must now come in the form of two function invocations instead of one. You have clearly differentiated coordinate system from value lookup, but at the expense of readability.

Let's back up a step and try a different approach. What if you simply factored out the great circle versus geographic system and North versus South Pole distinctions into class arguments? If you add a few enums to the code, the implementation might look like Listing Three (available online).

Now we're getting somewhere! You've refactored the distinction between great circle and geographic coordinate systems as a function argument to getValueAtCoordinate(...), as well as North and South Pole coordinates as a function argument to getValueAtPole(...).

Let's take this a step further. What if you used a type-based system to denote the coordinate system rather than a simple enum argument? You will create a Coordinate base class, from which you derive GeographicCoordinate and GreatCircleCoordinate. Similarly, you will create a Pole base class, from which we will derive NorthPole and SouthPole; see Listing Four (available online).

Now we're really making progress! Looking at the example client code, you see that the distinction between the three concepts—value lookup, coordinate system, and coordinate—is clearly differentiated. If you pursue this inheritance-based coordinate specification fully, the class hierarchy looks like Figure 1.

[Click image to view at full size]

Figure 1: Class hierarchy.

By using dynamic_cast to identify the coordinate system, you can finalize the PolarArray code. Pay particular attention to the example client code at the bottom of the listing; notice how it is both readable and clearly differentiates the three concepts into separate tokens; see Listing Five (available online).

Discussion

Even a simple class interface can mask a complex system of overlapping concepts. The original PolarArray interface consisted of (a constructor and) five accessors. Each accessor, by virtue of its naming convention, bound up the concepts of value lookup, coordinate system, and (for the polar accessors) coordinates in the function name itself. By recognizing those different concepts and finding alternate ways to functionally specify them, I improved the distinction between these concepts. I implemented a class hierarchy that lets the client specify coordinate system by type, rather than function name.

Besides improving the class's understandability, this improved its maintainability. If in the future you want to add new coordinate systems, you can do so simply by deriving from the existing coordinate hierarchy and amending the PolarArray getValue() code to deal with the new type. This would be much more awkward to do in the original system!

However, specifying the polar coordinate system is still bound up directly with specifying a particular pole (because the user must create a NorthPole or a SouthPole instance). We could easily use the enum Pole as a data member for class Pole, and eliminate the NorthPole and SouthPole classes. I chose not to do this because the client code would be less readable; see Listing Six (available online).

I think the second version is simply not very readable! Similarly, it's arguable that I should not have created an AbsoluteIndex class because it was easier just to have a getValue(int) for that purpose.

Lastly, I realize that the use of a dynamic_cast to perform a type-based switch is a little ugly and doesn't have great performance. We could just as well use function overloading (getValue(NorthPole&), getValue(SouthPole&), getValue(GeographicCoordinate&), and the like) to accomplish the same thing.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.