Win a copy of Re-engineering Legacy Software this week in the Refactoring forum
or Docker in Action in the Cloud/Virtualization forum!
    Bookmark Topic Watch Topic
 
friki data migration
Ranch Hand
Posts: 772
  • Mark post as helpful
  • send pies
  • Report post to moderator
(level: Beginner/Intermediate)

This page is "part 2" of the UserInput tutorial, so if you haven't read the first part. I suggest you do before you go any further.

.

UserInput focused on rationalizing the process of user input, which is notoriously fiddly and verbose, into methods that you can reuse. In this chapter, we plan to show you how to build on that by creating your own utility class (or classes).

There are two sections to this part - a "simple" one, and a more "object-oriented" one:


  • The first helps beginners to create their own utility class so that they don't have to constantly copy code when they're writing exercises that need user input. It's simple (especially if you read UserInput), but it has some drawbacks.



  • The second suggests a way to build a more flexible input object.


  • After which you may be interested in going on to UserInputPartIII, which shows a way to build a proper input framework that you can extend and configure any way you like. Needless to say, this part is MUCH longer, and will take more time to write; so if you see a notice at the start of it saying "[under construction]", please be patient.




    The simple way - A basic utility class  

    You will probably have run across utility classes in your lessons already - for example: Math - and their structure is almost always the same:


  • They are not instantiated.
  • Their methods are static, so they are called via the class name, eg: Math.log(2.0).


  • and the method for setting one up always follows the same pattern:



    There are three things to note about the above declaration:


  • The class is public, as you will probably want anyone to be able to use it.
  • It has a private no-args constructor. This prevents anyone from accidentally instantiating it.
  • It is also final. This is important because you don't want anyone to be able to extend it. The fact is that, since it can't be instantiated, it can't actually be extended either; but adding the final makes that point clear to anyone using it - ie, it's basically a form of documentation.


  • .

    So...now what do we do?

    Well, the UserInput page showed you how to set up generic input methods, so now you simply move (note: move, not copy) them to your new class, viz:



    Hopefully, most of the methods should be familiar from the UserInput page. We've simply transferred them to a class that combines them all, so that they can be used by anyone. I've also added a rangeString() helper method for creating the "range" part of a message.

    Note also the special comments that I've included for all the public methods. These are javadoc comments, and I advise you to get to know them, because they provide the wonderful documentation you get in the API.

    And now we change our old class to use the utility methods - ie, something like this:

















    And you can now use those Input methods in any class that needs input.




    Improving the Input class (slightly more advanced)  

    You may have noticed that there's still quite a lot of duplicated code in our Input class. The getInteger()/getDouble() and inRange() methods, for example, are virtually identical except for the type-specific calls they make. Wouldn't it be nice if we could write ones that simply take the type of thing we want to convert to?

    At this point, a lot of people (especially beginners) immediately start looking at reflection, because it seems like an answer to all your prayers for dynamic type-checking.

    My advice: DON'T.

    Reflection can be a very powerful tool in the right situations, but it should be used very sparingly because it's verbose, error-prone, difficult to test, and SLOW.

    Furthermore, it should only be used in cases where clients really don't know what a type is going to be until runtime, and that's not the case here. When we call an Input method, we DO know what type we want; we simply want a way to generalise our methods.

    So what can we do instead? Answer: Create a class or interface that encapsulates our "type". That's how Object-Orientation works.

    In our case, the only thing that's specific in our input process is the conversion; everything else is generic. And for types that don't need converting? Simple: don't convert; just return them.

    .

    So, putting that into practise, we might come up with something like this:



    and then we can write member types for our Input class as follows:



    and for types that don't need conversion:



    or indeed, just leave it out altogether. After all, a String can be obtained by simply calling Input.input().

    And just in case you're not familiar with the constructs above, they are anonymous classes - a very useful thing to get to know.

    .  

    If the above isn't very clear yet, let's see what happens when we put it in our Input class. Pay particular attention to the get() and inRange() methods that replace the type-specific ones we had before:



    Note the static qualifier on the Type interface. To be honest, I'm not sure it's absolutely necessary, but I always put it in to remind myself that the definition is nested; and it IS important when you're defining classes.

    .

    Do you see what's happened? No more overloaded methods, and no (or very little) duplicated code. Furthermore, we can add new member types (for example, a LONG type) as we find a need, and we shouldn't need to change anything else. Indeed, clients can supply their own types if they want to, by simply writing conversion classes that implement Input.Type. And that is what Object-orientation is all about.

    And now our calling class will look like this:

















    .

    If you find the definitions of the generic methods confusing, don't worry about it too much for the moment. The first Input style will probably do you just fine until you learn a bit more about generics.

    There is also a slight problem with what we've written so far: The error message. It simply says "Invalid input", when we probably want something a bit more descriptive. This could easily be remedied by adding a getType() method to our Type interface, but I'll leave that up to your ingenuity.

    It should also be added that this is only ONE way of doing it; there are many others.

    .

    However, there are still a few drawbacks to the "utility class" approach. Try and see if you can work out what some of them are before you read on.




    Drawbacks

    Basically, the class is rather brittle, and a lot of that is due to the fact that it can't be instantiated, and its methods are all static:


  • It only allows one prompt prefix: "Please enter".
  • It assumes that messages are always directed to System.out.
  • You have to pass the Scanner to every method. Wouldn't it be nice to be able to set that up once and just use it for every input?
  • It doesn't allow the user to break out of the input process, even if they just "don't get it". It might be nice, for example, to give them a certain number of tries before the class simply gives up and throws an Exception.


  • All the above can be easily remedied, but you need to use an object rather than a utility.




    A more 'Objective' way - An Input object  

    Right, so we want to to include the things mentioned above in the "Drawbacks" section. The simplest way to do that is to make Input a class that we can instantiate, rather than a utility, and add them as fields that can be set up at construction time. Let's have a look at what that might look like:











    Pretty simple, no? The main difference so far is that now our constructors are public. We still keep the class final because we really don't want anyone extending it (yet).

    And the rest of the Input class doesn't need to change too much, except that its methods will no longer be static. We also need to add logic to keep track of the number of tries on each input.

    Let's have a look at what it might look like. I've used the second version as my basis:



    Note that we've had to add quite a bit of extra logic to keep track of the number of attempts. This is because the value is independent of all the other things we're doing, which makes things a bit tricky. However, the "business" methods are still pretty much the same, apart from the fact that they are no longer static, and we don't have to supply a Scanner to them.

    Note also:

  • TryCount is a non-static nested class, because it needs to be associated with a specific Input instance, so it can access its maxTries value. There are probably many ways we could have done this, but a mutable "count" object seemed to me the clearest. As you'll see later, there is another approach that is arguably even better.
  • The increment() method throws IllegalStateException when the number of tries is exceeded. An alternative (and probably better) approach is to define your own custom Exception; again, you'll see that in action later.
  • I've changed the internal loops back to the "do forever" (while (true)) style, because we have to increment the count each time we iterate.
  • I've added a description() method to our Type interface, so that error messages are more specific. We could have done this at any point in the development, but now seemed like an opportune time.


  • .

    And now our calling class will look something like this:

















    As you see: very little different from before apart from the initial setup.

    .

    Phew. A lot of code. I bet you never thought you would be writing this much back when you started, but I go back to what I said right at the beginning of this tutorial:

    User input is tough.

    However:

  • It's all in one place, so it can be used any time you need an input value.
  • You never need to write it again.
  • (Very important) It's NOT in main() any more.
  • A lot of the bulk is documentation, which I strongly urge you to do as you write.
  • We've developed the class incrementally.


  • .

    However, we're still not finished yet. There's no doubt that our input object is a lot more flexible than before, but that "try count" logic is pretty cumbersome - and kind of ugly.

    UserInputPartIII shows you how to adapt what we've written into a proper input framework, but it does assume some knowledge of Java Generics. If you don't feel you're quite ready yet, you should still be able to use what you've read so far to write a decent utility class that will last you until you're ready for the "next stage of evolution".



    CategoryWinston
      Bookmark Topic Watch Topic
    • New Topic