Section 7.5 Discovering classes
Subsection 7.5.1 Discovering classes from code smells
It is time now to return to an earlier question: How do we find the classes our application needs? And how do we evolve our code to use those classes? The answer, as in past design problems, is to look for certain code smells and then apply refactorings meant to help us fix those code smells.
- Long Parameter List is one such smell, when we have a function that has many parameters. Such functions are hard to use, especially if some parameters have the same type. In general any number of parameters above 2 or 3 is cause for concern. One solution is to lump some parameters together into a new class, using the Introduce Parameter Object refactoring. Assuming, that is, that it makes sense for those parameters to go together. Which leads us to:
- Data Clumps. The term refers to data that tends to show up together and be used together. It could be two or more fields which are part of a larger class but they tend to be used together in most methods. It could be two or more local variables in a large function, which tend to be used at the same parts of that function, or passed together to many helper functions. Or it could be the same two or more parameters appearing in many functions. In all those cases, introducing a new class for those fields is the answer. This could be the Introduce Parameter Object refactoring, or it could be other refactorings like Extract Delegate or Extract Superclass.
- Primitive Obsession is another code smell that leads to the creation of new classes. It is visible when a primitive type is used to represent something more complex, and a lot of logic using that type appears all over the place. A good example of that is the letter string representing a letter grade. It is often beneficial to wrap these primitive types into a wrapper class. This can be achieved with some of the other refactorings, as well as the Wrap return value refactoring.
These techniques are often accompanied by move method refactorings, to move appropriate methods to the newly created classes.
To demonstrate these ideas, let us return to our grade-reporting example. Here is the most recent state of our code for it:
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println(processGrades(scanner));
}
private static String processGrades(Scanner scanner) {
int numCourses = 0;
double totalPoints = 0.00;
while (hasMoreEntries(scanner)) {
String letterGrade = processNextGradeRow(scanner);
numCourses += creditsForLetterGrade(letterGrade);
totalPoints += pointsForLetterGrade(letterGrade);
}
double gpa = calculateGPA(numCourses, totalPoints);
return formatResult(numCourses, gpa);
}
private static boolean hasMoreEntries(Scanner scanner) {
return scanner.hasNext();
}
private static String processNextGradeRow(Scanner scanner) {
readPrefix(scanner);
readCourseNo(scanner);
return readLetterGrade(scanner);
}
private static String readLetterGrade(Scanner scanner) {
return scanner.next();
}
private static void readCourseNo(Scanner scanner) {
scanner.next();
}
private static void readPrefix(Scanner scanner) {
scanner.next();
}
private static int creditsForLetterGrade(String letterGrade) {
return letterGrade.equals("W") ? 0 : 1;
}
private static double pointsForLetterGrade(String letterGrade) {
switch (letterGrade) {
case "A": return 4.00;
case "A-": return 3.67;
case "B+": return 3.33;
case "B": return 3;
case "B-": return 2.67;
case "C+": return 2.33;
case "C": return 2;
case "C-": return 1.67;
case "D+": return 1.33;
case "D": return 1.00;
case "D-": return 0.67;
default: return 0;
}
}
private static double calculateGPA(int numCourses, double totalPoints) {
return numCourses == 0 ? 0.00 : totalPoints / numCourses;
}
private static String formatResult(int numCourses, double gpa) {
return String.format("Courses: %d%nGPA: %.2f%n", numCourses, gpa);
}
}
Phew, quite a handful! But let’s start by noticing a "primitive obsession" code smell. There are a number of places with logic related to a letter grade, which is currently a string:
- The
processNextGradeRow
method returns a letter grade string. - The
creditsForLetterGrade
andpointsForLetterGrade
methods use this string to do some calculations.
This is a typical example of primitive obsession: There’s clearly some interesting logic going on here regarding these letter grade strings, but right now it lacks coherence because it is not housed into a specialized class, perhaps a
Grade
class. We therefore want to create that very class. What we want to achieve is this:- The
processNextGradeRow
method will instead return an object of classGrade
, which contains in it a string field with the letter. - The
creditsForLetterGrade
andpointsForLetterGrade
methods will be taking thisGrade
object as their input. Even better, they should probably be methods of theGrade
class in the first place, and not return anything.
That is the goal. The question is how do we get to that goal from where we currently are. We will look at two possible solutions to this, each depending on how we create the new class, in the sections that follow.
Subsection 7.5.2 Wrap return value refactoring
Our first approach starts as follows: We instruct our IDE (or do it manually if needed) to wrap the return value of the
processNextGradeRow
method, to a new class it will create for us, called Grade
. At the end of this the following has occurred:- There is a new class
Grade
with a final string field calledvalue
, that we will rename later, set in the constructor and available via a getter. - In its return line,
processNextGradeRow
creates a newGrade
object from the provided letter grade, and returns that. Its return type is thereforeGrade
. - Calls to
processNextGradeRow
now have a.getValue()
following them, to still do what they used to.
Here’s how the relevant parts of the code now look:
while (hasMoreEntries(scanner)) {
String letterGrade = processNextGradeRow(scanner).getValue();
...
}
...
}
private static Grade processNextGradeRow(Scanner scanner) {
readPrefix(scanner);
readCourseNo(scanner);
return new Grade(readLetterGrade(scanner));
}
That’s far from what we want yet, but it is a first start, and we’ll work with it a bit after codifying the "wrap return value" refactoring steps.
Mechanics of the "wrap return value" refactoring:
Identify the method whose return value you want to wrap. Identify the type of that return value. Create a new class containing a field of that type. Set the value of that field in the constructor via a parameter, and provide a getter method for it. Replace the return lines of the method by calls to the constructor of the new class, passing to the constructor what you would have returned, and instead returning the newly created object. Chain to every call of your method a call to the getter you created in your new class, to retrieve the stored value.
Let us return to our example. We have created the new class, but we are not really doing anything useful with it yet; we get the new grade object but then immediately forget about it by calling the getter. We will start by introducing a new local variable:
while (hasMoreEntries(scanner)) {
Grade grade = processNextGradeRow(scanner);
String letterGrade = grade.getValue();
numCourses += creditsForLetterGrade(letterGrade);
totalPoints += pointsForLetterGrade(letterGrade);
}
Now with that in mind, we want to gradually eliminate the
letterGrade
part. we will inline the letterGrade
value:while (hasMoreEntries(scanner)) {
Grade grade = processNextGradeRow(scanner);
numCourses += creditsForLetterGrade(grade.getValue());
totalPoints += pointsForLetterGrade(grade.getValue());
}
Now we arrive at a situation that you will encounter many times. We have a line like this:
creditsForLetterGrade(grade.getValue());
We want to instead end up with:
creditsForLetterGrade(grade);
So we want to end up with a function that takes a
Grade
parameter, rather than a string parameter. The process goes like this:- Extract a new method out of that code chunk. Name it the same way if possible (the other function takes a different parameter type, so it is OK for them to have the same name).
- In the body of this new function there is a call to the old function. Inline that call.
- If applicable, you may now be able to eliminate the old function (inlining may have done that for you already). If that function had some other usages, you’ll need to think about those usages as well, and adjust accordingly.
In our case, when we perform Extract Method we end up with the new method:
private static int creditsForLetterGrade(Grade grade) {
return creditsForLetterGrade(grade.getValue());
}
And when we inline that inside call we get:
private static int creditsForLetterGrade(Grade grade) {
return grade.getValue().equals("W") ? 0 : 1;
}
This does look a bit weird now, doesn’t it? We’ll fix it up in a moment. But first, we need to do the same process for
pointsForLetterGrade
. This is how that would end up:private static double pointsForLetterGrade(Grade grade) {
switch (grade.getValue()) {
case "A": return 4.00;
case "A-": return 3.67;
...
}
}
So at the end of this process in every place that we were previously accessing the letter grade directly we are instead using a
grade
object and its getValue
method. Or next logical step is to move these methods to the Grade
class itself. We’ll discuss this after looking at an alternative path to the same state. So hit the Undo button enough times to get back to our start state, and let’s give the other approach a whirl.Subsection 7.5.3 Introduce parameter object refactoring
First of all let’s address the question: Why another approach? The simple answer is that this approach works in more general situations, in particular it is suitable in situations where you have more than one parameter, which collectively should become a new class. The wrap-return-value approach creates a class with a single field, not two.
Now let’s see what this approach entails.
Mechanics of the Introduce Parameter Object refactoring
Identify the method whose parameter(s) you wish to extract to a new object. You don’t have to use all the parameters, but you certainly can. Create a new class whose constructor takes exactly those parameters you wish to group up, and stores their values in fields. Create getters for those fields. In the parameter list of your method replace those parameters with a new parameter for the class you created. Anywhere in the body where one of the old parameters was accessed, replace that access with a call to one of the getter methods in the new object, the getter corresponding to this parameter. In the callers of your method, replace the corresponding arguments with a single argument that results from calling the new class’ constructor, passing the arguments to the constructor instead.
Let’s take a look at this in action. We will perform this refactoring in the
creditsForLetterGrade
method. The method will change fromprivate static int creditsForLetterGrade(String letterGrade) {
return letterGrade.equals("W") ? 0 : 1;
}
to
private static int creditsForLetterGrade(Grade grade) {
return grade.getValue().equals("W") ? 0 : 1;
}
And the caller will change from
while (hasMoreEntries(scanner)) {
String letterGrade = processNextGradeRow(scanner);
numCourses += creditsForLetterGrade(letterGrade);
totalPoints += pointsForLetterGrade(letterGrade);
to
while (hasMoreEntries(scanner)) {
String letterGrade = processNextGradeRow(scanner);
numCourses += creditsForLetterGrade(new Grade(letterGrade));
totalPoints += pointsForLetterGrade(letterGrade);
And now that we have the new class in place, we need to use it more consistently, and at least the first of these steps is similar to steps we took before, namely introduce a new variable for the grade object, from the argument passed to
creditsForLetterGrade
:while (hasMoreEntries(scanner)) {
String letterGrade = processNextGradeRow(scanner);
Grade grade = new Grade(letterGrade);
numCourses += creditsForLetterGrade(grade);
totalPoints += pointsForLetterGrade(letterGrade);
}
Now we have a bit of a problem. We need to eliminate the use of
letterGrade
. And the use in the Grade
constructor is easy to replace, we can just inline it. What we need to fix a bit more manually is the use in the call to pointsForLetterGrade
. We need to replace letterGrade
there with its access via the grade
object, namely grade.getValue()
:while (hasMoreEntries(scanner)) {
String letterGrade = processNextGradeRow(scanner);
Grade grade = new Grade(letterGrade);
numCourses += creditsForLetterGrade(grade);
totalPoints += pointsForLetterGrade(grade.getValue());
}
We can now do the same trick we did earlier (introduce method, then inline the old method) to the
pointsForLetterGrade
call, to make that function accept a Grade
parameter.while (hasMoreEntries(scanner)) {
String letterGrade = processNextGradeRow(scanner);
Grade grade = new Grade(letterGrade);
numCourses += creditsForLetterGrade(grade);
totalPoints += pointsForLetterGrade(grade);
}
Next we inline the
letterGrade
variable, then extract a new method from the whole new Grade(...)
call, then inline the old call. You will need to come up with a new temporary name for your method, as two methods cannot vary just in their return value. But after those steps you should end up with our familiar version:while (hasMoreEntries(scanner)) {
Grade grade = processNextGradeRow(scanner);
numCourses += creditsForLetterGrade(grade);
totalPoints += pointsForLetterGrade(grade);
}
As you can see this took a few more steps to carry out. I’ll let you decide which one is easier to grasp. But in general you need to be able to utilize both.
Note as a shortcut: we could have instead performed an "Extract parameter object" refactoring on the
pointsForLetterGrade
, instead of the other way we tried to fix that method. Your IDE should allow you the option to use an existing class when introducing a parameter object, so you can just choose the Grade
class that was already created. This would lead to the following code:while (hasMoreEntries(scanner)) {
String letterGrade = processNextGradeRow(scanner);
numCourses += creditsForLetterGrade(new Grade(letterGrade));
totalPoints += pointsForLetterGrade(new Grade(letterGrade));
}
Then when you extract a variable from
new Grade(letterGrade)
, you can tell it to extract it from both places at once, and arrive at the same place.Subsection 7.5.4 Cleaning up: Moving methods around
We talked about the idea of responsibilities, and grouping similar responsibilities to a class. This becomes particularly clear when we consider a method, what that method does and where it is situated. For example the method
creditsForLetterGrade
is supposed to know how many credits a letter grade is worth (0 or 1). This is knowledge that should belong in the Grade
class, yet right now it exists in our grade reporter class instead. This is evident by the fact that in order for us to achieve the requirements of the method we ask the grade
object a bunch of questions and act accordingly.This is a code smell called feature envy, which ties closely with the tell, don’t ask principle: Our method cares more about this
grade
object than the class in which it currently resides. The remedy for this smell is the Move Method family of refactorings, that we will discuss here.The move method refactoring comes in three flavors:
- Turn an instance method into a static method. This in effect turns the implicit
this
object of the method into an explicit parameter passed into the method. - Turn static method into an instance method. This in effect turns one of the parameters into the
this
object, and relocates the method to the corresponding class. - Move static method from one class to another. This simply relocates a static method from one class to another. It is often an intermediate step as we perform the other two transformations.
Whenever you move a method to a new location, you will need to decide how to handle any methods from the old class that this method needs to get its job done. If there aren’t any then no problems. But if there are, then you will need to decide how to handle that. One option, if applicable, is to move those methods over first. Another is to simply make them public. You will need to decide which one is appropriate for your use case.
Let’s see this in action, consider the
creditsForLetterGrade
method. It is a static method that takes in grade
as a parameter of type Grade
. And luckily for us it doesn’t need anything specific from its current class. We will first move it to the Grade
class as a static method.Mechanics of Move Static Method refactoring
Identify any helper methods the method uses from its current class or package and make them public (or move them first). If the function uses any fields from the current class, you need to first encapsulate those fields so that they are accessible via a public setter method. Copy the class definition and body over to its new home in the other class. Add a class qualifier, likeGrade.doThings()
to any static methods from the old class that the method uses. This should provide you with a working method. Find all places where the old method is used, likely showing up with something likeReporter.doThings()
(or justdoThings()
within the same class), and change them to point to the new method with something likeGrade.doThings()
instead. Remove the old method that is no longer used by anyone.
I will do this to both
creditsForLetterGrade
and pointsForLetterGrade
, moving them both to the Grade
class. Our main while
loop will now look as follows:while (hasMoreEntries(scanner)) {
Grade grade = processNextGradeRow(scanner);
numCourses += Grade.creditsForLetterGrade(grade);
totalPoints += Grade.pointsForLetterGrade(grade);
}
Now we can perform the turn static method into instance method refactoring to these methods, with target the parameter
grade
. This means the following steps:Mechanics of Turn Static Method Into Instance Method refactoring
Identify the parameter that is to become thethis
object for the method If needed, move the static method to the class of that newthis
object. Remove thestatic
keyword from the method, remove the parameter that will become thethis
object, and replace any occurrence of the parameter withthis
, if needed. Find all places where the function is called, identify the argument corresponding to the parameter that was eliminated, and place that as the subject of the function call.
In our example,
Grade.creditsForLetterGrade(grade)
will instead become grade.creditsForLetterGrade()
. So after doing this to both methods, here is how things look:while (hasMoreEntries(scanner)) {
Grade grade = processNextGradeRow(scanner);
numCourses += grade.creditsForLetterGrade();
totalPoints += grade.pointsForLetterGrade();
}
And in the
Grade
class we now have the following methods:int creditsForLetterGrade() {
return getLetterGrade().equals("W") ? 0 : 1;
}
double pointsForLetterGrade() {
switch (getLetterGrade()) {
case "A": return 4.00;
case "A-": return 3.67;
...
}
}
Notice in particular the calls to
getLetterGrade
, which originally were grade.getLetterGrade()
and then this.getLetterGrade()
, and finally shortened to getLetterGrade()
since the "this.
" is implicit. As a further simplification, now that we are in the Grade
class and can access them directly, we’ll inline those getLetterGrade
calls, and have just letterGrade
in their place. As a beautiful side-effect of this, the letterGrade
variable is now truly a private element of the Grade
object and at least for now no one else needs to know about it.Subsection 7.5.5 Replace Method with Method Object refactoring
It is time to introduce one more refactoring, the Replace Method with Method Object refactoring. It is particularly useful when we have a large method that we imagine will need to be broken up into smaller pieces. Typically in such a method we have a number of local variables, which are used throughout the method. This presents a problem whenever we attempt to perform Extract Method, because the extracted methods will no longer have access to the local variables, but the code we are trying to extract may need them. This is exactly what happened earlier on with our
processGrades
method, and the resulting effect was that we had to pass those local variables as parameters into the extracted methods. And if that piece of code was performing a change in one of the local variable values, this extracted function can only perform that change by returning some value, and having that value assigned to the variable in the original function. We did that in a couple of places already. But we also hit a snag, namely the fact that within our while loop we end up changing two local variables, namely totalPoints
and numCourses
. A single method cannot return two values, and as a result we were unable to extract the body of the while
loop into another method, much as I would have liked to do so.The Replace Method with Method Object refactoring solves this problem as follows: It creates a new object, of a new class, and puts the method’s body as the body of a new method of this new object. This is typically followed by elevating all the local variables of this new method into fields of the new object. And as fields can be accessed anywhere within the new class, we can freely extract any methods we like and have them operate on these new fields.
By creating a new class to host our method, the Replace Method with Method Object refactoring allows us to convert the local variables into fields, broadening the scope of those variables from the one method to the whole class, and thus facilitates method extraction. No need to pass parameters, or return values.This refactoring is best done at the beginning of a long function refactoring. Use it when the responsibilities expressed by this function should have a new home, and turning the method into a non-instance method of the current class would not be appropriate.
In our case it is technically a bit late to carry out this refactoring, but we will still get some benefits from that so we are going to go ahead and do it. But if you want to practice, you should go back to a version of our program before we had extracted any methods, perform this refactoring at that point, then perform appropriate Introduce Field and Extract Method refactorings to arrive at a similar place to where we are now.
Mechanics of Replace Method with Method Object refactoring
Identify the method you wish to replace. Create a new class with a constructor that takes the same parameters as your method. Ensure that any methods accessed by your method are visible to the new class, make them public if needed. If the method was not static, add an extra constructor parameter for the old method’sthis
object. In the new class create fields corresponding to the constructor parameters and bind the parameters to them. Create a method in the new class, typically calledinvoke
at first, with return value matching that of the original method and no parameters. In the body of the new method paste the body of the old method, adjusting the implicit uses ofthis
, if needed, to refer to the field you created for it. You may need to qualify any static methods you might have used from the old class (i.e. refer to them by their full name, including the class name). Replace the old method’s body with a call to the new class’ constructor chained with a call to the newinvoke
method.
Let’s carry this out in our case. The function we want to work with is
processGrades
, which currently looks as follows:private static String processGrades(Scanner scanner) {
int numCourses = 0;
double totalPoints = 0.00;
while (hasMoreEntries(scanner)) {
Grade grade = processNextGradeRow(scanner);
numCourses += grade.creditsForLetterGrade();
totalPoints += grade.pointsForLetterGrade();
}
double gpa = calculateGPA(numCourses, totalPoints);
return formatResult(numCourses, gpa);
}
We will start by locating the four functions being called (
hasMoreEntries
, processNextGradeRow
, calculateGPA
, formatResult
), and make them public for now (or package-private as the new class we will create will be in the same package).Then we will carry out the refactoring, calling the new class
Summary
. Its constructor will take as input the scanner
. Here’s what the end result would be:private static String processGrades(Scanner scanner) {
return new Summary(scanner).invoke();
}
All the hard work takes place in the new class:
class Summary {
private Scanner scanner;
public Summary(Scanner scanner) {
this.scanner = scanner;
}
public String invoke() {
int numCourses = 0;
double totalPoints = 0.00;
while (Main.hasMoreEntries(scanner)) {
Grade grade = Main.processNextGradeRow(scanner);
numCourses += grade.creditsForLetterGrade();
totalPoints += grade.pointsForLetterGrade();
}
double gpa = Main.calculateGPA(numCourses, totalPoints);
return Main.formatResult(numCourses, gpa);
}
}
Notice the use of
Main
all over the place, as these methods are all in the Main
class still. I will move them all over to Summary
, as well as any static methods that they referenced. This will make our Main
method look "normal" again.You might rightly ask: What did we gain with all this? What we gained is a new class, whose sole responsibility is to carry out this function, which means that we can utilize the fields of this class to store any information needed, and in our case that means the
numCourses
and totalPoints
variables. Here’s the steps we will take to simplify our Summary class:- Perform Extract Field on the two variables, and initialize them to
0
at their point of declaration. - Turn the various static methods that we moved over into instance methods instead. In this instance it really amounts to just removing the word
static
from their name. This is a key step as it now allows us to access the fields we extracted. - Go through each helper method and inline any parameters whose values are just given by the corresponding fields.
- Also turn the methods back into private if they are not needed anywhere outside this file.
The resulting class should look something like this, condensed to save some space:
class Summary {
private Scanner scanner;
private int numCourses = 0;
private double totalPoints = 0.00;
public Summary(Scanner scanner) { this.scanner = scanner; }
public String invoke() {
while (hasMoreEntries()) {
Grade grade = processNextGradeRow();
numCourses += grade.creditsForLetterGrade();
totalPoints += grade.pointsForLetterGrade();
}
double gpa = calculateGPA();
return formatResult(gpa);
}
public boolean hasMoreEntries() { return scanner.hasNext(); }
public Grade processNextGradeRow() {
readPrefix();
readCourseNo();
return new Grade(readLetterGrade());
}
private void readPrefix() { scanner.next(); }
private void readCourseNo() { scanner.next(); }
private String readLetterGrade() { return scanner.next(); }
public double calculateGPA() {
return numCourses == 0 ? 0.00 : totalPoints / numCourses;
}
public String formatResult(double gpa) {
return String.format("Courses: %d%nGPA: %.2f%n", numCourses, gpa);
}
}
So the big benefit we gained from this is that all our function calls are now simpler, almost no parameters being passed around, ever.
The other advantage is that we can now perform Extract Method on the two lines that update
totalPoints
and numCourses
, into a new method called updateSummaryFor(Grade grade)
. After doing that, we can inline the grade
variable, and while I’m at it also inline the gpa
local variable, both of which are now only used in one place, and arrive at:public String invoke() {
while (hasMoreEntries()) {
updateSummaryFor(processNextGradeRow());
}
return formatResult(calculateGPA());
}
This is really nice, a much simpler
while
loop. and it expresses perfectly what is going on: If there are more entries, read the next row and update the summary with it. And looking at it more, I should also inline the parameter gpa
of the formatResult
method, so that calculateGPA()
is called in there instead, and simply return formatResult()
here.Notice how simple our
invoke
method has become. A while
loop plus a return.Subsection 7.5.6 Breaking a class: The Extract Delegate refactoring
Stepping back for a second, there is something uncomfortable about the new
Summary
class we created. We can see that the fields of the class fall into two "data clumps": There’s a group of methods that use the scanner
, and another group of methods that use numCourses
and totalPoints
. Thinking about it some more, the reasons are clear: This class currently has two very different responsibilities:- Reading a new entry from the file, using the
scanner
. - Keeping track of the summary information and reporting it.
You could argue that the second one of these is really two responsibilities (summarizing vs reporting), but let’s not go there just yet. If you go back to our discussion earlier in this section, we had outlined a third responsibility that had to do with knowing information about a letter grade. We already extracted that information in the
Grade
class. It is time to do the same here. We therefore have to split our fields, and our methods, in two parts. Half of it will stay in this class, half needs to move to another class.In order for this to happen, our current class will need to create or be given an object of the other class, in order to delegate responsibilities to it. This object is called a delegate and the process for achieving this is the Extract Delegate refactoring.
Extract Delegate is used when a set of fields and methods in a class is self-contained and represents a separate responsibility from the rest of the class, and we want to move that set to a new class.
Let’s take a look at the mechanics of it.
Mechanics of Extract Delegate refactoring
Identify the group of methods and fields that are to become the new delegate class. These methods should only use the fields that will move with them. Create the new class, introduce in it the same fields and methods as the old class. In the constructor of the old class, create an instance of the new class, and set it as a field of the old class. This is the delegate object. In the constructor of the old class, whenever one of the fields you are wanting to move is set, instead set the field in the delegate object. Alternatively, pass these as constructor arguments when you created the delegate object. In the old class, in each method that is to be moved, replace the body of the function with a call to the corresponding method of the delegate object. This may include getter and setter methods for the fields that you moved. You can then inline these methods. At this point the fields in the old class that you moved to the new class are no longer needed in the old class, so you can eliminate them.
Let’s carry these steps through. We will extract a delegate object for the
scanner
field and the related methods, to basically create a GradeReader
class. I will start by creating this class:public class GradeReader {
private Scanner scanner;
public GradeReader(Scanner scanner) {
this.scanner = scanner;
}
boolean hasMoreEntries() { return scanner.hasNext(); }
Grade processNextGradeRow() {
readPrefix();
readCourseNo();
return new Grade(readLetterGrade());
}
void readPrefix() { scanner.next(); }
void readCourseNo() { scanner.next(); }
String readLetterGrade() { return scanner.next(); }
}
There’s nothing particularly interesting going on here, it’s basically the same methods we had in the
Summary
class. But now the summary class will change. In the constructor ofSummary
, in addition to setting the scanner
field, we will also create and set a GradeReader
instance called reader
.public Summary(Scanner scanner) {
this.scanner = scanner;
reader = new GradeReader(scanner);
}
And we will change all the methods that we want to delegate, to do just that:
private boolean hasMoreEntries() {
return reader.hasMoreEntries();
}
Then we will inline them. After that we can remove the
scanner
field from the Summary
class, and we are left with the following:class Summary {
private final GradeReader reader;
private int numCourses = 0;
private double totalPoints = 0.00;
public Summary(Scanner scanner) {
reader = new GradeReader(scanner);
}
public String invoke() {
while (reader.hasMoreEntries()) {
updateSummaryFor(reader.processNextGradeRow());
}
return formatResult();
}
private void updateSummaryFor(Grade grade) {
numCourses += grade.creditsForLetterGrade();
totalPoints += grade.pointsForLetterGrade();
}
private double calculateGPA() {
return numCourses == 0 ? 0.00 : totalPoints / numCourses;
}
private String formatResult() {
return String.format("Courses: %d%nGPA: %.2f%n", numCourses, calculateGPA());
}
}
One final improvement: Since the
scanner
parameter in the constructor of Summary
is used solely for passing it to the GradeReader
constructor, why don’t we instead create that GradeReader
back in Main
, and pass that in to the Summary
constructor instead. We can achieve this by performing Extract Parameter. So mechanically it would now look like this:// In Summary.java
public Summary(GradeReader reader) {
this.reader = reader;
}
// In Main.java
private static String processGrades(Scanner scanner) {
return new Summary(new GradeReader(scanner)).invoke();
}
It is reasonable to ask: Should we do this? The question really depends on who should be responsible for creating the
GradeReader
instance. And we will return to that question at a later time.We are pretty much done, but I would like to do one more cleanup, that I should have done a while back. When we moved our methods to the
Grade
class, and started writing things like grade.pointsForLetterGrade
, we should have thought about it more, and done some renaming. Now that the grade
object is there, a simple grade.getPoints()
would probably do better. And similarly grade.getCredits()
instead of grade.creditsForLetterGrade()
. I will do those two rename refactorings. This is something you should think about every time you move methods around to their new homes; the old names may no longer be suitable.