![]() | Monkeys at Keyboards: The Javanomicon © Michael James Heron | ||||
| Topic: Java Programming Level: 2 Version: delta | |||||
15 - Case Study 4 - Calculators Revisited | |||||
| Previous | Table of Contents | Next |
| Forum |
| Chapter Objectives |
By the end of this chapter, the reader will be able to:
|
When we first learned about even driven programming, we looked at how we could use our various components to build a calculator in Java. In this section, we're going to revisit our calculator application and incorporate some of the new techniques we've discussed during the last two chapters. We're going to make use of the free standing application structure and convert our applet into something we can run independently of a web browser. We're going to look at our layout managers and how they can be used to create a calculator application that can be setup without requiring us to spend time developing storyboards that detail each of the co-ordinates of each of the components. We'll also look at separating out the functionality from the main application - making use of the model-view-controller architecture to allow for improved reusability and pave the way for future expansions. I bet it sounds as exciting to you as it does to me... so let's begin!
Before we start taking apart the hard work we've already done, we should begin with something simple to build up our confidence. The simple thing we're going to do is change our JApplet so that instead we are using a JFrame. In the process, we can get rid of our HTML file. The process for this should be relatively straightforward:
Step One: Changing the Inherited ClassThis part is as simple as changing the class definition from one thing to another. Before:
After:
Step Two: Configuring the Application in a ConstructorWe have an init method that is responsible for setting up the interface of our calculator. Alas, the init method is not actually called during the execution model of an application - instead, the interface must be set up within a constructor method - so we need to create one of these. It is enough to simply rename init in the format of a constructor method: Before:
After:
Step Three: Adding the main methodFinally, we need to add a main method that will configure and display an instance of our class. We use exactly the same format for this as we used in chapter 13 (free standing applications). First, we create a method stub for main:
Then within that method stub we need to create an object that will hold an instance of our Calculator application:
Then, we set the title:
Then the size:
And then set it to visible:
After this, we have a main method that will perform the functions we require of it, namely setting up the object and displaying it when the application is executed. From these simple steps, we have turned our JApplet into a JFrame, which means that we no longer require an HTML file to run our calculator. As you can see, the conversion process is not difficult, since we can use all of the tools we have discussed in the course of this book with both applets and applications - it is only the context that is different.
Now, let's look at separating out our calculator into logically discreet classes. At the moment, there's not a lot of functionality - most of the calculations are performed with a single line of code. A large amount of the code we wrote for our actionPerformed method exists for dealing with the user's interactions with the interface anyway - really all we want to separate out into a separate class is that functionality which is generic and applicable to the calculator logic only. Setting the text of a display box to an empty string is not generic functionality - it makes very specific demands on the way an application interface is to be laid out. For this reason, it is part of the view. The model for this particular application is really just the two numbers stored in the calculator, as well as the information regarding the operation to be performed and the code for performing it. We'll see why it is useful to break this functionality into a separate class later in this section. For now, we're just going to create a simple Calculator class that holds both the numbers and the code for each of the operations. We'll also add in some accessor methods for our two numbers:
We're also going to add in a method that takes two numbers and a integer representing an operation to be performed as parameters. This method will and handle the calling of the right method based on the desired operation, as well as returning the right answer. We'll call it handleArithmeti Remember how in the first Calculator we used some integer values to indicate which operation was to be performed:
We're going to use these same values within our new class to determine which method is to be selected. Alas, we have a problem in doing this - in our class we have no way of referring back to the application to access its variables. There is a way we can set this up, but it's a pretty ugly hack and I would feel guilty and morally culpable if I told you what it is. Really, those integer values should belong to the Calculator class and the application should refer to them in the same way we refer to the return types of JOptionPane. So let's do that! These are what are called constant values, in that they are used to make sure that code doesn't have to make use of magic numbers, and that a change needs to be implemented in only one place for code everywhere to be updated. We can easily add them in to our calculator class, but then what's to stop some nefarious ne'er-do-well from writing the following code?:
Obviously we could set the variables to private - but then no-one would be able to reference to them as constant values outside of the Calculator class. What a predicament! Java, little trooper that it is, comes to our rescue by providing a magic word called final that can be used to indicate that the state of a variable cannot be changed:
We should include a final declaration for each of these operations in our calculator:
Now it's possible for someone to refer to one of our constant values through their calculator object:
However, if you look closely you'll see a small difference in the way our calculator deals with constant values, and the way the JOptionPane class does - the difference is evident in how the constant values are referenced. In JOptionPane, we check against the class, which contains the values:
We never have to declare an object to use this constant, we can just refer to the class itself. This is called a static value, which we've already discussed briefly in the chapter on Object Orientation. We can make our constant variables static by adding the qualifier to their declaration:
And now people can use our constant values properly, without needing an object to do it - all they need to do is refer to the class name:
Okay, so we've moved these declarations into our Calculator class, and changed all references to them so that they refer to our constant values - we now need to write our handleArithmetic method:
The code for this method is very simple:
We also need to change our actionPerformed method a little so that it makes a call to an instance of our Calculator object rather than internally handle the arithmetic itself:
Hola! And now we've separated out our model, our view and our controller. As classes go, this is pretty pointless as it stands - the code required to handle the calculator object is not a lot simpler than the code required to handle the calculations internally. That's not the real point of this exercise however - the point is that we've got an independent Calculator class that we can now refine and expand indefinitely without requiring our application to be altered along with it. This is particularly relevant to this example - we had an applet that handled the calculations. Now we have an application that handles the calculations. Let's say that for one reason or another, we had both of them, and they both needed to be maintained simultaneously. If they are both handling the calculations internally, then we need to change two sets of code any time we want to make a change to the calculator logic, be it a bug fix or the implementation of additional functionality. We've gone down the other route and spent time separating out the calculator functionality. This way, if we need to change the way addition works - perhaps by adding in the facility to 'stack' additions so that we can add 2 and 3 to get 5, and then add 4 to get 9 - then we only need to change our Calculator object and then both our applet and out application would have the extra functionality without us having to change a single line. That's an excellent benefit, and one that will become more and more important as you begin developing more complex programs in this book and in later years as Java developers.
Okay, now we should look at the hard part of this section - changing the calculator so that it uses layout managers properly instead of setBounds. When the calculator is first executed, it looks okay - maybe a little sparse in the middle, but otherwise fine. However, when we resize the calculator, it looks awful:
So, we really need to apply a layout manager (or two, or more) to solve this problem for us. Oh, the back-breaking labour that will surely require is enough to move even the most stoic of souls to tears. However, we are very lucky in certain respects in that a calculator is ideally suited to the application of a layout manager because of the regular distribution of its components. The number buttons are in a nice neat grid, which we have a layout manager for (GridLayout). The operation buttons are in a nice column, which we also have a layout manager for (BoxLayout). All we have left after those are placed is a display area, which we can place where needed using whatever layout manager seems most appropriate. So we need at least two layout managers - one for our numbers, one for our operations. Whether we use one for the display area is a matter of personal choice. In this section, we'll just place it by itself. So, because we are using multiple layout managers, we need to make use of separate panels. Our first step is to setup some space in memory for these panels.
And then we need to instantiate objects for each:
The next step is to set a layout manager on them. Our numbers panel is going to make use of our nifty GridLayout manager to deal with our buttons:
And operations is going to use our BoxLayout manager to deal with the columns:
We'll also remove the call to the null layout manager on our content pane - we'll use the standard BorderLayout manager for placing our panels. Okay, that's our layout managers sorted out - now we just have to add the components we already have to the appropriate panels in the right order:
And then the same for our operations:
And then we add the two panels and the text field to the content pane:
So, we run this to see our lovely layout, and what we get is:
Hrm. Well, it looks mostly right - the number buttons look great, and the display JTextField is in the right place - what a shame though about those operations buttons at the right-hand edge. The reason that they are displayed in such a strange manner is that the various layout managers in use try to balance competing demands for the real estate on your application, and so they will try and fit into the constraints you give them even if that means that a layout manager like GridLayout is much greedier than one like BoxLayout. We can solve this by using a method called setMaximumSize on our components - essentially we set them to very large values and then BoxLayout will attempt to fill up its allotted space accordingly. setMaximumSize takes a parameter of type Dimension, which is a class that indicates the width and height. The constructor we want to use takes two integer parameters, which represent the - you guessed it - width and height. Let's set them both to three hundred:
We then pass this as a parameter to the setMaximumSize method on all of our operations buttons:
And then when we execute our program, we see:
Now, that's much nicer! We can experiment with different values for our BorderLayout to see what effect it has. For example, if we move the numbers panel to the CENTER of our BorderLayout, we get the following interface:
We can keep playing about with this until we get a look we are happy with. We can easily change the positioning of components using layout managers - certainly much easier than we can when using setBounds. If I decide that I would like the operations to appear along the bottom of the screen rather than at the side, all I need to do is change the orientation of my BoxLayout manager:
And then indicate that I want it to be placed in the SOUTH part of my BorderLayout:
By changing a mere two lines of code, we've now altered our application so that it looks like this:
You can call me over-excitable if you like, but I think that's pretty damn cool! We'd have even more control over the placement of these components if we made use of a GridBagLayout, but we're not going to because the benefits we gain are overshadowed by the additional complexity of specifying the interface, and the increased difficulty of making substantial changes to the layout as we just demonstrated above.
We've certainly improved our calculator a fair degree, but there is still further room for improvement. There's still an awful lot of repetition in our actionPerformed, which consists of, amongst other things, ten variations of the following code:
And our init method contains ten variations of:
For all of the usual reasons, repetition of code is something we should be striving to avoid. When we began developing our calculator, all we had covered in the book was event-driven programming. Since then we've added many more tools to our toolkit. Amongst these tools are arrays and ArrayLists, and general string parsing routines. We can fruitfully apply both of these to our application to make it much more succint. To begin with, we setup either an array or ArrayList of buttons. Which one of these we choose is up to us. We know how many buttons we need, and we know that it is extremely unlikely anyone is going to add a new number to the standard ten... we can easily use a standard Array to hold our buttons:
We get rid of all of our existing number JButtons (the ones we have named num1, num2 and so forth) and base all of our functionality on our array. Within our constructor method, we can then resolve twenty lines of code into a single for loop:
Next, we need to place these buttons onto our application... this is a little bit more tricky because we don't add the numbers in sequential order... instead we add them in accordance with the layout of a calculator. We could get away with simply changing the add method calls so that they point to the appropriate array object:
Or... we could be a little more adventurous and do this within a loop of its own. The tricky part is that we need another array that indicates which of the numbers should be added and in which order. The other tricky part is the last three add statements... we should really handle them outside of the loop because it will be too awkward to handle it internally. First, we setup a second array that contains the order in which we want to add the numbers:
And then we adjust our first nine add statements to occur within a loop:
This involves the close interaction of two arrays - we step over each of the numbers in the array numbersToAdd, and then we use the number we pull off of that as the index of our array of JButtons (myNumbers). The numbersToAdd array contains the configuration details we need to properly layout the buttons in their appropriate order. We can apply a similar process to setting up our operations buttons... this is slightly easier because there is a higher degree of repetition and the symbols can be placed sequentially provided we create them in the right order to begin with:
We've now managed to resolve a large construtor method into one that's much more manageable through the use of a few arrays... not bad. However, we also need to change the way our actionPerformed method works, because that too has a huge amount of repetition. Luckily, our arrays will make this much easier too, but we'll have to provide a couple of utility methods that navigate through our arrays to find out what object it was that triggered the event. One of these will take in an object and compare it to each element in our arrays... it'll then tell us what array it was (so we know what kind of functionality must be implemented). Our second method will step over each element in the appropriate array and return the button that triggered the event:
We could further reduce the repetition in these methods by a number of techniques. For one, we could put all of the buttons into a single array, but that makes it difficult to separate out the contextually related functional chunks... we treat numbers buttons differently from the way we treat operations buttons. Alternatively, we could have a 2D array of JButtons, and have the first index relate to whether it is an operations buttons or a numbers button. This would greatly complicate our original setup and make it more difficult to easily manipulate the two largely separate group of buttons. Finally, we could wrap each of the JButtons in a separate class that contains the JButton object and an attribute that indicates whether it is a number button or an operations button. This is a useful technique for very large and complex data structures, but is overkill for this application. We apply these two new methods to our actionPerformed method:
Now we do the same thing for our operations button:
We need a number of comparisons here, but that's only for the functionality that changes dependant on the button... the rest of it is all shared between all of the buttons, which cuts down on code repetition. We leave our equals and clears buttons as they were... they each have specialised functionality that isn't shared, and so we gain nothing by adding them into this structure. Our rewritten actionPerformed method looks like this:
The method looks nothing like it did before, but that's okay... it wasn't a very good method to begin with. Our method has increased complexity, but the result has been a much more maintainable design. It has also lead to a much smaller method footprint... we don't get buried in many variations of the same set of code statements.
Okay, we've changed our calculator quite a bit, but we still have some extra things we can do. One of these is to provide our calculator with a menu so that we can add some helpful options that don't really belong on our main interface. Of course, with a calculator, there aren't many of these - maybe a help option, and perhaps an about option. Since we know how to do this, we should make use of our MaD SkIlLz and implement just such an innovation for our current project. So, first we need a menu bar - we'll create variables for this in the standard way, at the top of our class:
We'll create a menu - usually things like 'help' and 'about' go into a menu called 'help', so we'll create that menu:
And then we need a JMenuItem for each option we are going to have on that menu:
And that's set us up with all the variables we are going to need. Now we need to instantiate the objects assosciated with them, and put them together in our constructor method:
Compile and execute, and you'll see we now have a neat little menu for our application. Alas, it doesn't do anything. We're going to make use of multiple windows to implement the about option - clicking this is going to bring up a new window that details all the information about our bad selves. We're going to implement the help functionality just as a JOptionPane, so let's do that first. When we click on a menu item that we have registered an ActionListener for, it flashes up an action event and so actionPerformed gets called - we need to add a new option into the actionPerformed method, checking the getSource object against our helpItem object:
Okay, perhaps the word 'help' was a little misleading. At least we're flashing up a message when the user clicks on the menu item though. (Of course, you should bear in mind that it's fine to abuse yourself in the programs you develop, but good interface design requires that you be more considerate of real, honest-to-goodness users). For the next item, we are going to create a new class that extends JDialog. All we're going to have within this class is a label and a button, so we could very well do this with another JOptionPane call. However, about boxes are often very detailed, with custom graphics and complex layouts, and we cannot do that with the showMessageDialog method. Our class is going to be very simple - it will extend JDialog, it will have an actionPerformed method for when the button is pressed, and it will have two components. We don't need a main method for this class, because it will never run by itself - it exists only to be instantiated from an already executing application:
We also need to add some event handling code to create the window when we press our about menu item. First of all, we need a variable that will contain the object:
We put this at the top of the actionPerformed method. Then, we add an if statement for executing the code when the object returned by getSource is the aboutItem object:
And there you go - bob is your close relative!
As you can see, using the more powerful user interface techniques we have discussed in previous chapters makes it much easier to develop an adaptable, expandable application. Our previous reliance on setBounds is no more, and we can now concentrate on how we want an application to look rather than on where we want individuals components to go - the distinction is subtle, but very important. The issue of the model-view-controller architecture is also becoming more important as we see areas in which its adoption is useful. The example given here was more for the purposes of reinforcing good practice than showing why it's necessary, but it should be easy to see why we would want to separate applications and applets into separate classes through this architecture. Further ReadingThe following table details further reading on the topic in this chapter, and also any external resources that you may find useful.
|
| Previous | Table of Contents | Next |
© 2004-2006 Michael James Heron