Refactoring with Lists and Loops

In this activity, you will:

  • Learn about refactoring
  • Practice working with lists
  • Practice using different Java loop patterns
  • Use unit tests to verify code correctness during refactoring
  • Practice thinking about and dealing with edge cases and special cases

In software development, refactoring means changing the structure of code without changing its functionality. On long-running projects, developers often refactor code to keep it readable, maintainable, and ready for the future.

Why? Why change code that already works, so that it does exactly the same thing?! There are at least three good reasons for this:

  1. to reflect on and improve our own work after completing a first working draft,
  2. to make the code clearer to people reading it in the future, and
  3. to prepare the code for some upcoming change.

Louis Brandeis said, “There is no great writing, only great rewriting.” This might be true of code as well: the first draft is rarely the best draft. It is possible to take rewriting too far — eventually, you need to just say “enough” and move on! — but it is crucial to have the capacity to rewrite.

Refactoring can restructure a whole program, or just one little piece. In this activity, you will practice refactoring several individual methods involving lists and loops. Sometimes one version is clearly better. Sometimes there are tradeoffs. Sometimes it is a matter of taste.

The List Basics reading says:

There are always many approaches! The important thing is not to learn the single best one and always use it, but rather to see the many alternatives and possibilities, and get comfortable with them.

This activity is about learning to see alternatives and see possibilities. Good refactoring starts with realizing that you have choices.

Look in ListFormatting. Read the documentation for the numberEachItem method. Do you understand what this method is supposed to do?

Look at the test for numberEachItem in ListFormattingTest. Study the test cases. Do you understand what they’re testing for? Run the tests and make sure they pass.

Your challenge: Refactor this method to use a C-style for loop instead of a for-each loop.

Hint: . There is an example of how to do that in

Use the tests to help you as you go. If you think you’re done, make sure the tests pass! Even if you aren’t done, however, the tests can still help: you can run the tests when it’s only partly working, and make sure that you are getting the (partial, incorrect) results you expect. “Fails as expected” is useful information. Test early and often! A common beginning programmer mistake is to write lots and lots of code before running any of it.

Once you have your new version and all these tests pass again, make sure that you have deleted the old code. (Be bold! Don’t just comment out defunct code; delete it!)

( Notice how having working tests allows you to refactor like this with much greater confidence.)

Now open up GitHub Desktop, and open up ListFormatting. The code you deleted will appear in red; the code you added will appear in green. This is called a “diff” (short for “difference”). Inspect the diff. Compare the old version to the new version.

Discuss with your partner: Is one clearly better? What are the tradeoffs?

Commit your work.

New method, same procedure:

  • Read the documentation for this method. Does it make sense?
  • Inspect the test for this method. Does it make sense?
  • Run the tests. Make sure they pass.

Your challenge: Refactor this method to use no loops at all, and instead use a helpful String method that Java provides for you.

You can find that documentation for String by putting “ java 21 string api ” into a web search engine. (21 is the version of Java we are using. The term “API” here refers to the parts of the String abstraction you can see from the outside. We’ll talk more about that later in this course!)

Scroll through the String docs. Read the descriptions of the methods. Is there one that would help you put commas in between items from a list? It’s very hard to spot it! (That is unless you already know about this method, in which case it is fairly easy. Experience sure does help.)

Answer:

(How are you supposed to find something like that?!? The answer: (1) reading other people’s code, (2) seeking help from people, documentation, or other resources, and (3) experience.)

Q: “OK, but…I don’t see how that method applies here! It doesn’t mention String or List.”

A: Yes, it is really tricky! It works because , and . You are not supposed to already know this. You simply cannot know absolutely everything about a programming language or tool at first (or, in most cases, ever). Again: reading code, seeking help, experience. Those things are the only way, and they simply take time. Today, you’re getting a nice bite of all three!

Now that you have the method, how do you call it?! Hint: Look at the declaration. . Answer: In the Unit testing activity, you learned that . So the way to call this method is .

Once you have figured out how to call the method, your code will ! The moral here is that sometimes it takes a lot of work to produce something simple. More code ≠ more accomplished.

Go to GitHub Desktop and compare the old versus the new version in the diff.

Commit your work.

New method, same procedure:

  • Read the documentation for formatGrammatically.
  • Inspect the test for it.
  • Run the tests.

Notice that this method is a lot like the previous one. Exactly the same, in fact, except that the thing joining the last two items needs to be “and” instead of “,”. (Here the ⎵ symbols represent spaces.)

There is a way to do this using again. Can you see how to do it?

Hint:

OK, but how do you do that? doesn’t have an option to only join some of the list!

Hint: Look at the documentation for List. . Having trouble spotting it? Here it is.

Spelling it out a little more:

Ask for help. Do the tests all pass? Yay! But wait…

Look at the tests for numberEachItem and formatWithCommas. Both of them test (1) an empty list, and (2) a single-element list. (Find those test cases!) For some reason, however, those test cases seem to be missing for formatGrammatically.

Add those test cases to the test for formatGrammatically. Run the tests.

Depending on how you implemented formatGrammatically, it is very likely that those cases fail. (If they don’t fail, check with your instructor or preceptor. It’s possible your test might be calling the wrong method, or otherwise not testing what it’s supposed to test. Or maybe you already spotted the problem and handled it!)

When there is some limit or boundary to data — an empty list, an empty string, a minimum value, a maximum value — we refer to the situations where we are working near that boundary as edge cases. When writing tests, it is always important to think about edges. Why? Because bugs tend to live there.

For this problem, we can write simple code that works just great when there are 2 elements in the list, or 10, or 100, or 1000000…but doesn’t work for 1 or 0. It’s the edge cases that get us!

Now, fix the edge cases of 0 and 1 elements. To do this, you will probably need to treat them as special cases: instead of handling them with the normal logic, you add an if statement that checks for 0 elements and does something special, and another that checks for 1. We don’t generally like special cases in code; it’s better to limit how many paths the code can take. Sometimes, however, a special case is the best solution.

When the new edge case tests pass, inspect the diff and commit your work.

Either now, or after class, or after you do the bonus challenges below:

Look in the solutions directory in the project. There you will find many ways of implementing these methods, with some opinionated commentary on the different approaches. Compare these to your own solutions. Think about each one. What do you notice? What do you wonder?

Try implementing formatGrammatically using a C-style for loop. Compare to the previous approaches.

Any other alternative approaches you can see for the other methods?

The test for this method is turned off; JUnit is currently skipping it. Enable the test by deleting @Disabled from the test file. Run the tests, and make sure that they now fail.

This one is tricky to implement: there’s just not a good way to make it really tidy. Consider all the different approaches you’ve seen for the methods above, and try one. See how it pans out.

Then consider refactoring.

If you are looking for something extra:

Create the following method that can either use the Oxford comma or not, depending on its second parameter:

  public static String formatGrammatically(List<String> items, boolean oxfordComma) {
      ...
  }

Don’t implement it by calling the methods already in the code. Instead, do it the other way around! Change the existing “format grammatically” methods so they both use your new one, like this:

  public static String formatGrammatically(List<String> items) {
      formatGrammatically(items, false);
  }

  public static String formatGrammaticallyWithOxfordComma(List<String> items) {
      formatGrammatically(items, true);
  }

…and see if all the tests still pass!