Java Tip 139: Ask the right questions in your survey application

Decouple survey content and logic for a faster, more cost-effective survey

Not long ago, I found myself involved in a project that determined the value of import/export goods for customs valuation as specified by the General Agreement on Tariffs and Trade (GATT) established by the World Trade Organization (WTO) (also know as GATT/WTO customs evaluation). Much like a tax preparation application, goods valuation depends on answers to a series of questions regarding the nature of how the product is made, transported, and so on. Basically, the application interviews the user in successive steps to gather facts (e.g., "Does the seller give a special discount to the buyer?"). Depending on the answer chosen from the given list, more questions and/or instructions are presented. An answer choice dictates the course of action. If you use tax preparation software such as Turbo Tax, you would see a similar concept.

For example, consider a simple scenario when using Turbo Tax:

Q. Choose your filing status:

  • Single
  • Married, filing jointly
  • Married, filing separately
  • Head of household
  • Qualifying widow(er)

If you choose "married, filing jointly," you must enter information for both spouses, as opposed to information for one spouse, which you would enter if you choose "married, filing separately." Similarly, you would not be further bothered with questions regarding stocks or bonds if, by answering a question, you told Turbo Tax that you do not own any. However, if you do own some, you must answer more questions about your holdings. By providing only one question at a time in successive order, the questionnaire would pose only relevant questions associated with the chosen answer.

If we use a survey design where an answer to a question does not determine the next question, then Turbo Tax users who are single or married but not filing jointly would be quite frustrated. Additionally, when new laws and regulations cause the logic and data to change from year to year, Turbo Tax code would have to be modified. It would be more cost effective if such logic and data was independent and maintained separately from the codebase. This article shows you how to develop an application that decouples the surveying content and logic from the application.

Note: Download this article's code from Resources.

The requirements

A good solution should determine the appropriate actions (logic) based on various data (survey content) without coupling the object structure with the decision and evaluation logic so both the data and logic can be maintained separately from the code. Thus, our problem requirements are:

  • Object interactions can be determined dynamically by one selection out of finite possible choices—choosing a path in a decision tree
  • The decision tree is built from the survey content and is loosely coupled with the object structure

Structure

With such requirements, we have a class diagram similar to the one in Figure 1.

Figure 1. Class diagram shows the design. Click on thumbnail to view full-size image.

Figure 2 illustrates a typical object structure.

Figure 2. Object diagram shows a possible object interaction. Click on thumbnail to view fill-size image.

From the structure, the major pieces are:

  • Action
    • Represents an abstraction of an action to be performed
    • May contain the successor link that is either statically or dynamically determined
    • May contain the predecessor link to provide "undo" capability (optional)
  • Question
  • Represents an abstraction of a question that needs to be answered
  • Answer
  • Represents an abstraction of an answer which may lead to the next possible action
  • ConcreteQuestion
  • Presents the question and receives an answer from a list of multiple choices
  • With the answer, it can determine the next available course of action
  • Client
  • Initiates the action execution, which may result in a successive action once it completes

If your survey requires data entry, instruction, or other capabilities, it extends the action abstraction and performs the appropriate function.

The UML diagrams shown above give us an object structure of how successive questions and their answers make up a decision tree. The structure does not depend on the actual content and how the decision tree is built.

Implementation

In implementing this solution, I find it helpful to apply various design patterns. For example, using the Visitor pattern, I can abstract the detail implementation of answering questions behind the QuestionInteraction interface. Also, I use the Template Method pattern to delay specific details about how each action is performed.

Listing 1 shows a possible implementation of the action abstraction. It provides only the basic but necessary structure dictated from the design diagram in Figure 1. In essence, the abstract class Action provides the necessary wiring while leaving the detail implementation for the subclasses:

Listing 1. Action.java

public abstract class Action
{
    //Constructor is protected - should always have a subclass object
    protected Action(String id) { id_ = id; }
    public String getId() { return id_; }
    public boolean isDone() { return done_; }
    public void setPreviousAction(Action prev) { prevAction_ = prev; }
    public String getNextActionId() { return nextActionId_; }
    public Action getPreviousAction() { return prevAction_; }
    public abstract void perform();
}

Listing 2 shows the structure of a question with various possible answers. The perform() method requires an available QuestionInteraction interface, shown in Listing 3, for answering the question, regardless of how this interaction is implemented:

Listing 2. Question.java

public abstract class Question extends Action
{
    //......
    // The actual question itself
    public String getText() { return questionText_; }
    // The explanation or instruction text for answering the question
    public String getExplanation() { return explanation_; }
    // Allow multiple choices
    public void addAnswerChoice(Answer ans) { ansChoices_.add(ans); }
    // Set the interaction method to be used
    public void setQuestionInteraction(QuestionInteraction qInt) {
        interaction_ = qInt;
    }
    /* The question is answered */
    public void setAnswer(int index) {
        Answer ans = (index >= 0 && index < ansChoices_.size()) ?
                        (Answer) ansChoices_.get(index) : null;
        setAnswer(ans);
    }
    public void setAnswer(Answer ans) {
        answer_ = ans;
        if (answer_ != null) {
            setDone(true); //Question has been answered
            Action consequence = answer_.getConsequenceAction();
            if (consequence != null)
                setNextActionId(consequence.getId());
        }
        else { // Not answered, or remove previous answer
            setDone(false);
        }
    }
    public void perform() {
        // Don't perform anything if there's no way of getting the answer
        if (interaction_ == null || isDone())
            return;
        // Use of double dispatch (first dispatch)
        interaction_.answer(this);
    }
}

Listing 3 shows an interface for providing a question-answer interaction mechanism. With this interface, we can have any kind of interaction implementation, a standard console I/O (input/output) or a colorful GUI (graphical user interface):

Listing 3. QuestionInteraction.java

// Implement this interface to display question and accept answer
public interface QuestionInteraction
{
    // Use of double dispatch when the implementation of this interface
    // sets the answer (second dispatch)
    public void answer(Question q);
}

Listing 4 shows an interface for loading the actions. This interface does not restrict the data in any way. A potential implementation could read the survey from file(s) or a database, and the data format could be XML or any other structure. The only contract this interface requires is that the implementation must create appropriate Actions:

Listing 4. ActionSource.java

// Interface to provide the source of all actions.  The actions
// are already wired together (decision tree).
public interface ActionSource
{
    /** Get the starting action. */
    public Action getStartingAction();
    /** Get the action given its ID. */
    public Action getAction(String id);
}

Once the survey content loads and the logic is all wired together, we need a controller class (ActionFlow shown in Listing 5) to facilitate the navigation and execution of one action at a time:

Listing 5. ActionFlow.java

public class ActionFlow
{
    //......
    /** Set the source of the actions. */
    public void setSource(ActionSource src) {
        actionSource_ = src;
        if (src != null)
            currentAction_ = src.getStartingAction();
    }
    public boolean isComplete() { return complete_; }
    public Action getCurrentAction() { return currentAction_; }
    /** Move forward one step in the flow, if possible. */
    public void moveForward()
    {
        //......
        Action next = getNextAction();
        if (next != null)
        {
            next.setPreviousAction(currentAction_);
            currentAction_ = next; //move forward
        }
        //......
    }
    /** Move backward one step in the flow, if possible.
     *  Otherwise, stay at current step. */
    public void moveBackward()
    {
        //......
        Action prev = currentAction_.getPreviousAction();
        if (prev != null) {
            currentAction_.setPreviousAction(null);
            currentAction_ = prev; //move backward
            complete_ = false;
        }
    }
    /** Answer the current question (current step is assumed to
     *  be a Question). */
    public void perform()
    {
        currentAction_.perform();
        moveForward();
    }
}

Putting things together, we now get something like below for the client code:

   try {
      System.out.println("Processing file " + dataFile);
      final DocumentBuilder db = getDocBuilder(); // Get an XML document builder
      final Document doc = db.parse(new FileInputStream(dataFile));
       ActionLoader loader = new ActionLoader(doc);
       ActionFlow flow = new ActionFlow(loader);
      while (!flow.isComplete())
         flow.perform();
   }
   catch (...) {
   }

On the client code, we have ActionLoader, which implements ActionSource. This class parses XML survey content and builds the appropriate decision tree by wiring the questions together (implemented as QuestionConcrete).

The XML survey content could look something like below:

 <act:ActionList version="1.0" xmlns:act="http://www.good_thang.net/xmlns/ActionList">
<act:Action id="1" type="Question">
   <act:Content value="What type of vehicle did you purchase?" />
   <act:Explanation value="Pick your choice" />
   <act:AnswerList>
      <act:Answer value="Car" consequence="2" />
      <act:Answer value="Truck" consequence="3" />
      <act:Answer value="SUV" consequence="4" /> 
   </act:AnswerList>
</act:Action>
<act:Action id="2" type="Question">
   <act:Content value="What type of car?" />
   <act:AnswerList>
   <act:Answer value="Sport Coupe" /> 
   <act:Answer value="Sedan" />
   </act:AnswerList>
</act:Action>
<act:Action id="3" type="Question">
   <act:Content value="What type of truck?" /> 
   <act:AnswerList>
      <act:Answer value="Pickup" /> 
      <act:Answer value="4x4" /> 
   </act:AnswerList>
</act:Action>
<act:Action id="4" type="Question">
   <act:Content value="Which brand of SUV?" /> 
   <act:AnswerList>
      <act:Answer value="Fore Runner" /> 
      <act:Answer value="Passport" /> 
      <act:Answer value="Path Finder" /> 
   </act:AnswerList>
</act:Action>
</act:ActionList>

Start managing complex surveys

This article illustrates a simple and extendable solution for managing complex surveys by presenting only the surveying questions applicable to the audience. The solution also applies to other problems that require the user to make choices within a complex decision tree, such as interviewing users, selecting menu options, and so on. With this solution, the survey (or menu, or interviewing) content and how it should be conducted (logic) can be decoupled from the code, allowing fast and low-cost development.

Terry N. Ngo is a senior software engineer at OpenHarbor, where he works on various enterprise software projects in global trade management. He holds a master's degree in electrical engineering from Stanford University, a bachelor's degree in computer engineering and a bachelor's degree in mathematics from Southern Methodist University.
1 2 Page 1
Page 1 of 2