CIS 350 - Spring 2013

Homework #5

Due: Tuesday, Apr 9, 1:30pm

Introduction

Presumably you are familiar with the game "Hangman", in which one player thinks of a word and the other tries to guess it by suggesting letters. If the player cannot figure out the word in a certain number of guesses, then he or she loses. You may have had something like this as an introductory programming assignment, in which the computer program randomly picks a word and the human player has to guess it.

In 2011, a variation of this game was presented as a "nifty assignment" at the SIGCSE conference on computer science education. In this variation, called "Evil Hangman", the computer program changes the target word according to what letters the human player has suggested, making it very hard to correctly guess the word. Sneaky, huh?

The "evil" program works by maintaining a list of words and then, each time the player suggests a letter, removing all words containing that letter from its list (assuming that some words would still remain), essentially dodging the human player's guesses as much as possible. Although it is possible for the human player to win, it is certainly tricky to do so.

In this assignment, you are given an implementation of Evil Hangman as a Java Swing app. The code works correctly (i.e., it correctly plays the game and passes its unit tests) but admittedly is not very "good" in terms of internal quality: design, understandability, readability, etc. Your job is to refactor the code to make it better.

This assignment is more complicated than it sounds. Even if you are a good programmer, do not wait until the last minute, as it can take quite a bit of time to figure out what the code is doing before you can start changing it. You may work with one other student on this assignment; it does not have to be the same student with whom you worked on previous assignments.


Implementation

The code that you will refactor is based on an open-source implementation of the game, though there have been some significant changes to it. Although you are free to look at the original implementation, please note that this code is not considered the "solution" to this assignment.

GUI Classes
The app that you will refactor uses the Java Swing API for its user interface; if you haven't encountered Swing before, it's pretty similar to Android in that you have objects to represent the window itself (in Swing, these are called JFrames), you have the various UI widgets (e.g. JLabel, JComboBox, JButton, etc.), and you have methods that are called whenever the user interacts with a widget (this method is usually called "actionPerformed")

There are four classes that represent the different windows that appear in the app:

Game Classes
There are three other classes that represent the data and logic for playing the game:

This app starts out by using EvilHangMan as the HangmanGame implementation; however, if the player guesses a letter, and removing all words with that letter would mean there are no legal words left, then that guess is considered to be correct (EvilHangMan may be evil, but it's not a cheater). At that point, the app chooses one of the legal words from the dictionary and switches over to NormalHangMan rules.

Tests
JUnit tests have been provided for the NormalHangMan and EvilHangMan classes. Note that there is overlap in some parts, but not in all parts; you will address this overlap in your refactoring.

Although these tests may not be "complete", you can assume that they are "sound": if any test fails, then that indicates a bug in the program.


Refactoring Activities

Your goal in this assignment is to refactor the code in the Evil Hangman game, in order to improve its design, as well as its understandability, readability, etc. In some cases, you may need to make changes to the JUnit tests so that all tests still compile and pass.

If you have a question about the specification or the intended behavior of the code, please ask the instructor immediately; relevant answers will be distributed to the entire class. You may find (unintentional) bugs in the code that are not related to the test cases we've provided; it is not necessary for you to fix them, but ask the instructor if you are unsure.

NOTE: it is recommended that you complete the refactoring steps in the order in which they are provided here. Although you will ultimately end up with the same final product anyway, the steps that are described here will help remove any redundant refactoring activities.

 

1. Set up Eclipse project
The code is provided to you as an Eclipse project. Install this project and then run it by using Start as the main class. You should be able to play the game using the UI. Try not to get addicted to it.

Run all the unit tests from the "tests" directory to make sure that they all pass. If any tests fail, please let us know immediately.

Because you are going to be making lots of changes to the code, and some of those changes will unintentionally break it, you may want to put this code into source control so that you can "roll back" to previous revisions if necessary. This may seem excessive, but better safe than sorry, right?

 

2. Refactor the NormalHangMan and EvilHangMan classes
The NormalHangMan and EvilHangMan classes both implement the HangmanGame interface, but there is duplicate code in that they share a lot of common fields and some basic methods. Remove the duplicate code by using one of the refactoring patterns discussed in class to consolidate everything that NormalHangMan and EvilHangMan have in common.

There are a few ways to do this: use the one that makes the most sense to you, and explain your decision in your write-up. If you are unsure how to do this in Java, please ask a member of the instruction staff for help (please don't post approaches/solutions on Piazza; it is important that all students figure this out for themselves!). Hint: you may want to look into Eclipse refactoring capabilities; it may help things move a bit faster.

Because you are not changing the exposed (public) methods of NormalHangMan and EvilHangMan, nor are you changing the functionality, you should not have to change the corresponding JUnit tests. But make sure that all tests still pass, of course! If you need to change any of the tests, then document that in your write-up, too.

Before moving on to the next step, also be sure that the entire app still works correctly, by playing the game from the user interface.

 

3. Refactor the LetterGuessHistory
In the implementation that was provided to you, the history of which letters the user guessed is stored as characters in a String (called "LetterGuessHistory") in the HangmanGame classes. Instead of using a String, change this to a more appropriate data structure: in your write-up, you'll explain which one you used (ask a member of the instruction staff if you are unsure). Again, please don't post your answer on Piazza; everyone needs to figure this out on their own.

Note: Whatever data structure you choose, it should be one that maintains the order in which the player guessed the letters. That will make the test cases easier to write.

After making that change, modify all of the affected methods in the application. Unlike in Step 2, in this case, you will have to modify the unit tests! Make sure that all tests compile and run successfully before moving on, and also play the game to make sure it still works.

 

4. Refactor the Wordlist
The EvilHangMan class stores the list of legal words (i.e., those words that appear in its dictionary and are of the specified length) in an array called Wordlist. When it updates the list after the user has guessed a letter, it creates a new array (egads!) and then copies over the remaining words that do not contain that letter.

Refactor the EvilHangMan class so that Wordlist is a more appropriate data structure (as in Step 3, you'll need to explain your choice in your write-up) and then modify the EvilHangMan.makeGuess method accordingly. This part is fairly tricky, so we recommend making a copy of the method and commenting out the original before you start your refactoring, rather than blowing the original away entirely.

In this case, you should not have to modify the test cases in EvilHangmanTest, but if it turns out that you need to, explain why in your write-up. As always, make sure the game still works before you continue.

 

5. Refactor the GUI_PlayGame.controller method
The GUI_PlayGame class not only contains the code for the user interface for playing the hangman game, but it also has a method called "controller" that is called each time a letter is guessed (on line 84, in the "actionPerformed" method). This game logic should not be in the same class as the UI code, so in this step you should move the method to the class you created in Step 2 (if, somehow, you did not create a new class in Step 2... well, then I don't know how you did that, but ask a member of the instruction staff to figure out what to do here), and then modify the method call on line 84.

As discussed in class, moving a method can be a bit tricky, because you'll no longer have direct access to all the fields. Before you start, think about all the "inputs" and "outputs" and how you'll deal with them; you'll need to discuss your approach in your write-up, and describe the tradeoffs that you had to make.

Okay, here's the really tricky part: now that you've refactored it, create JUnit tests for the "controller" method. To do this, create a new JUnit test class (i.e., don't add these test cases to the classes we provided you) and add test methods that cover the six branches in the "controller" method:

Note that you may, of course, cover multiple branches in the same test case if you prefer.

Your test methods should put the object into the appropriate state before calling the "controller" method directly, and then checking for the correct behavior (output and side effects). This will almost certainly entail changing the visibility of some fields and/or adding setter and getter methods; make sure you describe those changes in your write-up.

Keep in mind that you are not testing the GUI_PlayGame class, but your tests will, of course, need some of the same objects that it uses for input and output. You may create an instance of the GUI_PlayGame class if that helps you, but your test should be calling the "controller" method directly, not going through the GUI_PlayGame.actionPerformed method. Ask a member of the instruction staff if you need help.

After you've finished refactoring and writing your tests, be sure to re-run all unit tests (even the ones you didn't modify!) and run the app from the UI to make sure that everything still looks okay!

 

6. "Refactor mercilessly"
Now refactor the rest of the program by identifying code smells (particularly the ones discussed in class), as well as problems with the design or with the implementation, e.g. violations of coding conventions, code that is hard to read or understand, etc.. You may make any changes you'd like to the code, as long as it still passes its unit tests and does not affect the functionality of the game; however, you should not undo any of the refactorings described in Steps 2-5.

When looking for things to refactor, use the suggestions from class and from Fowler's Refactoring: Improving the Design of Existing Code, and anything else you can think of to improve the code. You can certainly use the Eclipse refactoring capabilities as needed. For instance, you may want to consider:

As discussed in class, you should refactor iteratively, instead of making lots of changes all at once. Each time you make a change, be sure to modify and run the corresponding JUnit test(s) to ensure that the functionality is still correct. If you remove a method (e.g., combining two methods into one), then you should make sure that any JUnit tests for the removed method are modified so that the functionality captured by that method is still tested. If you create new methods (e.g. splitting one method into two, or adding a helper method), you should create unit tests for those as well. In all cases, you must make a good-faith effort to ensure that the code works as expected, and not that it simply passes the provided unit tests.

Remember, the goal is not to improve the functionality or to find bugs, but rather to improve the design of the code.

Note that there are no unit tests for the GUI classes, but you should still consider them for refactoring. Make your best effort to ensure that their functionality still remains the same by running the app from the UI.

Keep track of the changes you make, and the justification for doing so (i.e., why you made the change). You will need to justify all of your changes in your report.

We suggest that you start by making small "local" changes to the code (within a single method) before making more "global" changes that affect numerous methods. But the order in which you make changes is up to you.

NOTE!! It bears repeating: every time you make a change, even if it seems trivial, run the corresponding unit tests to make sure you didn't break anything. If you make a lot of changes and then run the tests, only to find that they fail, you'll have to go back and debug the program!

 

7. Document your work
To assist the TAs in grading this assignment, please fill out a spreadsheet like this one in which you describe all the changes you made in Step 6, including:
  1. The class(es) in which the problem occurs.
  2. The method name(s) in which the problem occurs (if applicable).
  3. The line number(s) on which the problem occurs.
  4. The type of code smell (using the terminology from the Fowler textbook), if applicable.
  5. A brief (1-3 sentences) description of how you refactored/fixed it.
If the line numbers have changed as a result of your refactoring (which they definitely will), please try to use the line numbers from the original implementation if possible.

Additionally, create a PDF in which you answer the following questions:


Deliverables

Your final deliverables should consist of the following:

1. Modified code
Submit the entire Eclipse project containing your refactored code in its own zip/tar file. This should include all JUnit tests, including any new tests that you created; all unit tests should pass.

2. Spreadsheet
Submit a spreadsheet (described above) that describes the changes you made to the code in Step 6.

3. Report
Submit a single PDF file that answers the questions stated above (regarding Steps 2-5).


Submission

All deliverables are due by Tuesday, April 9, at 1:30pm. Late submissions will be penalized by 10% if less than 24 hours late, 20% for 24-48 hours late, and so on, up to one week, after which they will no longer be accepted.

To turn in your assignment, zip and/or tar up the the modified code (you don't need to submit the original!) and submit it along with the spreadsheet and PDF, following the submission instructions.

If you worked with someone else, only one of you needs to submit the assignment. Please indicate the names of both students in the comments field of your submission.


A word about grading

As in the previous assignments, there are certain problems in the code (this time with the design, not the functionality) that we already know about. Your grade for Step 6 will mostly be determined by how many of these "code smells" you fix, and to what extent your fix increases the internal quality of the code. So when refactoring, be on the lookout for the code smells described in class and in Fowler's book. These include but are not limited to Duplicate Code, Long Methods, Long Classes, Primitive Obsession, and Message Chains.

There are, of course, other issues with the code, e.g. problems with naming conventions, performance, etc., and you should describe those, too, but focus on the code smells from class.


Academic Honesty and Collaboration

You may not discuss or share solutions or findings with other students, nor should you be receiving any help from outside sources, including students not taking this course or online resources. If you find help online or use a third-party tool as part of your solution, you must cite it. Failure to do so will be considered academic dishonesty, and you will receive a grade of 0 on this assignment.

If you run into problems, please ask a member of the teaching staff for help before trying to find solutions online!


Updated: Mon, Mar 25 2013, 5:12pm