CompSci 307
Fall 2021
Software Design and Implementation

Lab Exercise : Refactoring to use Lambdas and Reflection

This exercise is intended to help you to practice using some of Java's "advanced" features that may take a few tries to understand how to use properly (but that require very little syntax). For example, Reflection is an amazing feature of many modern programming languages that lets you dynamically create objects and call methods based on String values instead of having to know their exact names when the code is compiled. This flexibility lets you more easily hold their values in data structures or data files instead of hardcoding them directly within your code.

Submission

At the end of lab, use Gitlab's Merge Request from your pair's forked repository's master branch back to the original organization repository's master branch to submit your group's refactored code. You only need to create one Merge Request, no matter how many commits you make, and you do not worry about potential conflicts since your request will not be approved (it is just an easy way for us to see what changes you made).

Make sure the Title of your Merge Request is of the form "lab_reflection - participating NETIDs".

Lab Workflow

In your project teams, work together using Pair Programming, with separate cloned repositories or use IntelliJ's Code With Me feature, to practice removing different kinds of hardcoded assumptions from your code by turning them into values:

On Gitlab, one person should fork the original project lab_reflection into their own repository to allow pushing your group's changes:

  1. On the resulting project web page, go to Settings -> Members to add your partner so both people in the group can access the same repository
  2. Search for your partner's name and give them a Maintainer role in the project and choose Add to Project
  3. Then, on each person's computer, use git clone to make a local copy of the one forked repository

To ensure everyone does some coding, switch which person actually writes the code every 10-15 minutes (in industry this switch happens only 1-3 times per day). The person who is not actively coding can be advising, suggesting better names, looking up documentation, or searching the Internet for solutions to small problems you are likely to face, but not multi-tasking (i.e., doing their own work or socializing).

After you think each numbered task below is complete, make a GIT commit with an appropriate comment (i.e., there should be at least 7 commits in your project history). After each sub-project is complete, push your changes up to Gitlab (i.e., there should be at least 3 pushes done).

Bins: Flexibility

For these refactorings, the only Java code changes need to be made in the bins.Bins class:

  1. Replace a conditional with polymorphism, using Lambdas to easily create the subclasses
    • Change the fitDisksAndPrint() method so that it takes an third parameter of type Consumer<List<Integer>> and replace the entire chain of conditionals with a single call to the parameter's accept() method, passing the correct list variable. Note, you may want to cut the conditional code since you will need it later as the body of the methods used in your Lambdas.
    • Within the main() method, change the calls to the fitDisksAndPrint() method to provide this third parameter using Lambdas rather than writing out the complete code to declare and instantiate class instances that implement Consumer<List<Integer>>. Two of the Lambdas will transform the list parameter in some way (e.g., sort, shuffle, or reverse) and one will be empty (i.e., do nothing). Note, you should be able to use the code between the braces of each if statement directly, one per call, using the Lambda's arrow, ->, syntax.
  2. Replace separate hardcoded method calls with a loop based on a List of these actions
    • Replace the three constant strings that describe the WORST_FIT algorithms with a constant Map<String, Consumer<List<Integer>>>, i.e., the title and its associated action. It can be immediately initialized using the Map.of() method with the three Lambdas you created directly in the main() method.
    • Within the main() method, replace the three calls to the fitDisksAndPrint() method with a loop using the constant Map to supply the parameter values for each call instead of the current hardcoded constants and actions.

Note, there is no standard way to reflectively create the code within a Lambda expression from a String so the expressions themselves cannot be further extracted to a .properties file (other languages provide this feature as well). However, if each implementation was just a call to a Collections method then the name of the method could be extracted (at the cost of limiting the flexibility of how the data could be organized). The next two examples will explore using classes and methods, i.e., things with names, instead of the anonymous Lambda code, to allow more complete flexibility.

Roulette: Factory Implemented Using Reflection

For these refactorings, the only Java code changes need to be made in the game.roulette.bets.BetFactory class (other changes will involve making .properties files):

  1. Change the hardcoded list of concrete Bet subclasses to use reflection (initially a hardcoded List of Strings).
    • For this step change the List<Bet> to a List<String>, where the strings represent the complete name of each Bet subclass (i.e., the class's package as well as its name).
    • Create a new method makeBet() that gets a Class object representing the Bet subclass named in the given string using the forName() method, then gets its Constructor object, and calls the newInstance() method (and catches its many exceptions) instead of using the instance directly in the previous list.
    • Note, you can use either the given default constructor for each Bet subclass or create some logic to provide the description and odds for each subclass — whichever makes more sense for you to practice for your project.
  2. Remove the hardcoded list and replace it with a list of class names from a properties file instead.
    • For this step create a properties file and, in a new class constructor, read it using a ResourceBundle object to determine which subclasses are possible to create so that nothing is hardcoded anywhere.
    • Note, the reflection code itself should likely not need to change because you are just using the properties file to populate the List instance variable(s).
    • Note, the properties file should map either a general bet name to the complete Bet subclass name (for the default constructor) or the complete Bet subclass name to its odds and description, on one line separated by a comma for easy parsing by your program (for the two argument constructor) — whichever makes more sense for you to practice for your project.

Browser: Data Driven GUI

For these refactorings, the only Java code changes need to be made in the browser.view.NanoBrowserView class (other changes will involve making .properties files):

  1. Change the makeButton() method so that it that takes a String parameter instead of an EventHandler (so two different String parameters)
    • For this step, in each call to makeButton(), replace each Lambda that calls a single method with no parameters to just the name of that method (so it is still essentially hardcoded, but now using Strings instead of Lambdas).
    • Change the call to setOnAction(), within the makeButton() method, to use your own Lambda (instead of the one previously passed in) that uses the given string parameter to get a Method object and then call its invoke() method (and catches its many exceptions).
  2. Change the makeButton() method so that it that only takes one String parameter (i.e., no extra parameter telling it what action to perform)
    • For this step create a new properties file that maps the button's key name (the same one used in the original properties file) to the name of the method to invoke when it is pressed.
    • Create a second ResourceBundle instance variable and, in the NanoBrowserView class constructor, read the new properties file so that the method name to call is not hardcoded anywhere.
    • After loading both of the properties files, you should be able to use the one given string to look up both the text for the button to display (from the original ResourceBundle) and the action for the button to perform (from the new ResourceBundle). Again, this reduces the assumptions and what is hardcoded in your program.
  3. Create a data structure to represent the buttons in the Browser's panels
    • For this step create a List<List<String>> instance variable that holds hardcoded values for the buttons' key names used in the properties files, with a separate List for each row of buttons (i.e., the first list will hold the button keys "BackCommand", "NextCommand", "HomeCommand", and "GoCommand" and the second list will hold the button keys "AddFavoriteCommand" and "SetHomeCommand").
    • Change the calls to the methods makePreferencesPanel() and makeNavigationPanel() in the constructor to take the appropriate list of keys.
    • Change the code in each method to use a loop to create the buttons from the given list rather than hardcoding their order and number.
    • Note, in this example, you will need to change the order of the second panel so the non-button components are explicitly created at the end for both panels (which is also why two separate makeXXXPanel() methods are still needed). But it should not be too big a step to see how to make a more comprehensive data structure that allows you the flexibility to create more than just buttons.

Note, it is typically likely better overall design to make an abstraction (substitutable subclasses) rather than methods, because that is more flexible and recognizable than this simple example. But in either case, reflection can be used to make it data driven rather than hardcoded.