Channels ▼
RSS

Design

Disentangling Concepts in Object-Oriented Systems

Source Code Accompanies This Article. Download It Now.


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:

  • getValueAtIndex(int)
  • getValueAtGeographicCoordinate(int, int)
  • getValueAtNorthPole()
  • getValueAtSouthPole()
  • getValueAtGreatCircleCoordinate(int, int)

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:

  • Value lookup. Getting a measurement from the globe.
  • Coordinate system. Specifying a coordinate system (absolute, geographic, or great circle).
  • Coordinate. Specifying the domain-specific coordinates themselves (single index, pair of latitude and longitude, or specific pole).

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.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video