Refactoring can therefore be thought of as serving two main uses:
Refactoring takes many forms, and a more precise list of these refactorings is described on this page as well as the refactoring book.
As an example of this process, consider the extremely straightforward but quite long-winded solution to the grade-processing activity on the first page of the handout.
Group activity: Discuss what features of this code make it hard to read, and possibly hard to change in the future. Do this before reading on.
So let’s list a number of problems here:
scanner
class that are by themselves somewhat obscure. We could use a comment for them, or we can also extract them into methods and use the method names to describe their intent.We can start with some simple changes:
t
to total
. We need to do this consistently, and also to make sure there isn’t already a variable named total
. A good rule of thumb if you do it manually is this: Change its name in its declaration, then find all the places that the compiler complains about. Note how many places this had to change in, phew lots of work!c
to courses
. This counts the number of courses that we count towards the gpa.l
stands for a letter
grade so we’ll use that name instead. We do this using the automated refactoring menu.switch
statement, and extract it into a separate method. Since the switch statement changes the total
value, we will need to make it return an int
. And really if we notice the various statements like t += 1.33;
we probably want our function to simply return the 1.33
, and do the addition at the end. So our code will say something like: total += getGradeForLetter(letter);
. We do these changes gradually: First we create the new method, copy the code over and fix any syntax errors. Then we replace the original code, and make sure the tests still pass.!letter.equals("W")
is really meant to determine if the letter grade should count for credit. We therefore want to replace it with a method call: countsForCredit(letter)
courses == 0 ? 0 : total / courses
computes the total gpa, so we’ll extract it into a function computeGPA
.scanner.next("\\s*\\w+\\s*");
lines at the beginning. Actually these two lines serve different purposes: The first reads the course department prefix, while the other reads the course number and letter grade. We extract them in two function readPrefix
and readCourseNo
. In the future, the code for these might no longer be the same.scanner.next("[ABCDFW][+-]?");
pattern reads a letter grade. We again extract it to a readLetterGrade
method.if (scanner.hasNewLine()) ...
basically reads to the end of the current line. We should extract this to a method as well: readToEndOfLine()
.String.format("Courses: %d\nGPA: %.2f\n", courses, gpa);
phrase should probably also be its own method, called formatSummary
.At this point our code looks as in the second page of the handout. Notice how much more readable the main function’s operations are.
Looking at all these, a couple of things stand out that suggest creating some new classes:
courses
, total
and gpa
are often used together. It would make sense for them to all be part of the same class. In fact, since gpa
is simply computed from the other two, it would make sense for such a class to basically maintain the courses and total, but to be able to report the gpa when asked. Perhaps such a class could be called Summary
.Grade
class.GradeReader
.Let’s see how we might go about creating these classes.
Summary
class. We will start by performing “Extract Parameter Object” on the computeGPA
method, to turn those two parameters into one summary
object of a new Summary
inner class of Main
.new Summary(courses, total)
argument inside the computeGPA
method. We extract it to a variable summary
.courses
and total
around. We gradually replace them with summary.courses
and summary.total
after we move the creation of summary
to just before the while loop. In the process, we have to tell the system that the two fields should not be final.Summary
constructor.Summary
class, we inline the two parameters in the constructor, as their value should really always start at 0. We then move the initializations to the declarations, and delete the now empty constructor (it will use the default constructor).computeGPA
method should really be an instance method of the Summary
class, we move it there and inline its use of getCourses
and getTotal
.formatSummary
method. But we cannot yet until it has the summary
as it parameter. We start by inlining the gpa
local variable.formatSummary(summary.courses, summary.computeGPA());
to get format
method with summary
as its parameter. Then we inline the old formatSummary
method.format
method to an instance method of the Summary
class, and inline any getters used in its body.courses
and total
values. Thinking about it more, these should happen in a single update, something like summary.add(units, points)
. Or even better, just summary.addGrade(letter)
. This letter will probably become the grade later on. So we extract a method from those lines and then move it to the summary
class.Summary
class was an inner class of Main
. We now move it to its own file, which means we have to make a number of our methods public (or at least package-protected).Our code now looks like the third page of the handout.
Now we proceed with our second class extraction. It looks like it might be nice to have the concept of a grade as more than a single letter, namely an entity that has some functions. Maybe later we can turn it into an enum
, but for now it would be good to simply have a Grade
class.
addGrade
method in Summary
, which currently takes as input a letter
. We perform “Extract Parameter Object” on it to turn that letter
into a Grade
class as an inner class of Summary
.addGrade
from which we can extract new methods of the Grade
class: Main.getGradeForLetter(grade.getLetter());
should become a getPoints
method and then moved to the grade
instance. And Main.countsForCredit(grade.getLetter())
should be extracted to a countsForCredit
method and then moved to the Grade
class.Main...
methods that are no longer needed.Grade
to the upper level and adjust the access modifiers of some of its methods.getLetter
is used only internally in Grade
, and we inline it.We will also handle the processing steps. We effectively want to replace all uses of the scanner
with uses of a newly-created Processor
, with the scanner as part of its constructor. In order to achieve this, we’ll have to perform a step that by itself does not appear useful, namely we’ll extract the whole while
loop into a single new method. This is a temporary step so that we have a place where we can perform “Extract object”.
while
loop to obtain a processAll
method.scanner
parameter of the processAll
method. We then perform “Inline” on the processAll
method to eliminate it.hasNext
, then move it to be an instance method of processor.readPrefix(processor.getScanner())
to a method called readPrefix
, for readCourseNo(processor.getScanner());
to a method called readCourseNo
, for readLetterGrade(processor.getScanner());
to readLetterGrade
and finally for readToEndOfLine(processor.getScanner());
to readToEndOfLine
. We convert each to an instance method of Processor
. We then inline the Main...
bodies of these new Processor
methods, to remove the original methods that are still in the Main
class.getScanner
and eliminate the method from Processor
. The rest of the world does not need access to our scanner.while
loop. It seems that there are at least four lines there that concern how the processor will process a line, in terms of the order in which events will happen. That really ought to be in the processor. So we will move it there. The conversion of a letter to a Grade
should also be a part of that, but right now it is intertwined with the addGrade
call. We start by extracting a grade
variable from the new Grade(letter);
expression. Then we grab all but the last line of the body of the while
loop, and perform extract method for it to a method getNext
which we then convert to an instance method of the processor. We then inline the grade
variable we temporarily created.Processor
class to the upper level.And now we see the final form of our code, nicely separated into four classes. Notice how simple the processGrades
method has become: