Tài liệu Growing Object-Oriented Software, Guided by Tests- P4 pdf

50 388 1
Tài liệu Growing Object-Oriented Software, Guided by Tests- P4 pdf

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

ptg developers shouldn’t be shy about creating new types. We think Main still does too much, but we’re not yet sure how best to break it up. We decide to push on and see where the code takes us. Sending a Bid An Auction Interface The next step is to have the Sniper send a bid to the auction, so who should the Sniper talk to? Extending the SniperListener feels wrong because that relationship is about tracking what’s happening in the Sniper, not about making external commitments. In the terms defined in “Object Peer Stereotypes” (page 52), SniperListener is a notification, not a dependency. After the usual discussion, we decide to introduce a new collaborator, an Auction . Auction and SniperListener represent two different domains in the application: Auction is about financial transactions, it accepts bids for items in the market; and SniperListener is about feedback to the application, it reports changes to the current state of the Sniper. The Auction is a dependency, for a Sniper cannot function without one, whereas the SniperListener , as we discussed above, is not. Introducing the new interface makes the design look like Figure 13.2. Figure 13.2 Introducing Auction The AuctionSniper Bids Now we’re ready to start bidding. The first step is to implement the response to a Price event, so we start by adding a new unit test for the AuctionSniper . It says that the Sniper, when it receives a Price update, sends an incremented bid to the auction. It also notifies its listener that it’s now bidding, so we add a sniperBidding() method. We’re making an implicit assumption that the Auction knows which bidder the Sniper represents, so the Sniper does not have to pass in that information with the bid. Chapter 13 The Sniper Makes a Bid 126 From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg public class AuctionSniperTest { private final Auction auction = context.mock(Auction.class); private final AuctionSniper sniper = new AuctionSniper(auction, sniperListener); […] @Test public void bidsHigherAndReportsBiddingWhenNewPriceArrives() { final int price = 1001; final int increment = 25; context.checking(new Expectations() {{ one(auction).bid(price + increment); atLeast(1).of(sniperListener).sniperBidding(); }}); sniper.currentPrice(price, increment); } } The failure report is: not all expectations were satisfied expectations: ! expected once, never invoked: auction.bid(<1026>) ! expected at least 1 time, never invoked: sniperListener.sniperBidding() what happened before this: nothing! When writing the test, we realized that we don’t actually care if the Sniper notifies the listener more than once that it’s bidding; it’s just a status update, so we use an atLeast(1) clause for the listener’s expectation. On the other hand, we do care that we send a bid exactly once, so we use a one() clause for its ex- pectation. In practice, of course, we’ll probably only call the listener once, but this loosening of the conditions in the test expresses our intent about the two relationships. The test says that the listener is a more forgiving collaborator, in terms of how it’s called, than the Auction . We also retrofit the atLeast(1) clause to the other test method. How Should We Describe Expected Values? We’ve specified the expected bid value by adding the price and increment .There are different opinions about whether test values should just be literals with “obvious” values, or expressed in terms of the calculation they represent. Writing out the calculation may make the test more readable but risks reimplementing the target code in the test, and in some cases the calculation will be too complicated to repro- duce. Here, we decide that the calculation is so trivial that we can just write it into the test. 127 Sending a Bid From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg jMock Expectations Don’t Need to Be Matched in Order This is our first test with more than one expectation, so we’ll point out that the order in which expectations are declared does not have to match the order in which the methods are called in the code. If the calling order does matter, the expectations should include a sequence clause, which is described in Appendix A. The implementation to make the test pass is simple. public interface Auction { void bid(int amount); } public class AuctionSniper implements AuctionEventListener { […] private final SniperListener sniperListener; private final Auction auction; public AuctionSniper(Auction auction, SniperListener sniperListener) { this.auction = auction; this.sniperListener = sniperListener; } public void currentPrice(int price, int increment) { auction.bid(price + increment); sniperListener.sniperBidding(); } } Successfully Bidding with the AuctionSniper Now we have to fold our new AuctionSniper back into the application. The easy part is displaying the bidding status, the (slightly) harder part is sending the bid back to the auction. Our first job is to get the code through the compiler. We implement the new sniperBidding() method on Main and, to avoid having code that doesn’t compile for too long, we pass the AuctionSniper a null implementation of Auction . Chapter 13 The Sniper Makes a Bid 128 From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg public class Main implements SniperListener { […] private void joinAuction(XMPPConnection connection, String itemId) throws XMPPException { Auction nullAuction = new Auction() { public void bid(int amount) {} }; disconnectWhenUICloses(connection); Chat chat = connection.getChatManager().createChat( auctionId(itemId, connection), new AuctionMessageTranslator(new AuctionSniper(nullAuction, this))); this.notToBeGCd = chat; chat.sendMessage(JOIN_COMMAND_FORMAT); } public void sniperBidding() { SwingUtilities.invokeLater(new Runnable() { public void run() { ui.showStatus(MainWindow.STATUS_BIDDING); } }); } } So, what goes in the Auction implementation? It needs access to the chat so it can send a bid message. To create the chat we need a translator, the translator needs a Sniper, and the Sniper needs an auction. We have a dependency loop which we need to break. Looking again at our design, there are a couple of places we could intervene, but it turns out that the ChatManager API is misleading. It does not require a MessageListener to create a Chat , even though the createChat() methods imply that it does. In our terms, the MessageListener is a notification; we can pass in null when we create the Chat and add a MessageListener later. Expressing Intent in API We were only able to discover that we could pass null as a MessageListener because we have the source code to the Smack library. This isn’t clear from the API because, presumably, the authors wanted to enforce the right behavior and it’s not clear why anyone would want a Chat without a listener. An alternative would have been to provide equivalent creation methods that don’t take a listener, but that would lead to API bloat. There isn’t an obvious best approach here, except to note that including well-structured source code with the distribution makes libraries much easier to work with. 129 Sending a Bid From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg Now we can restructure our connection code and use the Chat to send back a bid. public class Main implements SniperListener { […] private void joinAuction(XMPPConnection connection, String itemId) throws XMPPException { disconnectWhenUICloses(connection); final Chat chat = connection.getChatManager().createChat(auctionId(itemId, connection), null); this.notToBeGCd = chat; Auction auction = new Auction() { public void bid(int amount) { try { chat.sendMessage(String.format(BID_COMMAND_FORMAT, amount)); } catch (XMPPException e) { e.printStackTrace(); } } }; chat.addMessageListener( new AuctionMessageTranslator(new AuctionSniper(auction, this))); chat.sendMessage(JOIN_COMMAND_FORMAT); } } Null Implementation A null implementation is similar to a null object [Woolf98]: both are implementations that respond to a protocol by not doing anything—but the intention is different. A null object is usually one implementation amongst many, introduced to reduce complexity in the code that calls the protocol. We define a null implementation as a temporary empty implementation, introduced to allow the programmer to make progress by deferring effort and intended to be replaced. The End-to-End Tests Pass Now the end-to-end tests pass: the Sniper can lose without making a bid, and lose after making a bid. We can cross off another item on the to-do list, but that includes just catching and printing the XMPPException . Normally, we regard this as a very bad practice but we wanted to see the tests pass and get some structure into the code—and we know that the end-to-end tests will fail anyway if there’s a problem sending a message. To make sure we don’t forget, we add another to-do item to find a better solution, Figure 13.3. Chapter 13 The Sniper Makes a Bid 130 From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg Figure 13.3 One step forward Tidying Up the Implementation Extracting XMPPAuction Our end-to-end test passes, but we haven’t finished because our new implemen- tation feels messy. We notice that the activity in joinAuction() crosses multiple domains: managing chats, sending bids, creating snipers, and so on. We need to clean up. To start, we notice that we’re sending auction commands from two different levels, at the top and from within the Auction . Sending commands to an auction sounds like the sort of thing that our Auction object should do, so it makes sense to package that up together. We add a new method to the interface, extend our anonymous implementation, and then extract it to a (temporarily) nested class—for which we need a name. The distinguishing feature of this imple- mentation of Auction is that it’s based on the messaging infrastructure, so we call our new class XMPPAuction . 131 Tidying Up the Implementation From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg public class Main implements SniperListener { […] private void joinAuction(XMPPConnection connection, String itemId) { disconnectWhenUICloses(connection); final Chat chat = connection.getChatManager().createChat(auctionId(itemId, connection), null); this.notToBeGCd = chat; Auction auction = new XMPPAuction(chat); chat.addMessageListener( new AuctionMessageTranslator(new AuctionSniper(auction, this))); auction.join(); } public static class XMPPAuction implements Auction { private final Chat chat; public XMPPAuction(Chat chat) { this.chat = chat; } public void bid(int amount) { sendMessage(format(BID_COMMAND_FORMAT, amount)); } public void join() { sendMessage(JOIN_COMMAND_FORMAT); } private void sendMessage(final String message) { try { chat.sendMessage(message); } catch (XMPPException e) { e.printStackTrace(); } } } } We’re starting to see a clearer model of the domain. The line auction.join() expresses our intent more clearly than the previous detailed implementation of sending a string to a chat. The new design looks like Figure 13.4 and we promote XMPPAuction to be a top-level class. We still think joinAuction() is unclear, and we’d like to pull the XMPP-related detail out of Main , but we’re not ready to do that yet. Another point to keep in mind. Chapter 13 The Sniper Makes a Bid 132 From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg Figure 13.4 Closing the loop with an XMPPAuction Extracting the User Interface The other activity in Main is implementing the user interface and showing the current state in response to events from the Sniper. We’re not really happy that Main implements SniperListener ; again, it feels like mixing different responsibil- ities (starting the application and responding to events). We decide to extract the SniperListener behavior into a nested helper class, for which the best name we can find is SniperStateDisplayer . This new class is our bridge between two do- mains: it translates Sniper events into a representation that Swing can display, which includes dealing with Swing threading. We plug an instance of the new class into the AuctionSniper . public class Main { // doesn't implement SniperListener private MainWindow ui; private void joinAuction(XMPPConnection connection, String itemId) { disconnectWhenUICloses(connection); final Chat chat = connection.getChatManager().createChat(auctionId(itemId, connection), null); this.notToBeGCd = chat; Auction auction = new XMPPAuction(chat); chat.addMessageListener( new AuctionMessageTranslator( connection.getUser(), new AuctionSniper(auction, new SniperStateDisplayer()))); auction.join(); } […] 133 Tidying Up the Implementation From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg public class SniperStateDisplayer implements SniperListener { public void sniperBidding() { showStatus(MainWindow.STATUS_BIDDING); } public void sniperLost() { showStatus(MainWindow.STATUS_LOST); } public void sniperWinning() { showStatus(MainWindow.STATUS_WINNING); } private void showStatus(final String status) { SwingUtilities.invokeLater(new Runnable() { public void run() { ui.showStatus(status); } }); } } } Figure 13.5 shows how we’ve reduced Main so much that it no longer partici- pates in the running application (for clarity, we’ve left out the WindowAdapter that closes the connection). It has one job which is to create the various compo- nents and introduce them to each other. We’ve marked MainWindow as external, even though it’s one of ours, to represent the Swing framework. Figure 13.5 Extracting SniperStateDisplayer Chapter 13 The Sniper Makes a Bid 134 From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg Tidying Up the Translator Finally, we fulfill our promise to ourselves and return to the AuctionMessageTranslator . We start trying to reduce the noise by adding constants and static imports, with some helper methods to reduce duplication. Then we realize that much of the code is about manipulating the map of name/value pairs and is rather procedural. We can do a better job by extracting an inner class, AuctionEvent , to encapsulate the unpacking of the message con- tents. We have confidence that we can refactor the class safely because it’s protected by its unit tests. public class AuctionMessageTranslator implements MessageListener { private final AuctionEventListener listener; public AuctionMessageTranslator(AuctionEventListener listener) { this.listener = listener; } public void processMessage(Chat chat, Message message) { AuctionEvent event = AuctionEvent.from(message.getBody()); String eventType = event.type(); if ("CLOSE".equals(eventType)) { listener.auctionClosed(); } if ("PRICE".equals(eventType)) { listener.currentPrice(event.currentPrice(), event.increment()); } } private static class AuctionEvent { private final Map<String, String> fields = new HashMap<String, String>(); public String type() { return get("Event"); } public int currentPrice() { return getInt("CurrentPrice"); } public int increment() { return getInt("Increment"); } private int getInt(String fieldName) { return Integer.parseInt(get(fieldName)); } private String get(String fieldName) { return fields.get(fieldName); } private void addField(String field) { String[] pair = field.split(":"); fields.put(pair[0].trim(), pair[1].trim()); } static AuctionEvent from(String messageBody) { AuctionEvent event = new AuctionEvent(); for (String field : fieldsIn(messageBody)) { event.addField(field); } return event; } static String[] fieldsIn(String messageBody) { return messageBody.split(";"); } } } 135 Tidying Up the Implementation From the Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. [...]... Split-Merge on www.verypdf.com to remove this watermark Lee Bogdanoff This page intentionally left blank From Please purchase PDF Split-Merge on www.verypdf.com to remove the Library of Lee Bogdanoff this watermark Chapter 14 The Sniper Wins the Auction In which we add another feature to our Sniper and let it win an auction We introduce the concept of state to the Sniper which we test by listening to its... incrementally, by looking for features in classes that either go together or don’t Of course we’re influenced by our experience of working on similar codebases, but we’re trying hard to follow what the code is telling us instead of imposing our preconceptions Sometimes, when we do this, we find that the domain takes us in the most surprising directions From the Library of Please purchase PDF Split-Merge... discoveries of test-driven development is just how fine-grained our development steps can be From Please purchase PDF Split-Merge on www.verypdf.com to remove the Library of Lee Bogdanoff this watermark Emergent Design 137 Emergent Design What we hope is becoming clear from this chapter is how we’re growing a design from what looks like an unpromising start We alternate, more or less, between adding features... conclusion—sniperWinsAnAuctionByBiddingHigher() Here’s the test, with the new features highlighted: 139 Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark From the Library of Lee Bogdanoff 140 Chapter 14 The Sniper Wins the Auction public class AuctionSniperEndToEndTest { […] @Test public void sniperWinsAnAuctionByBiddingHigher() throws Exception { auction.startSellingItem(); application.startBiddingIn(auction);... would have to know something about how bidders are identified by the auction, with a risk of pulling in XMPP details that we’ve been careful to keep separate To decide whether it’s winning, the only thing the Sniper needs to know when a price arrives is, did this price come from me? This is a From Please purchase PDF Split-Merge on www.verypdf.com to remove the Library of Lee Bogdanoff this watermark... SniperStateDisplayer()))); auction.join(); } From Please purchase PDF Split-Merge on www.verypdf.com to remove the Library of Lee Bogdanoff this watermark The Sniper Has More to Say 143 The Sniper Has More to Say Our immediate end-to-end test failure tells us that we should make the user interface show when the Sniper is winning Our next implementation step is to follow through by fixing the AuctionSniper to interpret the... before adding the additional step that takes the Sniper to the Won state: Figure 14.2 A Sniper bids, then loses From Please purchase PDF Split-Merge on www.verypdf.com to remove the Library of Lee Bogdanoff this watermark The Sniper Acquires Some State 145 We start by revisiting an existing unit test and adding a new one These tests will pass with the current implementation; they’re there to ensure... […] public void sniperWon() { showStatus(MainWindow.STATUS_WON); } } From the Library of Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark Lee Bogdanoff 148 Chapter 14 The Sniper Wins the Auction Having previously made a fuss about PriceSource, are we being inconsistent here by using a boolean for isWinning? Our excuse is that we did try an enum for the Sniper state, but it just... the existing structure The next tasks we have to implement will shake this up From Please purchase PDF Split-Merge on www.verypdf.com to remove the Library of Lee Bogdanoff this watermark Chapter 15 Towards a Real User Interface 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... level windows contained 1 JFrame (with name "Auction Sniper Main" and showing on screen) contained 0 JTable () From Please purchase PDF Split-Merge on www.verypdf.com to remove the Library of Lee Bogdanoff this watermark A More Realistic Implementation 151 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 . purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg Emergent Design What we hope is becoming clear from this chapter is how we’re growing. Library of Lee Bogdanoff Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark. ptg We start by revisiting an existing unit test and

Ngày đăng: 14/12/2013, 21:15

Từ khóa liên quan

Tài liệu cùng người dùng

  • Đang cập nhật ...

Tài liệu liên quan