More on getters and setters

Build user interfaces without getters and setters

1 2 3 Page 2
Page 2 of 3

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.

1 2 3 Page 2
Page 2 of 3