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: