JavaRanch Home    
 
This page:         last edited 19 April 2014         What's Changed?         Edit

User Input Part I I I   

[This page is under construction, and should hopefully be ready in a couple of weeks (timeline: mid-Jan 2014). Please disregard any information you see until this message is removed]

.

(Level: intermediate - in particular, it assumes a basic knowledge of Java Generics)

.

In UserInputPartDeux we showed you how to build an Input utility class, and how to expand that into a more flexible input object.

However, there are still some drawbacks, even with the latter approach, the main of which is the rather cumbersome logic we have to write to implement the "try count". What if we could create an input object tailored exactly to our needs? Then we could make the number of tries part of that object, rather than having to pass around a "count" between methods.

Using a "month" value as an example, the process then becomes:

  1. Create an input object for a range (in this case: 1-12).
  2. Use it to get a month value.
  3. Forget about it, or re-use it as needed.

A second benefit of a tailor-made object is that it is inherently Thread-safe because we create individual objects for each value we want to input. Before, we created one object for all our input requirements.

Now this may sound a bit OTT for getting a simple value but, as you'll see, it allows us to create an extremely dynamic framework for the process, which we can extend almost any way we like.

So what if we end up creating a few extra objects? That's what Object-Oriented languages are designed for; and even a few thousand objects is nothing to a modern computer.

Furthermore, it simplifies the API enormously, since we only have one public method to worry about - get(). We can also make use of generics, both for compile-time safety and as a documentation aid.

However, such a design does require us to re-think our model a bit.

.

First: There are some things, like the input and output streams, our 'Please enter' prompt, and our generic "types", that we probably only want to set up (or define) once. It's only when we have specifc logic (eg: ranges, or limiting the number of tries) that we want to create tailor-made objects to do the work.

This implies that our structure needs to be tiered: first, we need an Input object of the kind we saw at the end of UserInputPartDeux; second, a new tier of objects that we create from our initial Input one. This makes our Input object a factory - quite a common pattern in software design.

Furthermore, if we design it properly, we can allow clients to create their own tailor-made classes, much as we did with our Type interface, and then simply use our Input factory to plug in its own streams, etc to create the final object.

.

If all this seems like a fabulous amount of effort simply to get a number or a string: you're right. I'll say it one more time:

User input is tough.

If you're happier off going back to simply writing code that you have to repeat every time you want to input something, or with the simple utility class we already covered, then the likelihood is that you're not ready for this. However, if you want a framework that you write once, stick in a package, and forget about, safe in the knowledge that it will probably handle anything you ever want to input - including things we haven't even thought about yet - read on.



STARTING OVER  

Whenever you embark on a new project, the first thing you should do is come up with a description of the problem. And that description should be what you want to do, not how you want to do it. You are NOT coding yet. For more information, have a look at the WhatNotHow page.

Furthermore, when you're doing this stuff, start simple and build on it bit by bit. The first thing you write is sometimes called a "Mission statement", and it should be very short - two or three sentences at most.

So: what do we want to do? How about this:

  "We need a framework for user input that accepts data from a source such as a keyboard, and converts it to any type we want, including any constraints we may wish to impose on its validity. The framework needs to allow users to abort the process at any time, and/or specify a maximum number of attempts, after which it exits in a recognisable manner."

.

Looks simple, huh? Don't be fooled. That one took me about an hour, and I've worked on projects where they took days, because getting it right is important. After all, it's going to be your primary source document. The main things to remember:

  • It must be short.
  • It must be simple.
  • It must be complete.

and that last requirement can be a real bastard.

Did you also notice that last sentence? It says that we need to allow the user to abort the process themselves. Note that we haven't said how we're going to do this, but allowing them to enter something like "quit" seems like a reasonable idea - and something we haven't yet talked about in our tutorial up to now.

.

So, what next? One thing I suggest is to start a list of assumptions. This can be very useful to make sure that you're not getting off track, and you can add to it as you see fit. There are only a few I can think of at the moment:

  1. The framework is for "user input",  so it will be dealing with a person, not a file or a stream.
  2. The process will be interactive (ie, a dialogue).
  3. The "source" will be character-oriented (eg, a keyboard).
  4. Output for prompts and messages will also be character-oriented (eg, a terminal).

that last one could be modified later on, but lets leave it in for now to save speculation.

.

Next: we probably want to expand on a few of the keywords in our mission statement, so that their meaning is unambiguous. For example:  

TYPE
Defines the type of object we will be needing. This may include any conversion that needs to be done on the text that the user enters, and any basic validation to ensure that we can in fact convert it.
CONSTRAINT
A validation constraint on the value the user types in, over and above that required for simple conversion. For example: a number may have to be in a certain range.

.

Do you see what's happening? We're building on our initial statement, filling it out as we go along. And also note that we haven't ONCE mentioned a Java class yet, or indicated HOW we're going to code this.



Input revisited  

OK, that's enough design stuff for the moment (actually, in the real world it probably isn't; but you're here to see some code - right?).

First, let's gut our Input class and reduce it to only what we need:


public final class Input {
  /**
   * Constant used to allow an unlimited number of tries.
   */
  public static final int UNLIMITED = Integer.MAX_VALUE;

  private final Scanner IN;
  private final PrintStream OUT;
  private final String prefix;
  private final String abortString;

  /**
   * Returns an Input object that uses "environment"
   * information supplied by the client.
   * @param in The input stream to be used for input.
   * @param out The output stream to be used for messages.
   * @param prefix The prefix to be used in all prompts.
   * @param abortString The string a user can type in to abort
   *        the input process. If null or blank, this option
   *        is suppressed.
   */
  public Input(InputStream in, OutputStream out,
      String prefix, String abortString) {
    this.IN = new Scanner(in);
    this.OUT = new PrintStream(out, true);
    this.prefix = default(prefix, "");
    this.abortString = default(abortString, "");
  }

  /**
   * Returns an Input object that uses System streams for
   * input and output, and prefixes each prompt with
   * "Please enter ".
   * @param tries The number of tries allowed. A value less
   *        than 1 means "unlimited".
   */
  public Input() {
    this(System.in, System.out, "Please enter""quit");
  }

  /**
   * The Exception thrown when a user specifically requests
   * the input process to be aborted.
   */
  public static final AbortRequestedException
    extends RuntimeException
  {
    private AbortRequestedException() {
      super();
    }
  }

  /**
   * The Exception thrown when a user exceeds the number of
   * attempts allowed for an input.
   */
  public static final TriesExceededException
    extends RuntimeException
  {
    private TriesExceededException(int maxTries) {
      super(String.format("number of allowed tries(%d) exceeded",
         maxTries));
    }
  }

  /**
   * Defines a type that needs to be created from an
   * input String.
   */
  public static interface Type<T> {
    /**
     * Returns the supplied String converted to this
     * type; or null if there was a problem.
     * @param s The String to be converted.
     * @return As described above.
     */
    public T convert(String s);
    /**
     * Returns the description of this Type, primarily for use
     * in error messages. It should include the INdefinite
     * article ('a' or 'an').
     * @return As described above.
     */
    public String description();
  }

  /**
   * A type for Integers.
   */
  public static final Type<Integer> INT = new Type<Integer>() {
    @Override
    public Integer convert(String s) {
      try {
        return Integer.valueOf(s);
      } catch (NumberFormatException e) {
        return null;
      }
    }
    @Override
    public String description() {
      return "an integer";
    }
  }; 

  /**
   * A type for Doubles.
   */
  public static final Type<Double> DOUBLE = new Type<Double>() {
    @Override
    public Double convert(String s) {
      try {
        return Double.valueOf(s);
      } catch (NumberFormatException e) {
        return null;
      }
    }
    @Override
    public String description() {
      return "a decimal number";
    }
  };

  /**
   * Returns the supplied String, or its default.
   * @param s The String.
   * @param default The default to be used if {@code s} is null.
   * @return As described above.
   */
  private static final default(String s, String default) {
    return s != null ? s.trim()
      : default != null ? default.trim() : "";
  }

  /**
   * Checks if the user is allowed to request an "abort".
   * @return {@code true} if, and only if, aborts are allowed.
   */
  public final boolean abortAllowed() {
    return abortString.length() > 0;
  }

  /**
   * Checks if the user's input is equal to our "abort" string
   * (ie, if they want to get out of the input process).
   * @param input The user's input.
   * @return {@code true} if, and only if, they requested an abort.
   */
  public final boolean abortRequested(String input) {
    return abortAllowed() && abortString.equalsIgnoreCase(input);
  }

  /**
   * Equivalent of System.out.println() for this Input object.
   * @param message The String to be displayed.
   */
  public final void println(String message) {
    OUT.println(message);
  }

  /**
   * Prompts for input and returns what the user typed in.
   * @param prompt The String to be used as a prompt.
   * @return The user's input, trimmed of all whitespace.
   */
  public final String input(String prompt) {
    println(prompt);
    return IN.nextLine().trim();
  }
}

Notice that maxTries is no longer a member variable, because we will be adding that to our 'tailor-made' objects in due course; but we have added an abortString field. This is so we can allow users to enter a string that means "get me out of here!".

mission, assumptions, keywords.



CategoryWinston

JavaRanchContact us — Copyright © 1998-2014 Paul Wheaton