Accessors: There and Back Again

In the June 1993 issue of The Smalltalk Report, Kent Beck tackled the touchy topic of accessor methods. Takeaways include:

  • accessors violate encapsulation;
  • methods should be services;
  • public accessors are acceptable when the object cannot perform the service itself; and
  • using accessors might indicate missing behaviour.

On September 5, 2003, Allen Holub rekindled the controversy by publishing a provocative article in JavaWorld. Pertinent points include:

  • accessor methods expose implementation details;
  • objects should not expose implementation details;
  • tell the object that has the information to do the work;
  • objects implement activities necessary to achieve use case scenario outcomes; and
  • design objects with well-defined responsibilities in conversational terms.

These concepts are succinctly echoed by Alec Sharp when he wrote, “Procedural code gets information then makes decisions. Object-oriented code tells objects to do things.”

This article applies these concepts in code. In my experience, the industry is wading in a cesspool of accessor methods — most Java IDEs and advanced editors generate them automatically. This exacerbates the problem of inflexible and brittle software. A major goal of object-oriented development is not to move data around the system; rather, object-oriented programming entails encapsulating behaviour by bundling data with services that modify (or query) the object’s state.

To ease into this object-oriented mindset, consider a common task: process a file based on its type. A procedural approach (using Java) would employ a File class as follows:

public class File {
  private String filename = "";

  public File( String filename ) {
    this.filename = filename;
  }

  /**
   * Get the extension for this file.
   * @return All characters after the last period.
   */
  @NotNull
  public String getExtension() {
    int index = this.filename.lastIndexOf( '.' );
    return index > 1 ? this.filename.substring( index + 1 ) : "";
  }

  public static void main( String args[] ) {
    File file = new File( args[0] );
    System.out.println( "Extension: " + file.getExtension() );
  }
}

Ignoring obvious tactical errors, exposing the extension to other objects breaks encapsulation. The implementation offers few strategic benefits over using the extension variable directly (i.e., line 20 above compared with line 22 below):

public class File {
  public String filename = "";

  /**
   * The extension for this file (all characters
   * after the last period in the filename variable).
   */
  public String extension = "";

  public File( String filename ) {
    int index = filename.lastIndexOf( '.' );

    if( index > 1 ) {
      this.extension = filename.substring( index + 1 );
    }

    this.filename = filename;
  }

  public static void main( String args[] ) {
    File file = new File( args[0] );
    System.out.println( "Extension: " + file.extension );
  }
}

These examples lead to seemingly reasonable program logic unwittingly constrained by the File‘s public interface:

  1. Acquire a File object.
  2. Get the file name extension from the file instance.
  3. Process the file based on its type.

A FileProcessor that uses such an interface might resemble:

public class FileProcessor {
  /**
   * Converts any type of file to a PDF.
   * @param file - File to convert.
   */
  public void process( File file ) {
    String extension = file.getExtension().toLowerCase();

    switch( extension ) {
      case "jpg":
      case "gif":
        processImage( file );
        break;

      case "csv":
        processSpreadsheet( file );
        break;
    }
  }
}

Any object that uses the File class is now so tightly coupled to it that not even a stable library can decouple it. By thinking of objects as having a conversation, a far more malleable implementation can be coded:

public class FileProcessor {
  /**
   * Converts any type of file to a PDF.
   * @param file - File to convert.
   */
  public void process( File file ) {
    String extension = file.getExtension().toLowerCase();

    if( file.isType( File.IMAGE ) ) {
      processImage( file );
    }

    if( file.isType( File.DOCUMENT ) ) {
      processDocument( file );
    }
  }
}

This improves upon the procedural approach: the isType method can now determine the file type based on contents rather than name. Note that file.isType( File.IMAGE ) and file.isImage() encapsulate the same knowledge: they are strategically equivalent. Clients to such a File class need never change when the underlying implementation for file type detection is replaced anew. In contrast, invoking accessors in a procedural way leads to much more effort when trying to revamp the code.

With the introduction to the object-oriented mindset out of the way, a more difficult concept approaches. Accessors can make software fragile due to uncontrolled composite object state changes. As an example, consider how to set the fuel system (e.g., diesel, petrol, hydrogen, electric) for a car’s engine:

Car car = CarFactory.newCar( "electric" );
Engine engine = car.getEngine();
engine.setFuelSystem( new DieselFuelTank() );

An electric car should not have a diesel fuel tank for its engine. These type of accessors allow any object in the system to change the state the Car‘s instance variables without the Car being notified of said changes. Effectively, a car cannot prevent invalid states for the objects that comprise it: this is the exact opposite of encapsulation.

A quick solution is this: deny state changes to composite member variables by issuing copies of the composite objects requested through accessor methods. For example:

public class Car {
  /** Composite object: the engine */
  private Engine engine = new Engine();

  /**
   * Creates a clone of the car's engine.
   * @return The car's engine instance.
   */
  @NotNull
  public Engine getEngine() {
    return this.engine.clone();
  }
}

public class ElectricCar extends Car {
  @Override
  public void setEngine( Engine engine ) {
    if( engine.isElectric() ) ) {
      this.engine = engine;
    }
    else {
      throw new InvalidEngineException( engine );
    }
  }
}

Code can no longer bypass setting an incompatible energy storage device to Car objects; objects are forced to use the Car‘s interface:

Car car = CarFactory.newCar( "electric" );
Engine engine = car.getEngine();
engine.setFuelSystem( new DieselFuelTank() );
car.setEngine( engine );

Line 3 no longer affects the car object. Line 4 attempts to update the car with the modified engine, but will cause an exception.

Prior to hopping the Atlantic ocean, Charles Lindbergh took an active interest in his airplane’s design. He evaluated everything; he opted for more fuel over a parachute, took no radio, trimmed the edges off maps, removed extra notebook pages, and even declined night-flying equipment to reduce weight.

Accessors, when used by rote rather than by thought, add weight to an application. For a web-based business where features, flexibility, and staying competitive matters, any extra weight can stop the shop from soaring. Like Lindbergh, business owners should take a keen interest in all aspects of their company, including the usually unseen source of their software.

Discuss this on Hacker News.