User Tools

Site Tools


at-m42:casestudies:cs04

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
at-m42:casestudies:cs04 [2009/04/14 11:26] eechrisat-m42:casestudies:cs04 [2011/01/14 12:59] (current) – external edit 127.0.0.1
Line 8: Line 8:
 The adventure game first appeared in [[at-m42:lecture4#case_studyan_adventure_game|Case Study 1]] in [[at-m42:lecture4|Lecture 4]]. There we showed how ''List''s and ''Map''s can be combined to produce data structures to manage the book-keeping required in a game. There, the data maintained in the collections where simple strings. in [[at-m42:casestudies:cs02|Case Study 2]] we enhanced the capabilities of the system by making use of procedural code and closures. A text-based menu was introduced to support user interaction. later in [[at-m42:casestudies:cs03|Case study 3]], we used objects with more interesting state information and behaviours to represent the game, the players and items. We also removed any input/output responsibilities from them and introduces another //action// class for this purpose.  The adventure game first appeared in [[at-m42:lecture4#case_studyan_adventure_game|Case Study 1]] in [[at-m42:lecture4|Lecture 4]]. There we showed how ''List''s and ''Map''s can be combined to produce data structures to manage the book-keeping required in a game. There, the data maintained in the collections where simple strings. in [[at-m42:casestudies:cs02|Case Study 2]] we enhanced the capabilities of the system by making use of procedural code and closures. A text-based menu was introduced to support user interaction. later in [[at-m42:casestudies:cs03|Case study 3]], we used objects with more interesting state information and behaviours to represent the game, the players and items. We also removed any input/output responsibilities from them and introduces another //action// class for this purpose. 
  
-In the first two iterations of this case study, we revisit the same case study and use class inheritance to model not just items with a name, description and values, but items in general. As with the earlier versions, we use containers to help model the relationships established between objects. Similarly we continue to make use of unit tests. In the [[third iteration]], we address the problem of error detection and user feedback as well as enhancing the functionality of the system. Finally, in the [[last iteration]], we demonstrate how easy it is to use Groovy to police constraints placed on the model.+In the first two iterations of this case study, we revisit the same case study and use class inheritance to model not just items with a name, description and values, but items in general. As with the earlier versions, we use containers to help model the relationships established between objects. Similarly we continue to make use of unit tests. In the [[#iteration_iiiprovide_user_feedback|third iteration]], we address the problem of error detection and user feedback as well as enhancing the functionality of the system. Finally, in the [[#iteration_ivenforce_constraints|last iteration]], we demonstrate how easy it is to use Groovy to police constraints placed on the model.
  
 An index to the source code for all the examples in this case study is [[/~eechris/at-m42/Case-Studies/case-study-04|available]]. An index to the source code for all the examples in this case study is [[/~eechris/at-m42/Case-Studies/case-study-04|available]].
Line 339: Line 339:
 </code> </code>
  
-Because we have changed the signature of the method ''addItem'' in the ''Game'' class, we have to change some of the the existing test cases. +Because we have changed the signature of the method ''addItem'' in the ''Game'' class, we have to change the existing test cases ''testAddItem_5'' and ''testAddItem_6'' to match the use of a return message rather than a returned boolean value. We also add some additional unit tests to test the newly added ''removeItem'' method. 
 + 
 +<code groovy> 
 +     /**  
 +      * Test that the Game had one Item after removal of 
 +      * an Item known to be in the Game 
 +      */ 
 +      void testRemoveItem_1() { 
 +         //  
 +         // book is created in the fixture 
 +         game.addItem(book) 
 +         def pre = game.inventory.size() 
 +         game.removeItem(book.id) 
 +         def post = game.inventory.size() 
 +          
 +         assertTrue('one more item than expected', post == pre - 1) 
 +      } 
 + 
 +     /**  
 +      * Test that the correct message is available to a client 
 +      */ 
 +      void testRemoveItem_2() { 
 +         //  
 +         // book is created in the fixture 
 +         game.addItem(book) 
 +         def actual = game.removeItem(book.id) 
 +         def expected = 'Item removed' 
 +          
 +         assertTrue('unexpected message', actual == expected) 
 +      } 
 + 
 +     /**  
 +      * Test that the correct message is available to a client 
 +      */ 
 +      void testRemoveItem_3() { 
 +         def actual = game.removeItem(book.id) 
 +         def expected = 'Cannot remove: item not present' 
 +          
 +         assertTrue('unexpected message', actual == expected) 
 +      } 
 +</code> 
 + 
 +Notice that we make use of //safe navigation// in the ''removeItem'' method:  
 +<code groovy> 
 +    String removeItem(Integer itemId) { 
 +    def message 
 +        if ( inventory.containsKey(itemId) == true ) { 
 +        ... 
 +            item.carrier?.drop(item) 
 +            ... 
 +        }  
 +        else { 
 +        ... 
 +        } 
 +        return message 
 +    } 
 +</code> 
 +This means that we don't have to make an explicit check that the ''Item'' to be removed is being carried. If its ''carrier'' property is ''null'', then the message ''drop'' will not be sent and a null pointer exception will not be thrown. 
 + 
 +A large number of other unit tests are are needed to test all the possible paths through the new ''Game'' methods. You will find them in the [[http://www.cpjobling.org.uk/~eechris/at-m42/Case-Studies/case-study-04/|source code]]. 
 + 
 +We also decide that the ''Action'' class should be responsible for checking the existence of a specified ''Item'' or ''Player'' before attempting to display it. It should inform the user about the nature of the problem encountered. This is a reasonable decision since it is the ''Action'' object that interacts with the user. 
 + 
 +To implement the remaining new use-cases, we introduce two more flexible display methods. Both make use of regular expressions with ''String''s, as discussed in [[at-m42:lecture2#regular_expressions|Lecture 2]]. The first, ''displaySelectedItems'', displays all ''Item''s whose idd start with the ''String'' entered by the user. The second is similar and it displays all ''Players'' whose id starts with the string entered. An outline of the updated ''Action'' class is now: 
 +<code groovy> 
 +import console.Console 
 + 
 +class Action { 
 + 
 +    // ... 
 +     
 +    def removePublication() { 
 +    print('\nEnter item id: ') 
 +    def itemId = Console.readInteger() 
 +     
 +    def message = game.removeItem(itemId) 
 +    println "\nResult: ${message}" 
 +    } 
 +     
 +    // ... 
 +     
 +    def displayOneItem() { 
 +    print('\nEnter item id: ') 
 +    def itemId = Console.readInteger() 
 +     
 +    def item = game.inventory[itemId] 
 +    if ( item != null ) { 
 +    this.printHeader('One item display'
 +    println item 
 +    } 
 +    else { 
 +    println '\nCannot print: no such item\n' 
 +    } 
 +    } 
 +     
 +    // ... 
 +     
 +    def displaySelectedItems() { 
 +    print('\nEnter start of item ids: ') 
 +    def pattern = Console.readLine() 
 +    pattern = '^' + pattern + '.*' 
 +    def found = false 
 +     
 +    this.printHeader('Selected publications display'
 +    game.inventory.each { itemId, item ->  
 +    if ( itemId.toString() =~ pattern ) { 
 +    found = true 
 +    println " ${item}"  
 +    } 
 +    } 
 +     
 +    if (found == false) { 
 +    println '\nCannot print: No such publications\n' 
 +    } 
 +    } 
 +     
 +    // ... 
 +     
 +    def displayOnePlayer() { 
 +    print('\nEnter player id: ') 
 +    def playerId = Console.readInteger() 
 +     
 +    def player = game.players[playerId] 
 +    if ( player != null ) { 
 +    this.printHeader('One player display'
 +    println player 
 +    def items = player.inventory 
 +    items.each { itemId, item -> println " ${item}"
 +    } 
 +    else { 
 +    println '\nCannot print: no such player\n' 
 +    } 
 +    } 
 +     
 +    // ... 
 +     
 +    def displaySelectedPlayers() { 
 +    print('\nEnter start of player ids: ') 
 +    def pattern = Console.readLine() 
 +    pattern = '^' + pattern + '.*' 
 +    def found = false 
 +     
 +    this.printHeader('Selected players display'
 +    game.players.each { playerId, player ->  
 +    if ( playerId.toString() =~ pattern ) { 
 +    found = true 
 +    println player 
 +    def items = player.inventory 
 +    items.each { itemId, item -> println " ${item}"
 +    } 
 +    } 
 +     
 +    if (found == false) { 
 +    println '\nCannot print: No such borrowers\n' 
 +    } 
 +    }  
 +     
 +    // ... 
 +     
 +    private printHeader(detail) { 
 +    println "\nGame: ${game.name}: ${detail}" 
 +    println '===============================\n' 
 +    } 
 +     
 +// ----- properties ----------------------- 
 + 
 + private game 
 + 
 +
 +</code> 
 + 
 +Note the introduction of the private ''printHeader'' method. This kind of modification during iterative development is quite common. Provides that the change is documented and tested, all should be well. 
 + 
 +All that remains is to modify the previous Groovy script to present and action a slightly different menu to the user. a partial listing is shown in Game 02. 
 + 
 +<code groovy|Game 02: An adventure game with weighty and magical items with error detection and user feedback (at-m42/Case-Studies/case-study-04/game2.groovy)> 
 +import console.Console 
 + 
 +def readMenuSelection() { 
 + // ... 
 + println('3: Remove an item\n'
 +  
 + // ... 
 + println('5: Display selected items'
 + println('6: Display one item'
 +  
 + // ... 
 + println('11: Display selected players'
 + println('12: Display one player\n'
 + 
 + // ... 
 + print('\n\tEnter choice>>>> ') 
 + return Console.readInteger()  
 +
 + 
 + // make the Action object 
 +def action = new Action(game : new Game(name : 'The Discworld')) 
 + 
 + // make first selection 
 +def choice = readMenuSelection() 
 +while (choice != 0) { 
 + switch (choice) { 
 + case 1: 
 + // ... 
 + case 3: 
 + action.removeItem() 
 + break 
 +     // ... 
 + case 5: 
 + action.displaySelectedItems() 
 + break 
 + case 6: 
 + action.displayOneItem() 
 + break 
 +     // ... 
 + case 11: 
 + action.displayeSelectedPlayers() 
 + break 
 + case 12: 
 + action.displayOnePlayer() 
 + break 
 +            // ... 
 + default: 
 + println "Unknown selection" 
 +
 + choice = readMenuSelection() 
 +
 +println '\n\nGame closing ... thanks for playing' 
 +</code> 
 + 
 +<html> 
 +<pre> 
 +0: Quit 
 + 
 +1: Add new weighty item 
 +2: Add new magical item 
 +3: Remove an item 
 + 
 +4: Display inventory 
 +5: Display selected items 
 +6: Dsiplay one item 
 +7: Display available items 
 +8: Display items being carried 
 + 
 +9: Register new player 
 +10: Display all players 
 +11: Display selected players 
 +12: Display one player 
 + 
 +13: Pick up an item 
 +14: Drop an item 
 + 
 +        Enter choice>>>> <b><i>2</i></b> 
 + 
 +Enter item id: <b><i>124</i></b> 
 + 
 +Enter item name: <b><i>potion</i></b> 
 + 
 +Enter item value: <b><i>5</i></b> 
 + 
 +Enter item description (return for none): <b><i>a magic potion</i></b> 
 + 
 +Enter item potency: <b><i>5</i></b> 
 + 
 +Result: Item added 
 + 
 +0: Quit 
 + 
 +1: Add new weighty item 
 +2: Add new magical item 
 +3: Remove an item 
 + 
 +4: Display inventory 
 +5: Display selected items 
 +6: Dsiplay one item 
 +7: Display available items 
 +8: Display items being carried 
 + 
 +9: Register new player 
 +10: Display all players 
 +11: Display selected players 
 +12: Display one player 
 + 
 +13: Pick up an item 
 +14: Drop an item 
 + 
 +        Enter choice>>>> <b><i>4</i></b> 
 + 
 +Game: The Discworld: 
 +=============================== 
 + 
 +  WeightyItem: 111; name = sword; value = 10; description: a rusty sword; with weight: 10 
 +  WeightyItem: 123; name = book; value = 5; description: a book of poems; with weight: 5 
 +  MagicalItem: 124; name = potion; value = 5; description: a magic potion; with potency: 5 
 + 
 +0: Quit 
 + 
 +1: Add new weighty item 
 +2: Add new magical item 
 +3: Remove an item 
 + 
 +4: Display inventory 
 +5: Display selected items 
 +6: Dsiplay one item 
 +7: Display available items 
 +8: Display items being carried 
 + 
 +9: Register new player 
 +10: Display all players 
 +11: Display selected players 
 +12: Display one player 
 + 
 +13: Pick up an item 
 +14: Drop an item 
 + 
 +        Enter choice>>>> <b><i>5</i></b> 
 + 
 +Enter start of item ids: <b><i>12</i></b> 
 + 
 +Game: The Discworld: Selected publications display 
 +=============================== 
 + 
 + WeightyItem: 123; name = book; value = 5; description: a book of poems; with weight: 5 
 + MagicalItem: 124; name = potion; value = 5; description: a magic potion; with potency: 5 
 + 
 +0: Quit 
 + 
 +1: Add new weighty item 
 +2: Add new magical item 
 +3: Remove an item 
 + 
 +4: Display inventory 
 +5: Display selected items 
 +6: Dsiplay one item 
 +7: Display available items 
 +8: Display items being carried 
 + 
 +9: Register new player 
 +10: Display all players 
 +11: Display selected players 
 +12: Display one player 
 + 
 +13: Pick up an item 
 +14: Drop an item 
 + 
 +        Enter choice>>>> <b><i>0</i></b> 
 + 
 + 
 +Game closing ... thanks for playing 
 +</pre> 
 +</html> 
 + 
 +At this point we consider the iteration complete. 
 + 
 +===== Iteration IV: Enforce Constraints ===== 
 + 
 +With graphical notations such as the UML, it is often difficult to record the finer details of a system's specifications. The aim of this iteration is to demonstrate how Groovy can help us do this. 
 + 
 +We can make assertions about our models by adding textual annotations to model elements. For example Figure 3 is a class diagram that illustrates the constraints placed on the Player class such that no player can carry more that a certain number of items. 
 + 
 +{{:at-m42:casestudies:constraint.png|A constraint shown as a textual annotation}} 
 + 
 +**Figure 3**: A constraint shown as a textual annotation. 
 + 
 +The text in the note describes the constraint. It may be informal English, as is the case here, or it may be stated more formally. In any event, we must ensure that the implementation that this constraint is not violated. To accomplish this, we have updated the ''Player'' class to have a ''public static final'' property ''LIMIT'', initialized with the maximum number of ''Item''s that can be carried: 
 +<code groovy> 
 +class Player { 
 +     
 +    // ... 
 +     
 +// ----- properties ----------------------------- 
 + 
 +    def nickname 
 +    def email 
 +    def id 
 +    static public final LIMIT = 4 
 +    def inventory = [ : ] 
 + 
 +
 +</code> 
 + 
 +We can then make checks in the ''Game''s methods so that we don't exceed that limuit. A typical check in the ''pickupItem'' method is: 
 + 
 +<code groovy> 
 +// class: Game 
 +    String pickupItem(Integer itemId, Integer playerId) { 
 +        def message 
 +        if ( inventory.containsKey(itemId) == true ) { 
 +        def item = inventory[itemId] 
 +        if ( item.carrier == null ) { 
 +        if ( players.containsKey(playerId) == true ) { 
 +        def player = players[playerId] 
 +        if (player.inventory.size() < Player.LIMIT) { 
 +         player.pickUp(item) 
 +        message = 'Item picked up' 
 +        } 
 +        else { 
 +        message = 'Cannot pickup: player has reached limit' 
 +        } 
 +        } 
 +        else { 
 +        message = 'Cannot pick up: player not registered' 
 +        } 
 +        }  
 +        else { 
 +        message = 'Cannot pick up: item already being carried' 
 +        } 
 +        } 
 +        else { 
 +        message = 'Cannot pick up: item not present' 
 +        } 
 +        return message 
 +    } 
 +</code> 
 + 
 +As usual, we update the ''Game'''s unit tests to confirm that the code executes as expected. For example we have: 
 +<code groovy> 
 +// class: GameTest 
 +     /** 
 +      * Test that over limit message works 
 +      */ 
 +      void testPickupItemCannotExceedItemLimit() { 
 +      def item4 = new WeightyItem(id : 444, name : 'item 4', value : 4, weight : 4) 
 +      def item5 = new MagicalItem(id : 555, name : 'item 5', value : 5, potency : 5) 
 +      def item6 = new WeightyItem(id : 666, name : 'item 6', value : 6, weight : 6) 
 +      // 
 +      // book, satchel and item3 are created in the fixture 
 +      def itemList = [book, satchel, item3, item4, item5, item6] 
 +       
 +      game.registerPlayer(player) 
 +       
 +      def actual 
 +      itemList.each{ item ->  
 +      game.addItem(item) 
 +      actual = game.pickupItem(item.id, player.id) 
 +      } 
 +       
 +      def expected = 'Cannot pickup: player has reached limit' 
 +       
 +      assertTrue('unexpected message', actual == expected) 
 +      } 
 +</code> 
 +Since Groovy's testing system is so easy to use, it encourages us to do more testing. For example, we can impose constraints on the relationships that exist among objects rather than on object in isolation. The relational constraints start at some object and then follow architectural links to other objects before applying some test. For example, we can assert that if we navigate from any ''Item'' being carried by a ''Player'', then the ''inventory'' of that ''Player'' must contain a reference to an ''Item'' with which we started. In other words, any ''Item'' being carried by a ''Player'' must be consistent. 
 + 
 +This is an example of a //loop variant//. although it does not concern us here, loop invariants are widely used in formal approaches to software development where proof of correctness is important. For our purposes, we just need to demonstrate that if we start at some object and follow a sequence of object links, then we arrive back at the same object. The object diagram of Figure 4 illustrates this.  
 + 
 +{{:at-m42:casestudies:loop-invariant.png|An Item-Player loop invariant.}} 
 + 
 +**Figure 4**: An Item-Player loop invariant. 
 + 
 +The figure shows that if we start from a given ''Item'' and navigate to its ''Player'', then we should find the ''Item'''s id is a key in the ''Player'''s map of carried items. For the model to be consistent, the associated value for that key should be the ''Item'' with which we started. We code the invariant check in Groovy as: 
 +<code groovy> 
 +// class: Game 
 +    private void checkItemCarrierLoopInvariant(String methodName) { 
 +    def items = inventory.values().asList() 
 +     
 +    def carriedItems = items.findAll{ item -> item.carrier != null } 
 +     
 +    def allOK = carriedItems.every { item -> 
 +    item.carrier.inventory.containsKey(item.id) 
 +    } 
 +     
 +    if (! allOK ) { 
 +    throw new Exception("${methodName}: Invariant failed"
 +    } 
 +    } 
 +</code> 
 + 
 +Since the violation of an invariant indicates that a serious error has occurred, we terminate the system by throwing an ''Exception'' with a suitable error message. Notice that we do not declare that the method throws an ''Exception'' (see [[at-m42:Exceptions]]). 
 + 
 +As before, we only check methods that are likely to cause a violation. In this case it is just the method ''pickupItem''
 + 
 +<code groovy> 
 +// class: Game 
 +    // ... 
 +        if (player.inventory.size() < Player.LIMIT) { 
 +         player.pickUp(item) 
 +         this.checkItemCarrierLoopInvariant('Game.pickupItem'
 +        message = 'Item picked up' 
 +        } 
 +        else { 
 +        message = 'Cannot pickup: player has reached limit' 
 +        } 
 +    // ... 
 +</code> 
 + 
 +Before we finish, we must create at least one unit test to check that the expected ''Exception'' is thrown. this turns out to be problematic since we have coded the ''pickUp'' method in the ''Player'' class to ensure that the loop invariant is not violated. 
 + 
 +One solution is to create a ''MockPlayer'' subclass whose redefined ''pickUp'' method as the required abnormal behaviour: 
 +<code groovy> 
 +class MockPlayer extends Player { 
 +     
 +    Boolean pickUp(Item item) { 
 +    if (! inventory.containsKey(item.id)) { 
 +        //  
 +        // Normal behviour commented out 
 +         // inventory[item.id] = item 
 +        item.pickedUpBy(this) 
 +        return true 
 +    } 
 +    else { 
 +    return false 
 +    } 
 +    } 
 + 
 +
 +</code> 
 + 
 +We create a ''MockPlayer'' object in the unit test where a ''Player'' object would normally be expected. 
 + 
 +<code groovy> 
 +// class: GameTest 
 +    void testCheckItemCarrierLoopInvariant() { 
 +      def mockPlayer = new MockPlayer(id : 1234, nickname : 'chris',  
 +                email : 'cpj@swan.ac.uk'
 +      game.registerPlayer(mockPlayer) 
 +      game.addItem(book) 
 +      game.addItem(satchel) 
 +       
 +      try { 
 +      game.pickupItem(book.id, mockPlayer.id) 
 +      fail('Expected: Game.checkItemCarrierLoopInvariant: Invariant failed'
 +      } catch (Exception e) { 
 +      // ignore exception 
 +      } 
 +    } 
 +</code> 
 + 
 +Note that the method ''fail'' reports a failure only if the ''Exception'' has not been thrown. The ''MockPlayer'' class is an example of the //mock object// testing design pattern. It avoids polluting normal code with abnormal behaviours. 
 + 
 +Happily all the tests in the ''runAllTests'' script pass. Therefore, at this point, we conduct functional tests by executing a Groovy script from the previous iteration. As expected, no problems occur and we consider this iteration to be finished. 
  
-===== Exercises ===== 
  
  
at-m42/casestudies/cs04.1239708390.txt.gz · Last modified: 2011/01/14 12:47 (external edit)