Martin Fowler is an influential professional software engineer who has written books on refactoring, and provides a free first chapter demonstrating common refactoring techniques using the Javascript programming language. In this assignment, you'll follow a refactoring process that closely follows the ideas and process in this chapter, but using Java.
Create a new public GitHub repo using this template repository. Make a local clone of your repo and complete Tasks 0 through 2.
For this assignment, you should do all of your work on the main branch.
Submit a link to your GitHub repo on MarkUs and run the self-tests.
By the end of this assignment, you will be able to:
- identify and fix common style and coding issues using the CheckStyle plugin for IntelliJ
- apply common refactoring techniques to clean existing code
- use built-in IntelliJ refactoring to more efficiently apply common refactoring techniques
- more confidently work with git
Please refer to the free textbook chapter if you would like additional context about the domain used in this example.
Briefly, we are working on a program for printing invoices for a theater company. The program currently only supports specific types of plays and generates a plaintext invoice like below:
Statement for BigCo
Hamlet: $650.00 (55 seats)
As You Like It: $580.00 (35 seats)
Othello: $500.00 (40 seats)
Amount owed is $1,730.00
You earned 47 credits
This information is derived from the play and performance information contained in the input json files shown below. The dollar amounts are calculated based on the type of performance and the size of the audience.
invoices.json:
[
{
"customer": "BigCo",
"performances": [
{
"playID": "hamlet",
"audience": 55
},
{
"playID": "as-like",
"audience": 35
},
{
"playID": "othello",
"audience": 40
}
]
}
]and plays.json:
{
"hamlet": {"name": "Hamlet", "type": "tragedy"},
"as-like": {"name": "As You Like It", "type": "comedy"},
"othello": {"name": "Othello", "type": "tragedy"}
}The starter code currently works for this example, but we are tasked with refactoring the code to make it easier to eventually add two new features:
- print the invoices in html format instead of plaintext.
- support other types of plays.
For reference, the UML Class Diagram for our starter code looks something like below:
If we follow the refactoring steps outlined in the free chapter, once we have added support for the html output format, our code will look something like below (omitting irrelevant classes; exact variables and methods may vary):
And once we have added support for other play types, our code will look something like below (omitting irrelevant classes; exact variables and methods may vary):
Our task is to transform our starter code so that it begins to resemble the fully refactored code. We'll do this through a sequence of refactoring steps.
The full refactoring takes quite a bit of time to go through, so we'll only do the first part in this assignment. The instructions for the full refactoring are included though for extra, optional practice.
When you refactor, you make a series of changes that improve the design of the software — but without changing the behaviour. As seen in lecture, there are lots of reasons for this: to make the code easier for other programmers to understand and navigate (and debug), to make it easier to automatically test, and to make it easier to add new features.
As a rule, when refactoring, the code should always pass the existing tests!
This assignment is about the process, not the final result:
- Do a refactoring
- Run the tests to ensure you didn't introduce any bugs
- Make a commit
Important: remember to run your tests before each commit to ensure they are still passing with each refactoring! That way, when you introduce a bug, you'll notice it right away!
Self tests will be available on MarkUs; they will be using your git logs to check your commits for evidence of following the process.
Important: Make sure to follow the instructions about what to put in your commit messages as you go.
Here are the tasks you will perform.
- Run the tests to confirm you are ready to start refactoring.
- Address the existing CheckStyle issues.
- Do a series of refactorings to improve the design of the code.
Before doing any coding, find the tests and run them.
The project should be configured with an existing run configuration for StatementPrinterTests
saved in the .idea/runConfigurations directory.
If you don't see this listed beside the run button, look in the test folder
to find class StatementPrinterTests. Right click on it and find Run … in the menu.
Tip: once the
StatementPrinterTestsrun configuration exists, you should see a run button in this readme; you can click that to run the tests whenever you are reminded to do so in these instructions. Neat!
Before we get to work refactoring the design, we'll take time to clean up our starter code.
Both Bob Martin and SonarSource, the company that develops the SonarQube plugin, promote a "Clean as You Code" mindset: leave the program cleaner than when you found it and your teammates and future-you will appreciate it.
Here's the TL;DR for what "Clean as You Code" means:
- Make sure that all your new code is beautiful.
- If you have to change old code (add a method to a class or update a method's return type, for example) and notice a style or design issue, then fix it. Don't go hunting; just tidy what's in front of you.
- Write a unit test or two for your changes if one doesn't already cover it. If the old code doesn't have any tests yet, this is especially important. (Note: for this assignment, we have provided the required tests for you.)
The parts of the old code that get touched frequently will be clean sooner (saving time each time it is touched).
Doing this kind of old-code refactoring in its own commit is an excellent idea.
We'll be using CheckStyle to automatically scan our Java projects for style issues.
Companies follow many different styles, but every established software company uses a style guide and automated tools to find style and design violations, so it is good to get in the habit of using tools like Checkstyle.
The project includes a file called mystyle.xml. The project should already be configured to use it, but if not, you can go into the IntelliJ settings,
go to Tools -> CheckStyle and add mystyle.xml as a new configuration file.
For the description, you can call it anything you want, such as CSC207 Checks.
Once you have CheckStyle set up, you'll see red underlines in the code.
Hover — don't click — and a popup will appear mentioning what the problem is and often also suggesting a fix.
The project stores information about CheckStyle in
.idea/checkstyle-idea.xml.
- For each Java source file in
src/main/java/, fix all CheckStyle issues that are detected. You should either see the CheckStyle Tool Window icon (it looks like a pencil) on the left of IntelliJ or you may need to go toView -> Tool Windows -> CheckStyleto open the CheckStyle Tool Window. The following provides more about this step.
Important: make sure you are using the provided
mystyle.xmlconfiguration for CheckStyle as outlined above and not one of the other style files bundled with IntelliJ.
Here is a list of CheckStyle issues you can expect to encounter in the starter code:
-
Public instance variables: the
Encapsulate Field...feature is useful! You usually only need a getter and not a setter. -
Non-final variables: variables that are only assigned to once should be declared
final. -
Braces required for if statements and loops: add braces
{}around one-line if statements. -
Javadoc comment is missing: public classes should have Javadoc describing them.
- Try writing a Javadoc comment yourself. For inspiration, you can look at some provided Javadoc in the starter files; IntelliJ can also help with this (https://www.jetbrains.com/help/idea/javadocs.html). The details of what exactly you write in your JavaDoc comments won't be graded, but practice writing good comments. As in first year, the documentation should help you understand the purpose of the code when you read it without explaining how Java works.
-
Magic numbers: these are an issue because there is no context for what the values represent. These mostly appear in the
statementmethod. There are constants defined inConstants.java. Use these instead. -
Static modifier out of order: always use
public,static, andfinalin that order. -
Line is longer than 120 characters: may show up if a line gets too long.
Tips:
- Browse through the various starter files for ideas about how to fix each CheckStyle issue, as we have already fixed some issues in the files.
- In general, take this time to get more comfortable with the IDE, as its features will help you be a more efficient programmer.
- After you fix each Checkstyle issue, run the test file (
StatementPrinterTests) to make sure you didn't introduce a bug!
- When you are done, commit your work with a commit message that includes the string
Task 1.1.
Very Important: we'll be looking for the string
Task 1.1in your git log of commit messages in the MarkUs autotests!
If you forget to commit or forget to include
Task 1.1in the message, you can just make another commit.
Git includes commands to allow you to conveniently summarize and explore the commit history of your git repository.
As highlighted already, the emphasis in this assignment is on the process of performing the refactoring. To document this process, you will also maintain a record of your git log in your repository.
- Run the command
git log --onelinein the terminal and copy+paste the output into thelog.txtfile to replace all of its contents.- Alternatively, make sure your terminal is open in your project directory,
and you can run
git log --oneline > log.txtto replace the contents of thelog.txtfile with the log output.
- Alternatively, make sure your terminal is open in your project directory,
and you can run
Important: do this each time you are asked to commit your work in the remaining tasks.
- Commit your changes to
log.txtnow; include the stringTask 1.2in your commit message.
Note: this commit won't include the updated log with the most recent commit, which is fine. The next time we ask you to commit, you'll update your
log.txtfile again and commit the updated log.
- There should be no CheckStyle issues and
StatementPrinterTestsshould still pass. - The
log.txtfile should contain a copy of your git log.
It's time to refactor the design of the code!
This roughly follows the steps outlined in the Refactoring textbook (page number reference in [] beside each). See below for a concise list.
- Create a helper method and move code into it (Extract Method) [page 6]
- Rename a local variable [page 9]
- Rename a parameter [page 10]
- Remove a parameter from the helper function (Replace Temp with Query, which has several sub steps) [page 10]
- Create another tiny helper function and call it (IntelliJ: Extract Method) [page 12]
- Remove a local variable (IntelliJ: Inline Variable) [page 12]
- Remove a parameter (IntelliJ: Change Signature) [page 12]
- Remove a local variable (IntelliJ: Inline Variable) [page 14]
- Move a calculation into a method (IntelliJ: Extract Method) [page 14]
- Rename a local variable [page 16]
- Create a helper method for a calculation (Replace Temp with Query) [page 16]
- Rename a method (IntelliJ: Change Signature) [page 17]
- Separate the accumulation of a few variables (Split Loop) [page 18]
- Move the initializations close to their loops (Slide Statements) [page 18]
- Remove a local variable (Replace Temp with Query) [page 19]
Each subtask roughly corresponds to a step described in that textbook.
Note: The textbook example is written in JavaScript, so some differences in programming language features lead to differences in how our code will end up looking.
Reminder: Address CheckStyle issues as they come up to ensure that you don't reintroduce any poor coding style as you refactor — future-you will appreciate it!
Stuck on a step described below? The Walkthrough for Task 2 contains animations of applying the main refactoring steps that you will need to complete.
The StatementPrinter.statement method consists of the main logic of the program. For a programmer seeing the code for the first time,
it can be easy to get lost in the details. Our first goal is to identify the high-level purpose of each logical chunk of code,
then refactor to make the logic easier to follow.
Throughout, we provide links to the IntelliJ documentation when available; we have also included a copy of the course notes chapter on refactoring, which describes most of the refactoring steps you will need to perform.
The switch statement exists to calculate the base amount for a given performance and play. Let's make that explicit by moving it to a helper method.
IntelliJ Documentation: Extract Method
-
Create a helper method and move code into it.
Apply
Extract Methodto turn theswitchstatement into its own helper method for calculating the base amount for a given performance and play. Name the new methodgetAmount.Tip: select the whole switch statement INCLUDING the declaration for
thisAmount, then IntelliJ's Refactor->Extract Method tool. That declaration is part of theswitchstatement calculation.- do the tests still run (
StatementPrinterTests)?
- do the tests still run (
-
Rename a local variable.
Many programmers like to use
result(orrslt) as the name for the variable used to hold the return value. The code currently usesthisAmount. Rename that local variable in your extracted method by right-clicking the variable name and choosingRefactor -> Rename. Notice that it updates it throughout the entire method!- don't forget to fix any CodeStyle or SonarLint issues which may have popped up during this refactoring step!
- do the tests still run (
StatementPrinterTests)?
-
Rename a parameter.
pis probably not a great name for a variable of typePerformance. Rename it to something longer, likeperformance. Do this by right-clicking on variable and selectingRefactor->Rename. This will renamepeverywhere it is used. -
Remove a parameter from the helper method.
The extracted method header has two parameters, a
Performancevariable and aPlayvariable. The second parameter is unnecessary. Since we have aPerformanceobject, we can always look up whichPlayobject is associated with it using theplaysinstance variable, so it feels unnecessary to pass in both aPerformanceobject and aPlayobject to our helper.We'll apply Replace Temp with Query to stop using the parameter. This happens in 2 steps: Extract Method, followed by Inline Variable.
-
Create another tiny helper method and call it
getPlay.Select the expression on the right-hand side of the initialization for variable
playin the for loop in methodstatement, then use Extract Method. Name itgetPlay. -
Remove a local variable.
Select that same
playvariable in methodstatementand choose Refactor->Inline Variable. This will replace all occurrences of variableplayin methodstatementwith a call togetPlay.If you don't see the
Inline Variableoption, you can use the keyboard shortcut Cmd+Option+N on Mac (Ctrl+Alt+N on Windows) as documented here. -
Remove a parameter.
With the new helper method, we can now remove the
playparameter from methodgetAmount. This requires 2 steps:a. Variable
playis used twice in the method. Replace both of them with a call to the new helpergetPlay.b. Now variable
playis no longer used in the method body forgetAmount. Update your first helper method, along with your call to it, such that it only takes aPerformanceobject as a parameter. In IntelliJ, use theRefactor->Change Signaturerefactoring and deleting the second parameter. Right-click on the method header to open the menu.
- That was big! Make sure the tests still run (
StatementPrinterTests), and commit your work.
-
-
The last step is to look back at how the result of our call to this helper is actually used in the
statementmethod. It turns out we are storing the value in a local variable for convenience, but the code is arguably easier to understand if we again apply theInline Variablerefactoring. In IntelliJ, right-click thethisAmountvariable and chooseRefactor -> Inline Variable. -
do the tests still run (
StatementPrinterTests)? -
Update your
log.txtfile and commit your changes. Make sure to include the stringTask 2.1in your commit message.
Did you update your log.txt and commit your changes when you completed the previous task?
Do it now before you go any further!
Now that we have dealt with getting the base amount for each performance, we can turn our attention to refactoring the part of the code responsible for calculating the volume credits.
-
Move a calculation into a method.
As before, we will use
Extract Methodto move the logic of calculating the volume credits for a performance into a helper method.Observe that the statements immediately following
// add volume credits(including theifstatement) update the volume credits for the current performancep. Our helper method will calculate and return the contribution from the current performance.Highlight those lines of code related to volume credits then right-click and choose
Refactor -> Extract Method. UsegetVolumeCreditsas the method name.- note that this will result in an awkward helper which takes in the current value for volume credits, possibly increments it, then returns it.
- also, CheckStyle may complain:
Assignment of parameter 'volumeCredits' is not allowed. - to fix this, replace the
int volumeCreditsparameter with a local variable and initialize it to 0. This removes the dependency on thevolumeCreditsvalue from methodstatement, because the method now just returns the contribution from the current performance. - you will need to update where you call method
getVolumeCreditsto make sure you're usingvolumeCredit += ..., as this refactoring step may have replaced that with just an assignment statement (=rather than+=).
-
Rename a local variable. As before, rename the local variable in your
getVolumeCreditshelper to beresult. -
Rename a parameter. As before, rename the parameter
pfor our helper to beperformance. -
do the tests still run (
StatementPrinterTests)? -
Update your
log.txtfile and commit the changes to any files. Make sure to include the stringTask 2.2in your commit message.
Note: this step looks slightly different from how it is done in the textbook, but the result is the same. Plus, it will demonstrate how helpful IntelliJ can be when refactoring.
The goal now is to clean up how the frmt variable is used in the code.
Initially, it was declared near the top of the method and then used twice later to format integers as dollar amounts. As we have already seen, it may make the code
easier to understand if we avoid extra local variables.
Follow the steps below to refactor this part of the code.
- Remove a local variable.
Apply the Inline Variable refactoring to the frmt variable.
- this won't immediately look like it accomplished much, but it will now make it convenient for our next step.
- Create a helper method for a calculation.
We're going to create a method that, given an amount, returns a String containing the amount of dollars in US currency. We'll move all that NumberFormat stuff inside there, too.
There are two expressions we want to replace with a call to this new helper method.
Select the SECOND call to NumberFormat.getCurrencyInstance(Locale.US).format as well as the arguments to that call. (The first may not do the correct refactoring.) Perform an Extract Method refactoring and press Enter once done. You'll see that the helper takes an int as its parameter now and the division by Constants.PERCENT_FACTOR is performed within the helper. This is precisely what the textbook arrives at, but it takes a bit more manual work.
-
Update the FIRST call to
NumberFormat.getCurrencyInstance(Locale.US).formatso that it calls our new helper. -
getFormatdoes not convey the meaning of the method. It's about US dollars, so rename it tousd. -
As a sanity check, our two method calls at this point are
usd(getAmount(p))andusd(totalAmount). Yours should look similar. -
Remember to run the tests (
StatementPrinterTests) frequently so mistakes are caught while they are easy to undo! -
make sure to check the parameter name for your helper and update it to ensure that it is descriptive.
- Update your
log.txtfile and commit the changes to any files. Make sure to include the stringTask 2.3in your commit message.
Currently, the accumulation of volume credits (volumeCredits += getVolumeCredits(p);) is in the same loop
as we are calculating the total amount (totalAmount += getAmount(p);) and also
the result return value (result.append(...)). This is preventing us from extracting a natural method
for calculating the total volume credits, so we need to break it up.
First, we will decouple the logic into three loops, then we will refactor using the same techniques as before.
-
Separate the accumulation of a few variables.
Apply the Split Loop refactoring to split out the logic of volume credits, the logic of the total amount, and the accumulation of the
resultstring into three independent loops.Copy and paste, then remove the repeated code is one way to achieve this.
-
Move the initializations close to their loops.
Apply the Slide Statements refactoring to slide the declaration and initialization
of each accumulator variable to just above its associated loop.
In IntelliJ, Option-Shift-UpArrow on Mac (Ctrl-Alt-Shift-UpArrow on Windows) and
Option-Shift-DownArrow on Mac (Ctrl-Alt-Shift-DownArrow on Windows) will move the current
line up or down so you don't have to copy and paste.
Note: this step is necessary so that IntelliJ can do some automatic refactoring for us next.
- Remove a local variable.
We'll again perform a Replace Temp with Query refactoring which amounts to first applying
Extract Method and then Inline Variable to refactor the calculation of volume credits.
- select the loop for volume credits, including the
int volumeCreditsdeclaration and applyExtract Method. Name the methodgetTotalVolumeCredits. Once that's done, right click onvolumeCreditsand applyInline Variable. - use the convention of accumulating into a
resultvariable as we have done previously.
-
Refactor the
totalAmountloop using the sameReplace Temp with Queryrefactoring from above. Name the methodgetTotalAmount. -
Update your
log.txtfile and commit the changes to any files. Make sure to include the string "Task 2.4" in your commit message.
Note: don't do this for the
StringBuilderresult part.
At this point, we have done a substantial refactoring and can admire our progress!
Our statement method is now just a handful of lines of code which are solely responsible for forming the string to be returned. All the actual computation is offloaded to our helper methods, and the code is much easier to understand at a glance.
For those interested, you can read more in the free textbook chapter, as the author says more about what we have accomplished so far with our refactoring.
Ensure that you have submitted a link to your GitHub repo and run the self-tests on MarkUs.
END OF MINI ASSIGNMENT
The rest of the tasks are optional, but they are included here for anyone interested in seeing the steps to finish the full refactoring and implement the new features.
Recall that one of our goals is to implement the new feature of HTML output. With this in mind, our next step is to further split out the steps of i) calculating the data we want to display and ii) actually displaying the data using the specific format we want. Once this division is clear in our code, it will be much easier to add our HTML output feature without copying and pasting large chunks of code.
As the name suggests, our goal is to split the code into distinct phases (i and ii from above).
-
Apply
Extract Methodon the code which corresponds to (ii), which is precisely the entire body of ourstatementmethod! Call the extracted methodrenderPlainTextas they do in the textbook.- This may seem to not accomplish anything at all, but it will make sense in a few more refactoring steps.
-
In Split Phase, we will create an object whose responsibility it is to transfer data between the two phases. We'll introduce this object now and incrementally build up its variables and methods.
- Add the line
StatementData statementData = new StatementData();as the first line of the body of yourstatementmethod. - Hover over
StatementDataand choose the option to create a new class calledStatementData. - Update the call to
renderPlainTextto take instatementData. - Update the method signature for
renderPlainTextto be consistent; IntelliJ will help with this.
- Add the line
In the following steps, our goal is now to extract any code that isn't strictly about rendering the invoice
out of renderPlainText and into our StatementData class. Starting from the top of the method,
the first bit of code will be getInvoice().getCustomer().
-
Refactor so that you can replace
getInvoice().getCustomer()withstatementData.getCustomer(). -
Refactor so that you can replace
getInvoice().getPerformances()withstatementData.getPerformances().- Hint: after you have done this, you should be able to delete the
StatementPrinter.getInvoicemethod. - Make sure the tests (
StatementPrinterTests) still pass.
- Hint: after you have done this, you should be able to delete the
For our next step, we'll try to address the issue that renderPlainText is currently doing computations
involving each Performance object from our instance of Invoice. Our goal in the next steps is to
move all of these calculations into our constructor for StatementData. The StatementData class will
then need to provide a public method to access the list of computed results so that renderPlainText can still
work as before.
Tip: it can be helpful to have multiple files open at once for some refactorings — or for reading these instructions as you are working. Right-clicking the tab for a file at the top of IntelliJ will give you options to "Split and Move Right". Give it a try if you haven't used this editor feature before.
- Edit the loop in
renderPlainTextto usefor (PerformanceData performanceData : statementData.getPerformances()).- Have IntelliJ create this new class
PerformanceDatafor you, then add an instance variable inStatementDataof typeList<PerformanceData>and modify method signatures as needed. - Since we have replaced the loop variable with a new type, you'll notice a number of things now break in the loop body; fix them now by doing the following:
- Add a
getAudiencemethod to thePerformanceDataclass. - Add a
getTypemethod to thePerformanceDataclass. Use this in place of statements likegetPlay(p).getType()inrenderPlainTextHint: you'll need to pass the plays into yourStatementDataconstructor to make this work. - Add an
amountFormethod to thePerformanceDataclass. Use this in place of statements likeamountFor(p)inrenderPlainText. Hint: you'll need to move the logic of theamountFormethod into yourPerformanceDataclass. You can copy+paste the method over and remove its parameter; adjusting the body to address any errors. There should only be a couple places you need to fix. After this is done, you can delete theamountFormethod entirely from theStatementPrinterclass! - Hint: don't forget to populate your list of
PerformanceDataobjects in the constructor forStatementData!
- Have IntelliJ create this new class
The above step required a lot of refactoring since introducing the PerformanceData class causes a lot
of parts of the code in StatementPrinter to need to move.
Make sure the tests (StatementPrinterTests) still pass.
At this point, all that is left is moving over the calculations of totalAmount and volumeCredits.
-
Create a method called
totalAmountin theStatementDataclass and implement it based on the existing logic fromrenderPlainText. TherenderPlainTextmethod should then call this newtotalAmountmethod. -
Create a method called
volumeCreditsin theStatementDataclass and implement it based on the existing logic fromrenderPlainText. TherenderPlainTextmethod should then call this newvolumeCreditsmethod.
Make sure the tests (StatementPrinterTests) still pass.
Now, the logic has been completely moved out of the StatementPrinter class and into the StatementData class!
One last step.
- We no longer need our private instance variables for the plays and invoice, so make the
StatementDataobject we were using an instance variable calledstatementDataand move theStatementData statementData = new StatementData(invoice, plays);line up into the constructor.
Make sure the tests (StatementPrinterTests) still pass.
- Update your
log.txtfile and commit the changes to any files. Make sure to include the string "Task 3.1" in your commit message.
With the Split Phase complete, we can go ahead and finally add our first new feature: HTML output!
-
Uncomment the test in
HTMLPrinterTests.java. -
Subclass the
StatementPrinterclass with a new class calledHTMLStatementPrinter. Override thestatementmethod such that the sample test inHTMLPrinterTests.javapasses. You can refer to page 31 of the textbook for roughly what the code will look like; we have also put a Java copy below. Note that you may need to adjust things slightly depending on what you named things, but your code should be similar.
- Hint: to help debug, we recommend that you set a breakpoint in the test case at the point where the expected and actual are both defined so that you can conveniently compare them character by character. These tests will also be available on MarkUs to double check in case there are any issues with newline differences between MacOS and Windows.
public String statement() {
final StringBuilder result = new StringBuilder(String.format("<h1>Statement for %s</h1>%n",
statementData.getCustomer()));
result.append("<table>").append(System.lineSeparator());
result.append(String.format(" <caption>Statement for %s</caption>%n", statementData.getCustomer()));
result.append(" <tr><th>play</th><th>seats</th><th>cost</th></tr>").append(System.lineSeparator());
for (PerformanceData perfData : statementData.getPerformances()) {
// print line for this order
result.append(String.format(" <tr><td>%s</td><td>%s</td><td>%s</td></tr>%n",
perfData.getName(),
perfData.getAudience(),
usd(perfData.amountFor())));
}
result.append("</table>").append(System.lineSeparator());
result.append(String.format("<p>Amount owed is <em>%s</em></p>%n", usd(statementData.totalAmount())));
result.append(String.format("<p>You earned <em>%s</em> credits</p>%n", statementData.volumeCredits()));
return result.toString();
}- Update your
log.txtfile and commit the changes to any files. Make sure to include the string "Task 3.2" in your commit message.
That is one new feature implemented, but what about handling other play types? We observe that right now we use a conditional to calculate quantities based on the play type. If we want to add another play type, we need to add to our switch statement. This doesn't sound very open-closed now does it!
Intuitively, we should be able to introduce polymorphism to replace this conditional structure.
The following describes how we can start going about accomplishing this.
We start by creating an inheritance hierarchy and applying the Replace Constructor with Factory Method refactoring.
Our desired structure will look something like below (only the factory method is shown; variables and other methods omitted):
- In a new Java file, create a class called
AbstractPerformanceCalculatorwith two instance variables, a performance and a play. - Have IntelliJ generate a constructor for you which takes in and sets the performance and the play by right-clicking
inside the class and choosing
Generate... -> Constructor.
In our StatementData class, before we create a PerformanceData object,
we will delegate calculations to our new AbstractPerformanceCalculator class.
To do this, we will need to create an instance of this class. However, we don't want to directly create this object,
as we will want to allow for an appropriate subclass of AbstractPerformanceCalculator to later be used based on which kind
of play we are dealing with — we'll get to that a bit later.
For now, we will create a "factory function" which will simply return a new AbstractPerformanceCalculator object.
"Function" refers to the fact that it will be a static method, so a class method.
-
In
AbstractPerformanceCalculator, create a new static method calledcreatePerformanceCalculatorwhich has the same parameters as this class constructor and simply returns a new instance ofAbstractPerformanceCalculatorfor now. -
Use
Extract Methodto create a private helper for the snippet of code which creates a newPerformanceDataobject in theStatementDataconstructor. Call this helpercreatePerformanceData. -
Inside this private helper, use the new static
AbstractPerformanceCalculator.createPerformanceCalculatormethod to create a local variable referencing a newAbstractPerformanceCalculatorobject.
- Note: this object won't do anything yet; the next steps will move the calculation of the amount
and volume credits out of the
PerformanceDataclass and into ourAbstractPerformanceCalculatorclass.
Fundamentally, we are going through a sequence of steps to move around lines of code to make it easier to identify the responsibility of each part of the code.
Make sure the tests (StatementPrinterTests) still pass.
- Update your
log.txtfile and commit the changes to any files. Make sure to include the string "Task 4.1" in your commit message.
As we have already noted, we now want to delegate all work related to calculating
the amount and volume credits for a specific performance to our new AbstractPerformanceCalculator class.
To do so, we will identify which variables and methods we want to move
from our PerformanceData class to our AbstractPerformanceCalculator class.
-
Apply the
Move Methodrefactoring twice to move the two methods responsible for calculating the amount and volume credits from ourPerformanceDataclass into ourAbstractPerformanceCalculatorclass. Importantly, don't get rid of the original methods just yet (so copy+paste code but don't actually remove the original methods)! More on this in the next step.- depending on the names of things, you may need to update the variable names in these methods once they are in their new class.
-
Now that the
AbstractPerformanceCalculatorhas the logic for calculating the amount and volume credits, we can remove that logic from thePerformanceDataclass entirely.- Change the signature of the
PerformanceDataconstructor such that it also takes in the amount and volume credits as parameters which ourAbstractPerformanceCalculatorcan now calculate for us. - Add instance variables to store the amount and volume credits; change the signatures of the methods for getting the
amount and volume credits in
PerformanceDatato align with thegetVolumeCreditsandgetAmountconventions for getter methods. Make sure all calls to these methods are updated. - make sure to run the tests (
StatementPrinterTests) at this point to ensure the code is still functionally correct!
- Change the signature of the
-
Update your
log.txtfile and commit the changes to any files. Make sure to include the string "Task 4.2" in your commit message.
Our final step is to incorporate subclasses of the AbstractPerformanceCalculator to handle the differences
between tragedies and comedies. Currently, the differences are realized as conditional statements in our code.
Instead, we will override these methods and pull out the conditional logic into the corresponding subclasses.
- In two new Java files, create empty subclasses of
AbstractPerformanceCalculatorcalledTragedyCalculatorandComedyCalculator.
- when you make each extend its parent class, IntelliJ will flag that you haven't defined a constructor yet, so you can have it automatically do this for you.
- For the method calculating the amount, determine which logic should remain in the super version of the method and which logic should be implemented in the subclass.
- hint: the behaviour is unique to each subclass
-
If you didn't in the previous step, make this method abstract in the
AbstractPerformanceCalculatorclass and makeAbstractPerformanceCalculatoran abstract class. -
Since
AbstractPerformanceCalculatoris now abstract, we will want to take this opportunity to updateAbstractPerformanceCalculator.createPerformanceCalculatorto actually return an instance of the appropriate subclass ofAbstractPerformanceCalculator.
- you can do this with a switch statement similar to what was in the original method for calculating the amount.
- at this point, you should be able to run the tests (
StatementPrinterTests) and have them pass.
- Repeat for step 13 above for the method calculating the volume credits.
- hint: for this one, only one subclass has any extra behaviour, so we won't make this method abstract.
- again, run the tests (
StatementPrinterTests) to ensure everything is still working.
- Update your
log.txtfile and commit the changes to any files. Make sure to include the string "Task 4.3" in your commit message.
And with this final change, your refactoring is complete! We are finally ready to implement the new feature of other types of plays!
You are almost there! The test/java/resources/new_invoices.json and test/java/resources/new_plays.json files
contain additional data which is used for the provided test (NewPlayTypeTests) for this feature.
- Implement the new feature so that this test passes.
- hint: you should only need to modify
AbstractPerformanceCalculator.createPerformanceCalculatorand write two new classes.
The two new play types are history and pastoral. The sample data is shown below.
new_plays.json:
{
"henry-v": {"name": "Henry V", "type": "history"},
"as-like": {"name": "As You Like It", "type": "pastoral"}
}For the new play types, the calculations are provided below for you:
History:
amountFor() {
int result = 20000;
if (getPerformance().getAudience() > 20) {
result += 1000 * (getPerformance().getAudience() - 20);
}
return result;
}
volumeCredits() {
return Math.max(getPerformance().getAudience() - 20, 0);
}Pastoral:
amountFor() {
int result = 40000;
if (getPerformance().getAudience() > 20) {
result += 2500 * (getPerformance().getAudience() - 20);
}
return result;
}
volumeCredits() {
return Math.max(getPerformance().getAudience() - 20, 0) + getPerformance().getAudience() / 2;
}- Update your
log.txtfile and commit the changes to any files. Make sure to include the string "Task 5" in your commit message.
To wrap up, we want to make sure that no other issues made it into our code during our work. Hopefully you were able to deal with most of these as you were coding, but take the time now to clean up any remaining issues.
-
Ensure there are no
CheckStyleissues left in any of your Java files. -
Update your
log.txtfile and commit the changes to any files. Make sure to include the string "Task 6" in your commit message.
- If your code was already clean, just [X] the checkbox on this line and commit that change — again, remembering to include "Task 6" in your commit message.
- Update your
log.txtone final time so that it contains your last "Task 6" commit, then push your completed code to MarkUs.
It is worth emphasizing again that in the refactoring stages, we never changed the functionality of the code: at every step we could run the tests to confirm we really hadn't broken anything. It was only once we finished refactoring that we implemented our new functionality with a relatively small amount of new coding required.
Additionally, because of the refactoring, the code was then structured such that we could confidently work on our new feature with little to no fear of accidentally breaking existing code in the process, since implementing the new features required minimal changes to the existing code and mostly the writing of new, independent code. And, as always, we have tests to provide a sanity check along the way!
You may also have noticed that in moving around the computations, we were moving around variables and methods. In some cases, the work we did in one step was essentially undone in a later step, since we couldn't always predict what direction our next refactoring step may take us in. Importantly, you are hopefully getting more comfortable performing these kinds of refactoring operations with the help of your IDE to identify and suggest how to resolve errors which naturally arise when shuffling around functionality.
We'll end off with a couple nice quotes from the Refactoring textbook's first chapter:
"The true test of good code is how easy it is to change it."
"Code should be obvious: When someone needs to make a change, they should be able to find the code to be changed easily and to make the change quickly without introducing any errors."
As an exercise for later, we encourage you to go back to the original code and try to implement these two new features directly. Compare this to how easy it was to implement the new features in the refactored code.
When you have time after the course, we also encourage you to read the full Refactoring textbook in more detail, as this exercise just scratched the surface of refactoring!



