Encapsulation is not information hiding

The principles of information hiding go beyond the Java language facility for encapsulation

1 2 3 Page 2
Page 2 of 3

For example, I previously noted that the distance calculation in class Position did not indicate units. To be useful, the reported distance of 6.09 from my house to the coffee shop clearly needs a unit of measure. I may know the direction to take, but I don't know whether to walk 6.09 meters, drive 6.09 miles, or fly 6.09 thousand kilometers.

Analysis of the modifications necessary to add units to class Position reveals another possible design flaw: the geometry to use has not been specified. You can calculate the distance and travel between any two points on the Earth's surface in several ways. Depending on the need for accuracy, distances within 50 kilometers could use the local Cartesian coordinates of plane geometry. These simple calculations may suffice for locations within the same area, but calculating the distance and traveling from San Francisco to Paris requires more difficult geometric calculations along a great circle path. And conducting earthquake studies by measuring distance and direction between precise locations along the San Andreas Fault in California may require the use of hyper accurate elliptical geometry, even for locations within 10 kilometers of each other.

Rather than directly include all of these domain choices in Position, I add reference variables of type Units and Geometry. The objects referenced by these variables handle the details concerning units and geometry. I don't show the actual code for the definition of interfaces Units and Geometry nor any concrete classes implementing these interfaces. Suffice it to say there are four implementations for units:

  • Kilometers
  • NauticalMiles
  • StatueMiles
  • Radians

and three implementations for geometry:

  • PlaneGeometry
  • SphericalGeometry
  • ElipticalGeometry

Appropriate Position getters and setters allow the dynamic change of either the units or the geometry used for distance and heading calculations.

Now for the most important and potentially damaging discovery: While creating the various geometry classes, it is determined that to delegate work to these objects, class Position should not maintain data items latitude and longitude, but should instead represent the internal location using spherical coordinate angles theta and phi. I won't digress into the reason. Regardless of why such a change is necessary, the change itself can prove painful or impossible if Position's clients directly access internal data. Before examining the ramifications of these changes, let's look at the updated version of Position that features the new reference variables:

public class Position
{
  public Position( double latitude, double longitude )
  {
    setLatitude( latitude );
    setLongitude( longitude );
    // Default to plane geometry and kilometers
    geometry = new PlaneGeometry();
    units = new Kilometers();
  }
  public void setLatitude( double latitude )
  {
    setPhi( Math.toRadians( latitude ) );
  }
  public void setLongitude( double longitude )
  {
    setTheta( Math.toRadians( longitude ) );
  }
  public void setPhi( double phi )
  {
    // Ensure -pi/2 <= phi <= pi/2 using modulo arithmetic.
    // Code not shown.
    this.phi = phi;
  }
  public void setTheta( double theta )
  {
    // Ensure -pi < theta <= pi using modulo arithmetic.
    // Code not shown.
    this.theta = theta;
  }
  // Setters for geometry and units not shown
  public double getLatitude()
  {
    return( Math.toDegrees( phi ) );
  }
  public double getLongitude()
  {
    return( Math.toDegrees( theta ) );
  }
  // Getters for geometry and units not shown
  public double distance( Position position )
  {
    // Calculate and return the distance from this object to the specified
    // position using the current geometry and units.
  }
  public double heading( Position position )
  {
    // Calculate and return the heading from this object to the specified
    // position using the current geometry and units.
  }
  private double phi;
  private double theta;
  private Geometry geometry;
  private Units units;
}

Notice that although Position no longer maintains internal data items latitude and longitude, the corresponding getter and setter methods remain. That stability isolates client objects from the internal change. By having getters and setters access the latitude and longitude attributes, clients needn't even know about the change. The only modifications necessary in the following code usage of the new Position class concern the addition of the units and geometry attributes:

Position myHouse = new Position( 36.538611, -121.797500 );
Position coffeeShop = new Position( 36.539722, -121.907222 );
double distance = myHouse.distance( coffeeShop );
double heading = myHouse.heading( coffeeShop );
System.out.println
  ( "Using " + myHouse.getGeometry() + " geometry, " +
    "from my house at (" +
    myHouse.getLatitude() + ", " + myHouse.getLongitude() +
    ") to the coffee shop at (" +
    coffeeShop.getLatitude() + ", " + coffeeShop.getLongitude() +
    ") is a distance of " + distance + " " + myHouse.getUnits() + 
    " at a heading of " + heading + " degrees."
  );
myHouse.setGeometry( Geometry.SPHERICAL );
myHouse.setUnits( Units.STATUTE_MILES );
distance = myHouse.distance( coffeeShop );
heading  = myHouse.heading(  coffeeShop );
System.out.println
  ( "Using " + myHouse.getGeometry() + " geometry, " +
    "from my house at (" +
    myHouse.getLatitude() + ", " + myHouse.getLongitude() +
    ") to the coffee shop at (" +
    coffeeShop.getLatitude() + ", " + coffeeShop.getLongitude() +
    ") is a distance of " + distance + " " + myHouse.getUnits() + 
    " at a heading of " + heading + " degrees."
  );

The above code generates the following output:

    ===================================================================
    Using Plane geometry, from my house at (36.538611, -121.7975)
    to the coffee shop at (36.539722, -121.907222) is a distance of
    9.79675938972254 Kilometers at a heading of 270.58013375337254
    degrees.
    Using Spherical geometry, from my house at (36.538611, -121.7975)
    to the coffee shop at (36.539722, -121.907222) is a distance of
    6.0873776351893385 Statute Miles at a heading of 270.7547022304523
    degrees.
    ===================================================================

The output includes both the geometry used for calculations and the distance units. The relatively small distance yields a negligible, though not imperceptible, difference between using plane and spherical geometries. The calculated headings differ slightly, and comparing the distances using the conversion factor of 1.61 kilometers per mile shows that the distances also vary slightly.

Cars and crows

Now that I know the coffee shop is 6.09 miles west of my house, I decide to drive there. In desperate need of a coffee fix, I jump in my car and prepare to head west. Unfortunately, that takes me right through the back of my garage, across the back lawn, and into a 20-meter deep ravine. So much for location-based services! I'd need more than coffee to get out of that mess.

Though I may think the idea great, the municipality in which I live hasn't yet approved a road directly linking my house with the coffee shop. So if I want java, I'll have to alter my planned route. Position's distance and direction information works fine for crows, but not quite so well for cars.

I decide to build a class that represents the actual driving route between my house and the coffee shop. The path consists of a series of segments that connect points along the way. So class Route maintains the Position objects necessary to define a route. A first-cut (and certainly not complete) design looks like:

public class Route
{
  public Route( int segments )
  {
    positions = new Position[ segments + 1 ];
  }
  public void setPosition( int index, Position position )
  {
    positions[ index ] = position;
  }
  public Position getPosition( int index )
  {
    return position[ index ];
  }
  public Position[] getPositions()
  {
    return positions;
  }
  public double distance( int segmentNumber )
  {
    // Calculate the distance of the specified segment number
  }
  public double distance()
  {
    // Iterate over the positions and accumulate the distances between each.
  }
  public double heading( int segmentNumber )
  {
    // Calculate the heading for the specified segment number
  }
  private Position[] positions;
}

The following outlines a usage of class Route:

  • Create a Route object with 10 segments
  • Create 11 Position objects
  • Place each Position in the appropriate spot along the route
  • Use Route to calculate the distance and direction of the first segment
  • Use Route to calculate the total distance

Exposing internal structure

Many problems abound in the simple first-cut design of class Route. From an encapsulation and information-hiding perspective, the accessor method getPositions(), which returns the array of Position objects used within class Route, proves potentially troublesome. Though class Route encapsulates the array, it does not protect the design decision that resulted in using an array. That is, by returning the internal array of Position's objects, Route has exposed the decision to use an array. This is particularly egregious when the design uses a method name like getPositionsArray(). If at a later time the design is changed to an ArrayList, Route's clients are exposed to the change. You could create and return a primitive array from the ArrayList, but the issue still remains. The choice of internal data structure used to manage the route should not affect external clients. That is a distinct difference between encapsulation and information hiding.

A second-cut design looks like this:

public class Route
{
  public Route()
  {
    positions = new ArrayList();
  }
  public void append( Position position )
  {
    positions.append( position );
  }
  public Position getPosition( int index )
  {
    return positions.get( index );
  }
  public double distance( int segmentNumber )
  {
    // Calculate the distance of the specified segment number
  }
  public double distance()
  {
    // Calculate the accumulated distance between each segment.
  }
  public double heading( int segmentNumber )
  {
    // Calculate the heading for the specified segment number
  }
  private List positions;
}

Removing getPositions() corrects that method's unwarranted exposure of internal details. Changing the data structure positions to a List and using append(Position) rather than setPosition(int,Position) further isolates the design decision regarding the internal collection being used.

Exposing internal implementation

More information-hiding problems lurk in this design. The method getPosition(int) exposes the actual internal data items maintained by Route. Any client can obtain a reference to any of the Position objects along the route and freely change the state of that Position object.

To appreciate the ramifications of this exposure, consider a possible class Route implementation of method distance(), which calculates the accumulated distance across all route segments. Suppose that the implementation iterates over the internal Position objects and uses each object to calculate the distance to the next Position. The calculation clearly requires using a single type of distance unit. To enforce this requirement, the method append(Position) in the code below uniformly sets the units of all input Position objects.

And now for the full vulnerability of the getPosition(int) method exposure: Though all the Position objects added to class Route might have their units properly set when added to the route, any client object could obtain a Position object and set the units to whatever the client chooses, without informing Route. The potential damage when calculating the result in method distance() proves particularly unnerving. distance() could unknowingly and easily add kilometers to statute miles.

The following version of class Route modifies the append(Position) method and corrects the getPosition(int) method by returning an equivalent Position object rather than the internal Position object:

public class Route
{
  public Route()
  {
    positions = new ArrayList();
  }
  public void append( Position position )
  {
    Position aPosition = new Position( position );
    aPosition.setUnits( myUnits );
    aPosition.setGeometry( myGeometry );
    positions.append( aPosition );
  }
  public Position getPosition( int index )
  {
    Position position = new Position( positions.get( index ) );
    return position;
  }
  public double distance( int segmentNumber )
  {
    // Calculate the distance of the specified segment number
  }
  public double distance()
  {
    // Calculate the accumulated distance between each segment.
  }
  public double heading( int segmentNumber )
  {
    // Calculate the heading for the specified segment number
  }
  private ArrayList positions;
  private Units myUnits;
  private Geometry myGeometry;
}

Class Route is not yet complete, but I have strengthened the design by properly hiding the design decisions made thus far.

Conclusion

Encapsulation is a language construct that facilitates the bundling of data with the methods operating on that data. Information hiding is a design principle that strives to shield client classes from the internal workings of a class. Encapsulation facilitates, but does not guarantee, information hiding. Smearing the two into one concept prevents a clear understanding of either.

The Java language manifestation of encapsulation doesn't even ensure basic object-oriented objects. The argument is not necessarily that it should, just that it doesn't. Java developers can blithely create bags of data in one class and place utility functions operating on that data in a separate class. So as a first rule:

1 2 3 Page 2
Page 2 of 3