More on getters and setters

Build user interfaces without getters and setters

It's a 25-year-old principle of object-oriented (OO) design that you shouldn't expose an object's implementation to any other classes in the program. The program is unnecessarily difficult to maintain when you expose implementation, primarily because changing an object that exposes its implementation mandates changes to all the classes that use the object.

Unfortunately, the getter/setter idiom that many programmers think of as object oriented violates this fundamental OO principle in spades. Consider the example of a Money class that has a getValue() method on it that returns the "value" in dollars. You will have code like the following all over your program:

double orderTotal;
Money amount = ...;
//...
orderTotal += amount.getValue(); // orderTotal must be in dollars

The problem with this approach is that the foregoing code makes a big assumption about how the Money class is implemented (that the "value" is stored in a double). Code that makes implementation assumptions breaks when the implementation changes. If, for example, you need to internationalize your application to support currencies other than dollars, then getValue() returns nothing meaningful. You could add a getCurrency(), but that would make all the code surrounding the getValue() call much more complicated, especially if you persist in using the getter/setter strategy to get the information you need to do the work. A typical (flawed) implementation might look like this:

Money amount = ...;
//...
value       =  amount.getValue();
currency    =  amount.getCurrency();
conversion  =  CurrencyTable.getConversionFactor( currency, USDOLLARS );
total       += value * conversion;
//...

This change is too complicated to be handled by automated refactoring. Moreover, you would have to make these sorts of changes everywhere in your code.

The business-logic-level solution to this problem is to do the work in the object that has the information required to do the work. Instead of extracting the "value" to perform some external operation on it, you should have the Money class do all the money-related operations, including currency conversion. A properly structured object would handle the total like this:

Money total  = ...;
Money amount = ...;
total.increaseBy( amount );

The add() method would figure out the currency of the operand, do any necessary currency conversion (which is, properly, an operation on money), and update the total. If you used this object-that-has-the-information-does-the-work strategy to begin with, the notion of currency could be added to the Money class without any changes required in the code that uses Money objects. That is, the work of refactoring a dollars-only to an international implementation would be concentrated in a single place: the Money class.

The problem

Most programmers have no difficulty grasping this concept at the business-logic level (though it can take some effort to consistently think that way). Problems start to emerge, however, when the user interface (UI) enters the picture. The problem is not that you can't apply techniques like the one I just described to build a UI, but that many programmers are locked into a getter/setter mentality when it comes to user interfaces. I blame this problem on fundamentally procedural code-construction tools like Visual Basic and its clones (including the Java UI builders) that force you into this procedural, getter/setter way of thinking.

(Digression: Some of you will balk at the previous statement and scream that VB is based on the hallowed Model-View-Controller (MVC) architecture, so is sacrosanct. Bear in mind that MVC was developed almost 30 years ago. In the early 1970s, the largest supercomputer was on par with today's desktops. Most machines (such as the DEC PDP-11) were 16-bit computers, with 64 KB of memory, and clock speeds measured in tens of megahertz. Your user interface was probably a stack of punched cards. If you were lucky enough to have a video terminal, then you may have been using an ASCII-based console input/output (I/O) system. We've learned a lot in the past 30 years. Even Java Swing had to replace MVC with a similar "separable-model" architecture, primarily because pure MVC doesn't sufficiently isolate the UI and domain-model layers.)

So, let's define the problem in a nutshell:

If an object may not expose implementation information (through get/set methods or by any other means), then it stands to reason that an object must somehow create its own user interface. That is, if the way that an object's attributes are represented is hidden from the rest of the program, then you can't extract those attributes in order to build a UI.

Note, by the way, that you're not hiding the fact that an attribute exists. (I'm defining attribute, here, as an essential characteristic of the object.) You know that an Employee must have a salary or wage attribute, otherwise it wouldn't be an Employee. (It would be a Person, a Volunteer, a Vagrant, or something else that doesn't have a salary.) What you don't know—or want to know—is how that salary is represented inside the object. It could be a double, a String, a scaled long, or binary-coded decimal. It might be a "synthetic" or "derived" attribute, which is computed at runtime (from a pay grade or job title, for example, or by fetching the value from a database). Though a get method can indeed hide some of this implementation detail, as we saw with the Money example, it can't hide enough.

So how does an object produce its own UI and remain maintainable? Only the most simplistic objects can support something like a displayYourself() method. Realistic objects must:

  • Display themselves in different formats (XML, SQL, comma-separated values, etc.).
  • Display different views of themselves (one view might display all the attributes; another might display only a subset of the attributes; and a third might present the attributes in a different way).
  • Display themselves in different environments (client side (JComponent) and served-to-client (HTML), for example) and handle both input and output in both environments.

Some of the readers of my previous getter/setter article leapt to the conclusion that I was advocating that you add methods to the object to cover all these possibilities, but that "solution" is obviously nonsensical. Not only is the resulting heavyweight object much too complicated, you'll have to constantly modify it to handle new UI requirements. Practically, an object just can't build all possible user interfaces for itself, if for no other reason than many of those UIs weren't even conceived when the class was created.

Build a solution

This problem's solution is to separate the UI code from the core business object by putting it into a separate class of objects. That is, you should split off some functionality that could be in the object into a separate object entirely.

This bifurcation of an object's methods appears in several design patterns. You're most likely familiar with Strategy, which is used with the various java.awt.Container classes to do layout. You could solve the layout problem with a derivation solution: FlowLayoutPanel, GridLayoutPanel, BorderLayoutPanel, etc., but that mandates too many classes and a lot of duplicated code in those classes. A single heavyweight-class solution (adding methods to Container like layOutAsGrid(), layOutAsFlow(), etc.) is also impractical because you can't modify the source code for the Container simply because you need an unsupported layout. In the Strategy pattern, you create a Strategy interface (LayoutManager) implemented by several Concrete Strategy classes (FlowLayout, GridLayout, etc.). You then tell a Context object (a Container) how to do something by passing it a Strategy object. (You pass a Container a LayoutManager that defines a layout strategy.)

The Builder pattern is similar to Strategy. The main difference is that the Builder class implements a strategy for constructing something (like a JComponent or XML stream that represents an object's state). Builder objects typically build their products using a multistage process as well. That is, calls to various methods of the Builder are required to complete the construction process, and the Builder typically doesn't know the order in which the calls will be made or the number of times one of its methods will be called. Builder's most important characteristic is that the business object (called the Context) doesn't know exactly what the Builder object is building. The pattern isolates the business object from its representation.

The best way to see how a simple builder works is to look at one. First let's look at the Context, the business object that needs to expose a user interface. Listing 1 shows a simplistic Employee class. The Employee has name, id, and salary attributes. (Stubs for these classes are at the bottom of the listing, but these stubs are just placeholders for the real thing. You can—I hope—easily imagine how these classes would work.)

This particular Context uses what I think of as a bidirectional builder. The classic Gang of Four Builder goes in one direction (output), but I've also added a Builder that an Employee object can use to initialize itself. Two Builder interfaces are required. The Employee.Exporter interface (Listing 1, line 8) handles the output direction. It defines an interface to a Builder object that constructs the representation of the current object. The Employee delegates the actual UI construction to the Builder in the export() method (on line 31). The Builder is not passed the actual fields, but instead uses Strings to pass a representation of those fields.

Listing 1. Employee: The Builder Context

   1  import java.util.Locale;
   2
   3  public class Employee
   4  {   private Name        name;
   5      private EmployeeId  id;
   6      private Money       salary;
   7
   8      public interface Exporter
   9      {   void addName    ( String name   );
  10          void addID      ( String id     );
  11          void addSalary  ( String salary );
  12      }
  13
  14      public interface Importer
  15      {   String provideName();
  16          String provideID();
  17          String provideSalary();
  18          void   open();
  19          void   close();
  20      }
  21
  22      public Employee( Importer builder )
  23      {   builder.open();
  24          this.name   = new Name      ( builder.provideName()     );
  25          this.id     = new EmployeeId( builder.provideID()       );
  26          this.salary = new Money     ( builder.provideSalary(),
  27                                    new Locale("en", "US") );
  28          builder.close();
  29      }
  30
  31      public void export( Exporter builder )
  32      {   builder.addName  ( name.toString()   );
  33          builder.addID    ( id.toString()     );
  34          builder.addSalary( salary.toString() );
  35      }
  36
  37      //...
  38  }
  39  //----------------------------------------------------------------------
  40  // Unit-test stuff
  41  //
  42  class Name
  43  {   private String value;
  44      public Name( String value )
  45      {   this.value = value;
  46      }
  47      public String toString(){ return value; };
  48  }
  49
  50  class EmployeeId
  51  {   private String value;
  52      public EmployeeId( String value )
  53      {   this.value = value;
  54      }
  55      public String toString(){ return value; }
  56  }
  57
  58  class Money
  59  {   private String value;
  60      public Money( String value, Locale location )
  61      {   this.value = value;
  62      }
  63      public String toString(){ return value; }
  64  }

Let's look at an example. The following code builds Figure 1's UI:

Employee wilma = ...;
JComponentExporter uiBuilder = new JComponentExporter();    // Create the builder
wilma.export( uiBuilder );                                  // Build the user interface
JComponent userInterface = uiBuilder.getJComponent();
//...
someContainer.add( userInterface );

Listing 2 shows the source for the JComponentExporter. As you can see, all the UI-related code is concentrated in the Concrete Builder (the JComponentExporter), and the Context (the Employee) drives the build process without knowing exactly what it's building.

Figure 1. A Builder-constructed output UI

Listing 2. Exporting to a client-side UI

   1  import javax.swing.*;
   2  import java.awt.*;
   3  import java.awt.event.*;
   4
   5  class JComponentExporter implements Employee.Exporter
   6  {   private String name, id, salary;
   7
   8      public void addName  ( String name   ){ this.name = name;     }
   9      public void addID    ( String id     ){ this.id = id;         }
  10      public void addSalary( String salary ){ this.salary = salary; }
  11
  12      JComponent getJComponent()
  13      {   JComponent panel = new JPanel();    
  14          panel.setLayout( new GridLayout(3,2) );
  15          panel.add( new JLabel("Name:  ") );
  16          panel.add( new JLabel( name ) );
  17          panel.add( new JLabel("Employee ID:  ") );
  18          panel.add( new JLabel( id ) );
  19          panel.add( new JLabel("Salary:  ") );
  20          panel.add( new JLabel( salary ) );
  21          return panel;
  22      }
  23  }

The JComponentExporter follows the structure of the example in the Gang of Four book, but you can eliminate that getJComponent() call in the earlier example by rewriting the Builder as a JPanel that implements Employee.Exporter. Listing 3's EmployeeUI class does just that. You would use it to create Figure 1's UI as follows:

EmployeeUI ui = new EmployeeUI();   
wilma.export( ui );                 // Export state to the user interface
//...
someContainer.add( ui );

Listing 3. A JComponent-based Builder

   1  import javax.swing.*;
   2  import java.awt.*;
   3  import java.awt.event.*;
   4
   5  class EmployeeUI extends JPanel implements Employee.Exporter
   6  {   private JLabel nameWidget   = new JLabel("");
   7      private JLabel idWidget     = new JLabel("");
   8      private JLabel salaryWidget = new JLabel("");
   9
  10      public void addName  ( String name   ) { nameWidget.setText(name);   }
  11      public void addID    ( String id     ) { idWidget.setText(id);   }
  12      public void addSalary( String salary ) { salaryWidget.setText(salary); }
  13
  14      public EmployeeUI()
  15      {   setLayout( new GridLayout(3,2) );
  16          add( new JLabel("Name:  ") );
  17          add( nameWidget );
  18          add( new JLabel("Employee ID:  ") );
  19          add( idWidget );
  20          add( new JLabel("Salary:  ") );
  21          add( salaryWidget );
  22      }
  23  }

What about the server side? A third implementation of Employee.Exporter (HTMLExporter in Listing 4) builds the following HTML representation of an Employee (Figure 2 shows the resulting UI):

<table border="0">

<tr><td>Name:</td><td><input type="text" name="name" value="Fred"></td></tr>

<tr><td>Employee ID:</td><td><input type="text" name="id" value="1"></td></tr>

<tr><td>Salary:</td><td><input type="text" name="salary" value="100.00"></td></tr>

</table>

Use it like this:

Employee fred = //...
HTMLExporter htmlBuilder = new HTMLExporter();
fred.export( htmlBuilder );
String htmlRepresentation = htmlBuilder.extractHTML();

You can add additional representations (XML, SQL, etc.) simply by providing your own Employee.Exporter implementations, all without modifying the Employee.

Figure 2. An HTML representation of an Employee

Listing 4. Building an HTML representation

   1  /** This particular importer creates only that part of the
   2   *  HTML page that represents attributes of the Employee.
   3   *  The surrounding context (the &lt;html&gt; tags, etc.) is
   4   *  created elsewhere.
   5   */
   6
   7  class HTMLExporter implements Employee.Exporter
   8  {
   9      private final String       HEADER = "<table border=\"0\">\n";
  10      private final StringBuffer out = new StringBuffer(HEADER);
  11
  12      public void addName( String name )
  13      {   out.append( "\t<tr><td>" );
  14          out.append( "Name:");
  15          out.append( "</td><td>" );
  16          out.append( "<input type=\"text\" name=\"name\" value=\"" );
  17          out.append( name );
  18          out.append( "\">");
  19          out.append( "</td></tr>\n" );
  20      }
  21      public void addID( String id     )
  22      {   out.append( "\t<tr><td>" );
  23          out.append( "Employee ID:");
  24          out.append( "</td><td>" );
  25          out.append( "<input type=\"text\" name=\"id\" value=\"" );
  26          out.append( id );
  27          out.append( "\">");
  28          out.append( "</td></tr>\n" );
  29      }
  30      public void addSalary( String salary )
  31      {   out.append( "\t<tr><td>" );
  32          out.append( "Salary:");
  33          out.append( "</td><td>" );
  34          out.append( "<input type=\"text\" name=\"salary\" value=\"" );
  35          out.append( salary );
  36          out.append( "\">");
  37          out.append( "</td></tr>\n" );
  38      }
  39      String extractHTML()
  40      {   out.append("</table>");
  41          String toReturn = out.toString();
  42          out.setLength(0);                   // erase the buffer
  43          out.append(HEADER);
  44          return toReturn;
  45      }
  46  }

Now let's look at the input side. The Employee constructor takes as its argument a class that I think of as a "reverse builder." This class, the Employee.Importer, really follows the same philosophy as the Gang of Four Builder, but it initializes the current object by providing default values rather than creating (and initializing) some external object. Like the output-side builder, the Importer serves to isolate the external representation of the object from the business object itself. It's not an accident, by the way, that the constructor takes an Importer argument and no import() method exists. It's a potential runtime error to create an uninitialized object and initialize it later with an "import" or "set" operation. You don't want to run the risk of using uninitialized objects.

Let's assume you built an input form using the HTMLExporter I just discussed (that creates three <input...> fields named name, id, and salary). The following code shows how a servlet can build an Employee using the data from that form. It uses my "reverse builder" to initialize an Employee from the ServletRequest object passed to an HTTPServlet's doPost() or doGet() method:

class myServlet extends HTTPServlet
{   
    void doPost(HttpServletRequest request, HttpServletResponse response) 
    {   Employee fred = new Employee( new HTMLImporter(request) );
        //...
    }
    //...
}

Listing 5 shows the sources for the HTMLImporter. As you can see, it just extracts the required data from the ServletRequest and relays it to the Employee under construction.

Listing 5. Builder-based initialization in a servlet

   1  import javax.servlet.ServletRequest;
   2
   3  class HTMLImporter implements Employee.Importer
   4  {   ServletRequest request;
   5
   6      public void open() { /*nothing to do*/ }
   7      public void close(){ /*nothing to do*/ }
   8
   9      public HTMLImporter( ServletRequest request )
  10      {   this.request = request; 
  11      }
  12      public String provideName()
  13      {   return request.getParameter("name");
  14      }
  15      public String provideID()
  16      {   return request.getParameter("id");
  17      }
  18      public String provideSalary()
  19      {   return request.getParameter("salary");
  20      }
  21  }
  22
  23  /***********************************************************************
  24   * Stub for debugging. The ServletRequstAdapter (not shown) just
  25   * implements every method of the {@link javax.servlet.ServletRequest}
  26   * interface to throw an {@link UnsupportedOperationException}.
  27   */
  28
  29  class MyServletRequest extends com.holub.servlet.ServletRequestAdapter
  30  {
  31      String name, id, salary;
  32
  33      public MyServletRequest( String name, String id, String salary )
  34      {   this.name   = name;
  35          this.id     = id;
  36          this.salary = salary;
  37      }
  38
  39      public String getParameter( String requested )
  40      {   if( requested.equals("name")        ) return name;
  41          else if( requested.equals("id")     ) return id;
  42          else if( requested.equals("salary") ) return salary;
  43          else throw new Error("Unexpected Paramether");
  44      }
  45  }

It's marginally harder to build an Employee with an interactive client-side UI, but that's possible too. Figure 3 depicts the JComponentImporter class to initialize an Employee using the UI. Use it like this:

JFrame parent = ...;
Employee wilma = new Employee( new JComponentImporter(parent) );

Listing 6 displays the source code for JComponentImporter. The open() method (line 16) does the hard work of building the dialog box and displaying it. In this particular implementation, open() doesn't return until the user dismisses the dialog by hitting the Okay button. The remaining methods just get the information from the dialog.

Listing 6. Builder-based client-side initialization

   1  import javax.swing.*;
   2  import java.awt.*;
   3  import java.awt.event.*;
   4
   5  class JComponentImporter implements Employee.Importer
   6  {
   7      private JTextField  name   = new JTextField(20);
   8      private JTextField  id     = new JTextField(20);
   9      private JTextField  salary = new JTextField(20);
  10      private JDialog frame;
  11
  12      public JComponentImporter( Frame parent )
  13      {  frame = new JDialog( parent, "Test Input", true );
  14      }
  15
  16      public void open()
  17      {
  18          Container content = frame.getContentPane();
  19          content.setLayout( new GridLayout(4,2) );
  20          content.add( new JLabel("Name:  ") );
  21          content.add(  name );
  22          content.add( new JLabel("Employee ID:  ") );
  23          content.add(  id );
  24          content.add( new JLabel("Salary:  ") );
  25          content.add(  salary );
  26
  27          JButton okay = new JButton("Okay");
  28          okay.addActionListener
  29          (   new ActionListener()
  30              {   public void actionPerformed(ActionEvent e)
  31                  {   frame.dispose();
  32                  }
  33              }
  34          );
  35          content.add(new JLabel(""));    // place holder
  36          content.add(okay);
  37
  38          frame.pack();
  39          frame.show();
  40      }
  41
  42      public String provideName()     {   return name.getText();   }
  43      public String provideID()       {   return id.getText();     }
  44      public String provideSalary()   {   return salary.getText(); }
  45
  46      public void close()
  47      {   name = id = salary = null;
  48      }
  49  }
Figure 3. Client-side initialization

Consequences of refactoring the Context

Obviously, the Builder pattern doesn't eliminate the accessor and mutator methods, but—and this is critical—the accessors and mutators are now associated with the Builder, not with the business object. This means that changes to implementation of the business object will not ripple out into the program as a whole—our main goal in encapsulating implementation.

The choice of methods in the Builder interface does expose some Context-related implementation information to the Builder derivatives, and makes them susceptible to change when the business object changes. In the current example, I tried to minimize that impact by using String arguments to pass information to (and get information from) the builders, but that's not always possible. Nonetheless, the pattern does not—and again, this fact is critical—expose business-object implementation details to anyone except the Builders. That is, changes to the Employee that mandate changes to the Importer and Exporter interfaces affect only a limited number of classes (the Concrete Builders). If you put get/set methods on the Employee class itself, the scope of the damage in the event of a change is unlimited.

Even though the composition of the Builder interface is a minor weak point, if you design this interface carefully, you can reduce the odds of changes to it. For example, the Importer and Exporter interfaces would be more delicate if I imported a salary as a double instead of using a String.

Summing up

A good OO system hides implementation as thoroughly as possible. Dubious programming idioms like get/set methods seriously damage the maintainability of the system. Fortunately, design patterns like Builder can provide ways to eliminate get/set methods—or at least limit their scope—while improving the program's structure and maintainability. It's a win-win situation. Builder is not the only solution to the get/set-elimination problem, by the way, but it's effective in many situations.

If you decide not to use the get/set idiom, thereby forcing yourself to think of alternative ways to accomplish the same end, your code will become easier to maintain and more object oriented. The main rule is "don't ask for the data that you need to do something; ask the object that has the data to do the work for you." (OO designers call this principle "delegation.") Similarly, if you keep your design in the problem domain as long as possible, as I described in the previous getter/setter article, then get/set methods tend not to appear in the first place.

I know this article will probably engender as much trash in the associated Talkback discussion as the last one; so let me finish up with a few additional points. As I said in the first article (though this point seems to have been ignored by the most vitriolic of the posters), you can't eliminate get/set methods entirely from any program. For example, you'll often need them at the "procedural boundary" of the program, where you create general-purpose classes that must exist outside of the domain of a particular problem, and these classes must communicate with a procedural subsystem like the operating system or a relational database server. Low-level UI classes like the Swing components also typically need at least a few get/set methods, primarily because they're so generic in structure.

Giving up procedural idioms is neither easy nor comfortable. Too many things that are "right" in a procedural context are "wrong" in an OO context. I've heard many lame excuses for staying in the procedural comfort zone. One of the contributors to the previous article's discussion, for example, claimed that hard-core data abstraction such as I've described was worthless in situations where maintenance was not important, which he infers, is often the case. I can't personally imagine any situation where maintenance is not important, however. First of all, throw-away programs never seem to be thrown away. I've worked on 20-year-old "throw-away" code. More importantly, my code hits maintenance roughly two seconds after I finish writing it. Whenever I look at code I wrote yesterday, I'm in maintenance mode. I imagine that the writer of that comment is just a much better programmer than me. He writes perfect code right off the bat and never looks back at his code once it's written. Though I can aspire to that level of perfection, I'm not there yet. I'm constantly refactoring my code to improve it. Every time a business rule changes—and this often happens while the program is under construction—the existing code is effectively in maintenance.

Giving up "procedural thinking" isn't easy, but it's worth doing.

Allen Holub has worked in the computer industry since 1979. He currently works as a consultant, helping companies not squander money on software by providing advice to executives, training, and design-and-programming services. He's authored eight books, including Taming Java Threads (Apress, 2000) and Compiler Design in C (Pearson Higher Education, 1990), and teaches regularly for the University of California Berkeley Extension. Find more information on his Website (http://www.holub.com).

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies