In which we grow the user interface from a label to a table. We achieve this by adding a feature at a time, instead of taking the risk of replacing the whole thing in one go. We discover that some of the choices we made are no longer valid, so we dare to change existing code. We continue to refactor and sense that a more interesting structure is starting to appear.
So far, we’ve been making do with a simple label in the user interface. That’s been effective for helping us clarify the structure of the application and prove that our ideas work, but the next tasks coming up will need more, and the client wants to see something that looks closer to Figure 9.1. We will need to show more price details from the auction and handle multiple items.
The simplest option would be just to add more text into the label, but we think this is the right time to introduce more structure into the user interface. We deferred putting effort into this part of the application, and we think we should catch up now to be ready for the more complex requirements we’re about to implement. We decide to make the obvious choice, given our use of Swing, and replace the label with a table component. This decision gives us a clear direction for where our design should go next.
The Swing pattern for using a JTable
is to associate it with a TableModel
. The table component queries the model for values to present, and the model notifies the table when those values change. In our application, the relationships will look like Figure 15.1. We call the new class SnipersTableModel
because we want it to support multiple Snipers. It will accept updates from the Snipers and provide a representation of those values to its JTable
.
The question is how to get there from here.
We want to get the pieces into place with a minimum of change, without tearing the whole application apart. The smallest step we can think of is to replace the existing implementation (a JLabel
) with a single-cell JTable
, from which we can then grow the additional functionality. We start, of course, with the test, changing our harness to look for a cell in a table, rather than a label.
This generates a failure message because we don’t yet have a table.
We fix this test by retrofitting a minimal JTable
implementation. From now on, we want to speed up our narrative, so we’ll just show the end result. If we were feeling cautious we would first add an empty table, to fix the immediate failure, and then add its contents. It turns out that we don’t have to change any existing classes outside MainWindow
because it encapsulates the act of updating the status. Here’s the new code:
As you can see, the SnipersTableModel
really is a minimal implementation; the only value that can vary is the statusText
. It inherits most of its behavior from the Swing AbstractTableModel
, including the infrastructure for notifying the JTable
of data changes. The result is as ugly as our previous version, except that now the JTable
adds a default column title “A”, as in Figure 15.2. We’ll work on the presentation in a moment.
Our next task is to display information about the Sniper’s position in the auction: item identifier, last auction price, last bid, status. These values come from updates from the auction and the state held within the application. We need to pass them through from their source to the table model and then render them in the display. Of course, we start with the test. Given that this feature should be part of the basic functionality of the application, not separate from what we already have, we update our existing acceptance tests—starting with just one test so we don’t break everything at once. Here’s the new version:
We need the item identifier so the test can look for it in the row, so we make the ApplicationRunner
hold on it when connecting to an auction. We extend the AuctionSniperDriver
to look for a table row that shows the item identifier, last price, last bid, and sniper status.
The test fails because the row has no details, only the status text:
With an acceptance test to show us where we want to get to, we can fill in the steps along the way. As usual, we work “outside-in,” from the event that triggers the behavior; in this case it’s a price update from Southabee’s On-Line. Following along the sequence of method calls, we don’t have to change AuctionMessageTranslator
, so we start by looking at AuctionSniper
and its unit tests.
AuctionSniper
notifies changes in its state to neighbors that implement the SniperListener
interface which, as you might remember, has four callback methods, one for each state of the Sniper. Now we also need to pass in the current state of the Sniper when we notify a listener. We could add the same set of arguments to each method, but that would be duplication; so, we introduce a value type to carry the Sniper’s state. This is an example of “bundling up” that we described in “Value Types” (page 59). Here’s a first cut:
To save effort, we use the reflective builders from the Apache commons.lang
library to implement equals()
, hashCode()
, and toString()
in the new class. We could argue that we’re being premature with these features, but in practice we’ll need them in a moment when we write our unit tests.
We don’t want to break all the tests at once, so we start with an easy one. In this test there’s no history, all we have to do in the Sniper is construct a SniperState
from information available at the time and pass it to the listener.
Then we make the test pass:
To get the code to compile, we also add the state argument to the sniperBidding()
method in SniperStateDisplayer
, which implements SniperListener
, but don’t yet do anything with it.
The one significant change is that the Sniper needs access to the item identifier so it can construct a SniperState
. Given that the Sniper doesn’t need this value for any other reason, we could have kept it in the SniperStateDisplayer
and added it in when an event passes through, but we think it’s reasonable that the Sniper has access to this information. We decide to pass the identifier into the AuctionSniper
constructor; it’s available at the time, and we don’t want to get it from the Auction
object which may have its own form of identifier for an item.
We have one other test that refers to the sniperBidding()
method, but only as an “allowance.” We use a matcher that says that, since it’s only supporting the interesting part of the test, we don’t care about the contents of the state object.
allowing(sniperListener).sniperBidding(with(any(SniperState.class)));
We’ll take larger steps for the next task—presenting the state in the user interface—as there are some new moving parts, including a new unit test. The first version of the code will be clumsier than we would like but, as you’ll soon see, there’ll be interesting opportunities for cleaning up.
Our very first step is to pass the new state parameter, which we’ve been ignoring, through MainWindow
to a new method in SnipersTableModel
. While we’re at it, we notice that just passing events through MainWindow
isn’t adding much value, so we make a note to deal with that later.
To get the new values visible on screen, we need to fix SnipersTableModel
so that it makes them available to its JTable
, starting with a unit test. We take a small design leap by introducing a Java enum
to represent the columns in the table—it’s more meaningful than just using integers.
The table model needs to do two things when its state changes: hold onto the new values and notify the table that they’ve changed. Here’s the test:
We attach a mock implementation of TableModelListener
to the model. This is one of the few occasions where we break our rule “Only Mock Types That You Own” (page 69) because the table model design fits our design approach so well.
We add a first test to make sure we’re rendering the right number of columns. Later, we’ll do something about the column titles.
This expectation checks that we notify any attached JTable
that the contents have changed.
This is the event that triggers the behavior we want to test.
We assert that the table model returns the right values in the right columns. We hard-code the row number because we’re still assuming that there is only one.
There’s no specific equals()
method on TableModelEvent
, so we use a matcher that will reflectively compare the property values of any event it receives against an expected example. Again, we hard-code the row number.
After the usual red/green cycle, we end up with an implementation that looks like this:
We provide an initial SniperState
with “empty” values so that the table model will work before the Sniper has connected.
For the dimensions, we just return the numbers of values in Column
or a hard-coded row count.
This method unpacks the value to return depending on the column that is specified. The advantage of using an enum
is that the compiler will help with missing branches in the switch
statement (although it still insists on a default case). We’re not keen on using switch
, as it’s not object-oriented, so we’ll keep an eye on this too.
The Sniper-specific method. It sets the fields and then triggers its clients to update.
If we run our acceptance test again, we find we’ve made some progress. It’s gone past the Bidding
check and now fails because the last price column, “B”, has not yet been updated. Interestingly, the status column shows Winning
correctly, because that code is still working.
and the proof is in Figure 15.3.
We have one kind of Sniper event, Bidding
, that we can handle all the way through our application. Now we have to do the same thing to Winning
, Lost
, and Won
.
Frankly, that’s just dull. There’s too much repetitive work needed to make the other cases work—setting them up in the Sniper and passing them through the layers. Something’s wrong with the design. We toss this one around for a while and eventually notice that we would have a subtle duplication in our code if we just carried on. We would be splitting the transmission of the Sniper state into two mechanisms: the choice of listener method and the state object. That’s one mechanism too many.
We realize that we could collapse our events into one notification that includes the prices and the Sniper status. Of course we’re transmitting the same information whichever mechanism we choose—but, looking at the chain of methods calls, it would be simpler to have just one method and pass everything through in SniperState
.
Having made this choice, can we do it cleanly without ripping up the metaphorical floorboards? We believe we can—but first, one more clarification.
We want to start by creating a type to represent the Sniper’s status (winning, losing, etc.) in the auction, but the terms “status” and “state” are too close to distinguish easily. We kick around some vocabulary and eventually decide that a better term for what we now call SniperState
would be SniperSnapshot
: a description of the Sniper’s relationship with the auction at this moment in time. This frees up the name SniperState
to describe whether the Sniper is winning, losing, and so on, which matches the terminology of the state machine we drew in Figure 9.3 on page 78. Renaming the SniperState
takes a moment, and we change the value in Column
from SNIPER_STATUS
to SNIPER_STATE
.
Our first step is to take the method that does most of what we want, sniperBidding()
, and rework it to fit our new scheme. We create an enum
that takes the SniperState
name we’ve just freed up and add it to SniperSnapshot
; we take the sniperState
field out of the method arguments; and, finally, we rename the method to sniperStateChanged()
to match its intended new role. We push the changes through to get the following code:
In the table model, we use simple indexing to translate the enum
into displayable text.
We make some minor changes to the test code, to get it through the compiler, plus one more interesting adjustment. You might remember that we wrote an expectation clause that ignored the details of the SniperState
:
allowing(sniperListener).sniperBidding(with(any(SniperState.class)));
We can no longer rely on the choice of method to distinguish between different events, so we have to dig into the new SniperSnapshot
object to make sure we’re matching the right one. We rewrite the expectation with a custom matcher that checks just the state:
Now we’re in a position to feed the missing price to the user interface, which means changing the listener call from sniperWinning()
to sniperStateChanged()
so that the listener will receive the value in a SniperSnapshot
. We start by changing the test to expect the different listener call, and to trigger the event by calling currentPrice()
twice: once to force the Sniper to bid, and again to tell the Sniper that it’s winning.
We change AuctionSniper
to retain its most recent values by holding on to the last snapshot. We also add some helper methods to SniperSnapshot
, and find that our implementation starts to simplify.
We remove sniperWinning()
from SniperListener
and its implementations, and add a value for winning to SnipersTableModel.STATUS_TEXT
.
Now, the end-to-end test passes.
This works, but we still have two notification methods in SniperListener
left to convert before we can say we’re done: sniperWon()
and sniperLost()
. Again, we replace these with sniperStateChanged()
and add two new values to SniperState
.
Plugging these changes in, we find that the code simplifies further. We drop the isWinning
field from the Sniper and move some decision-making into SniperSnapshot
, which will know whether the Sniper is winning or losing, and SniperState
.
We note, with smug satisfaction, that AuctionSniper
no longer refers to SniperState
; it’s hidden in SniperSnapshot
.
We would have preferred to use a field to implement whenAuctionClosed()
. It turns out that the compiler cannot handle an enum
referring to one of its values which has not yet been defined, so we have to put up with the syntax noise of overridden methods.
We remove the accessor setStatusText()
that sets the state display string in SnipersTableModel
, as everything uses sniperStatusChanged()
now. While we’re at it, we move the description string constants for the Sniper state over from MainWindow
.
The helper method, textFor()
, helps with readability, and we also use it to get hold of the display strings in tests since the constants are no longer accessible from MainWindow
.
We still have a couple of things to do before we finish this task. We start by removing all the old test code that didn’t specify the price details, filling in the expected values in the tests as required. The tests still run.
The next change is to replace the switch
statement which is noisy, not very object-oriented, and includes an unnecessary default:
clause just to satisfy the compiler. It’s served its purpose, which was to get us through the previous coding stage. We add a method to Column
that will extract the appropriate field:
and the code in SnipersTableModel
becomes negligible:
Of course, we write a unit test for Column
. It may seem unnecessary now, but it will protect us when we make changes and forget to keep the column mapping up to date.
Finally, we see that we have some forwarding calls that we no longer need. MainWindow
just forwards the update and SniperStateDisplayer
has collapsed to almost nothing.
SniperStateDisplayer
still serves a useful purpose, which is to push updates onto the Swing event thread, but it no longer does any translation between domains in the code, and the call to MainWindow
is unnecessary. We decide to simplify the connections by making SnipersTableModel
implement SniperListener
. We change SniperStateDisplayer
to be a Decorator and rename it to SwingThreadSniperListener
, and we rewire Main
so that the Sniper connects to the table model rather than the window.
The new structure looks like Figure 15.4.
To make the user interface presentable, we need to fill in the column titles which, as we saw in Figure 15.3, are still missing. This isn’t difficult, since most of the implementation is built into Swing’s TableModel
. As always, we start with the acceptance test. We add extra validation to AuctionSniperDriver
that will be called by the method in ApplicationRunner
that starts up the Sniper. For good measure, we throw in a check for the application’s displayed title.
The test fails:
Swing allows a JTable
to query its TableModel
for the column headers, which is the mechanism we’ve chosen to use. We already have Column
to represent the columns, so we extend this enum
by adding a field for the header text which we reference in SnipersTableModel
.
All we really need to check in the unit test for SniperTablesModel
is the link between a Column
value and a column name, but it’s so simple to iterate that we check them all:
The acceptance test passes, and we can see the result in Figure 15.5.
There’s more we should do, such as set up borders and text alignment, to tune the user interface. We might do that by associating CellRenderer
s with each Column
value, or perhaps by introducing a TableColumnModel
. We’ll leave those as an exercise for the reader, since they don’t add any more insight into our development process.
In the meantime, we can cross off one more task from our to-do list: Figure 15.6.
SnipersTableModel
has one responsibility: to represent the state of our bidding in the user interface. It follows the heuristic we described in “No And’s, Or’s, or But’s” (page 51). We’ve seen too much user interface code that is brittle because it has business logic mixed in. In this case, we could also have made the model responsible for deciding whether to bid (“because that would be simpler”), but that would make it harder to respond when either the user interface or the bidding policy change. It would be harder to even find the bidding policy, which is why we isolated it in AuctionSniper
.
In this chapter we repeatedly used the practice of adding little slices of behavior all the way through the system: replace a label with a table, get that working; show the Sniper bidding, get that working; add the other values, get that working. In all of these cases, we’ve figured out where we want to get to (always allowing that we might discover a better alternative along the way), but we want to avoid ripping the application apart to get there. Once we start a major rework, we can’t stop until it’s finished, we can’t check in without branching, and merging with rest of the team is harder. There’s a reason that surgeons prefer keyhole surgery to opening up a patient—it’s less invasive and cheaper.
We have a well-developed sense of the value of our own time. We keep an eye out for activities that don’t seem to be making the best of our (doubtless significant) talents, such as boiler-plate copying and adapting code: if we had the right abstraction, we wouldn’t have to bother. Sometimes this just has to be done, especially when working with existing code—but there are fewer excuses when it’s our own. Deciding when to change the design requires a good sense for tradeoffs, which implies both sensitivity and technical maturity: “I’m about to repeat this code with minor variations, that seems dull and wasteful” as against “This may not be the right time to rework this, I don’t understand it yet.”
We don’t have a simple, reproducible technique here; it requires skill and experience. Developers should have a habit of reflecting on their activity, on the best way to invest their time for the rest of a coding session. This might mean carrying on exactly as before, but at least they’ll have thought about it.
When the facts change, I change my mind. What do you do, sir?
—John Maynard Keynes
During this chapter, we renamed several features in the code. In many development cultures, this is viewed as a sign of weakness, as an inability to do a proper job. Instead, we think this is an essential part of our development process. Just as we learn more about what the structure should be by using the code we’ve written, we learn more about the names we’ve chosen when we work with them. We see how the type and method names fit together and whether the concepts are clear, which stimulates the discovery of new ideas. If the name of a feature isn’t right, the only smart thing to do is change it and avoid countless hours of confusion for all who will read the code later.
Examples in books, such as this one, tend to read as if there was an inevitability about the solution. That’s partly because we put effort into making the narrative flow, but it’s also because presenting one solution tends to drive others out of the reader’s consciousness. There are other variations we could have considered, some of which might even resurface as the example develops.
For example, we could argue that AuctionSniper
doesn’t need to know whether it’s won or lost the auction—just whether it should bid or not. At present, the only part of the application that cares about winning is the user interface, and it would certainly simplify the AuctionSniper
and SniperSnapshot
if we moved that decision away from them. We won’t do that now, because we don’t yet know if it’s the right choice, but we find that kicking around design options sometimes leads to much better solutions.
3.141.46.130