In which we introduce a stop price so we don’t bid infinitely, which means we can now be losing an auction that hasn’t yet closed. We add a new field to the user interface and push it through to the Sniper. We realize we should have created an Item
type much earlier.
So far the functionality has been prioritized to attract potential customers by giving them a sense of what the application will look like. We can show items being added and some features of sniping. It’s not a very useful application because, amongst other things, there’s no upper limit for bidding on an item—it could be very expensive to deploy.
This is a common pattern when using Agile Development techniques to work on a new project. The team is flexible enough to respond to how the needs of the sponsors change over time: at the beginning, the emphasis might be on proving the concept to attract enough support to continue; later, the emphasis might be on implementing enough functionality to be ready to deploy; later still, the emphasis might change to providing more options to support a wider range of users.
This dynamic is very different from both a fixed design approach, where the structure of the development has to be approved before work can begin, and a code-and-fix approach, where the system might be initially successful but not resilient enough to adapt to its changing role.
Our next most pressing task (especially after recent crises in the financial markets) is to be able to set an upper limit, the “stop price,” for our bid for an item.
With the introduction of a stop price, it’s possible for a Sniper to be losing before the auction has closed. We could implement this by just marking the Sniper as Lost
when it hits its stop price, but the users want to know the final price when the auction has finished after they’ve dropped out, so we model this as an extra state. Once a Sniper has been outbid at its stop price, it will never be able to win, so the only option left is to wait for the auction to close, accepting updates of any new (higher) prices from other bidders.
We adapt the state machine we drew in Figure 9.3 to include the new transitions. The result is Figure 18.1.
Of course we start with a failing test. We won’t go through all the cases here, but this example will take us through the essentials. First, we write an end-to-end test to describe the new feature. It shows a scenario where our Sniper bids for an item but loses because it bumps into its stop price, and other bidders continue until the auction closes.
This test introduces two new methods into our test infrastructure, which we need to fill in to get through the compiler. First, startBiddingWithStopPrice()
passes the new stop price value through the ApplicationRunner
to the AuctionSniperDriver
.
This implies that we need a new input field in the user interface for the stop price, so we create a constant to identify it in MainWindow
(we’ll fill in the component itself soon). We also need to support our existing tests which do not have a stop price, so we change them to use Integer.MAX_VALUE
to represent no stop price at all.
The other new method in ApplicationRunner
is hasShownSniperIsLosing()
, which is the same as the other checking methods, except that it uses a new Losing
value in SniperState
:
and, to complete the loop, we add a value to the display text in SnipersTableModel
:
The failure message says that we have no stop price field:
Now we have a failing end-to-end test that describes our intentions for the feature, so we can implement it.
To make any progress, we must add a component to the user interface that will accept a stop price. Our current design, which we saw in Figure 16.2, has only a field for the item identifier but we can easily adjust it to take a stop price in the top bar.
For our implementation, we will add a JFormattedTextField
for the stop price that is constrained to accept only integer values, and a couple of labels. The new top bar looks like Figure 18.2.
We get the test failure we expect, which is that the Sniper is not losing because it continues to bid:
To make this feature work, we need to pass the stop price from the user interface to the AuctionSniper
, which can then use it to limit further bidding. The chain starts when MainWindow
notifies its UserRequestListener
using:
void joinAuction(String itemId);
The obvious thing to do is to add a stopPrice
argument to this method and to the rest of the chain of calls, until it reaches the AuctionSniper
class. We want to make a point here, so we’ll force a slightly different approach to propagating the new value.
Another way to look at it is that the user interface constructs a description of the user’s “policy” for the Sniper’s bidding on an item. So far this has only included the item’s identifier (“bid on this item”), but now we’re adding a stop price (“bid up to this amount on this item”) so there’s more structure.
We want to make this structure explicit, so we create a new class, Item
. We start with a simple value that just carries the identifier and stop price as public immutable fields; we can move behavior into it later.
Introducing the Item
class is an example of budding off that we described in “Value Types” (page 59). It’s a placeholder type that we use to identify a concept and that gives us somewhere to attach relevant new features as the code grows.
We push Item
into the code and see what breaks, starting with UserRequestListener
:
First we fix MainWindowTest
, the integration test we wrote for the Swing implementation in Chapter 16. The language is already beginning to shift. In the previous version of this test, the probe variable was called buttonProbe
, which describes the structure of the user interface. That doesn’t make sense any more, so we’ve renamed it itemProbe
, which describes a collaboration between MainWindow
and its neighbors.
We make this test pass by extracting the stop price value within MainWindow
.
This pushes Item
into SniperLauncher
which, in turn, pushes it through to its dependent types such as AuctionHouse
and AuctionSniper
. We fix the compilation errors and make all the tests pass again—except for the outstanding end-to-end test which we have yet to implement.
We’ve now made explicit another concept in the domain. We realize that an item’s identifier is only one part of how a user bids in an auction. Now the code can tell us exactly where decisions are made about bidding choices, so we don’t have to follow a chain of method calls to see which strings are relevant.
The last step to finish the task is to make the AuctionSniper
observe the stop price we’ve just passed to it and stop bidding. In practice, we can ensure that we’ve covered everything by writing unit tests for each of the new state transitions drawn in Figure 18.1. Our first test triggers the Sniper to start bidding and then announces a bid outside its limit—the stop price is set to 1234
. We’ve also extracted a common expectation into a helper method.1
1. jMock allows checking()
to be called multiple times within a test.
The other tests are similar:
We change AuctionSniper
, with supporting features in SniperSnapshot
and Item
, to make the test pass:
The end-to-end tests pass and we can cross the feature off our list, Figure 18.3.
It looks like we’re making significant changes again to the user interface at a late stage in our development. Shouldn’t we have seen this coming? This is an active topic for discussion in the Agile User Experience community and, as always, the answer is “it depends, but you have more flexibility than you might think.”
In truth, for a simple application like this it would make sense to work out the user interface in more detail at the start, to make sure it’s usable and coherent. That said, we also wanted to make a point that we can respond to changing needs, especially if we structure our tests and code so that they’re flexible, not a dead weight. We all know that requirements will change, especially once we put our application into production, so we should be able to respond.
Some presentations of TDD appear to suggest that it supersedes all previous software design techniques. We think TDD works best when it’s based on skill and judgment acquired from as wide an experience as possible—which includes taking advantage of older techniques and formats (we hope we’re not being too controversial here).
State transition diagrams are one example of taking another view. We regularly come across teams that have never quite figured out what the valid states and transitions are for key concepts in their domain, and applying this simple formalism often means we can clean up a lucky-dip of snippets of behavior scattered across the code. What’s nice about state transitions diagrams is that they map directly onto tests, so we can show that we’ve covered all the possibilities.
The trick is to understand and use other modeling techniques for support and guidance, not as an end in themselves—which is how they got a bad name in the first place. When we’re doing TDD and we’re uncertain what to do, sometimes stepping back and opening a pack of index cards, or sketching out the interactions, can help us regain direction.
The string is a stark data structure and everywhere it is passed there is much duplication of process. It is a perfect vehicle for hiding information.
—Alan Perlis
Looking back, we wish we’d created the Item
type earlier, probably when we extracted UserRequestListener
, instead of just using a String
to represent the thing a Sniper bids for. Had we done so, we could have added the stop price to the existing Item
class, and it would have been delivered, by definition, to where it was needed.
We might also have noticed sooner that we do not want to index our table on item identifier but on an Item
, which would open up the possibility of trying multiple policies in a single auction. We’re not saying that we should have designed more speculatively for a need that hasn’t been proved. Rather, when we take the trouble to express the domain clearly, we often find that we have more options.
It’s often better to define domain types to wrap not only String
s but other built-in types too, including collections. All we have to do is remember to apply our own advice. As you see, sometimes we forget.
18.118.37.254