Consider the following implementation for a RangeCombiner
class.
RangeCombiner
class has methods for adding a new range: combiner.addRange(2.4, 3.5)
will add a new range that represents all numbers from 2.4 to 3.5.isRangeOrderValid()
checks this property for us.Group discussion
Look over the implementation in the handout, it should appear complicated.
Don’t read further until you have thoroughly discussed the two questions above.
In order to address the problems, we are going to have a small refactoring session, and we will use the incremental replacement method: Incrementally replace existing features with the new functionality. Recall the overall steps:
Here are the steps in detail (REMEMBER to CHECK your tests after each step):
Range
class.
addRange
method without changing the external behavior of it. For this reason we want to create a new method. So select the contents (body) of addRange
method, and perform “Extract Method” to obtain a new method called addRangeInternal
. Not a great name but we’ll worry about the name later.addRangeInternal
method. We will use it to create the new Range
class. It has parameters min
and max
now. Perform “Extract Parameter Object” to create a new class using both parameters. You will need to specify that it is an Inner class. Call it Range
.mins
and maxs
arraylists are created. Add a line to create and a new ArrayList
instance named ranges
to hold the ranges we are working with. Make sure to specify its type correctly so it will be able to contain Range
instances.mins
and maxs
with usages of ranges
.ranges
list whenever we update the mins
and maxs
lists.
addRangeInternal
method we are performing adds on the mins
and maxs
lists, for the case where we add the new range at the end. Put after those a line that does ranges.add(range);
, so that we also update the ranges
list.insertValueAtIndexAndFixForward
method. We want to get to a point where this method takes as input a range
instead of min
and max
. This is a 2-step process:
insertValueAtIndexAndFixForward
, namely insertValueAtIndexAndFixForward(range.getMin(), range.getMax(), i);
, and perform “Extract Method” to a method also called insertValueAtIndexAndFixForward
. It will have a range
parameter and an integer i
parameter.insertValueAtIndexAndFixForward
method, which took min
and max
as inputs, and “Inline” that method.range
values whenever corresponding min
and max
values change. Look at the top of the insertValueAtIndexAndFixForward
method. You will see that we add currMin
to the mins mins
list and we add currMax
to the maxs
list. Right below those add instructions, insert a ranges.add(i, range);
line to also update the ranges
list.insertValueAtIndexAndFixForward
:
currMin
and currMax
. Right after those two computations, set range
to equal a call new Range(currMin, currMax)
. Thus when currMin
and currMax
changed, we accordingly changed range
to agree with them. The next thing that happens in the body of that conditional is that we set
some values in mins
and maxs
. We must also set a value in ranges
when that happens, so add a line ranges.set(i, range);
.if
removes some entries from mins
and maxs
. We must remove a corresponding entry from ranges
, so add a ranges.remove(i);
call at the end.mins
and maxs
in favor of ranges
.
mins.get(...)
with ranges.get(...).getMin()
. We will effect this change in three steps:
mins.get(i)
usage, and extract a tempMins
method from it. When the option pops up, tell it to “Keep the original signature”, then tell it so substitute all all occurrences.mins.get
only happen via the tempMins
method, go in the body of this tempMins
method and change it from return mins.get(i);
to return ranges.get(i).getMin();
.tempMins
. It has served its purpose.maxs.get(i)
.mins.getSize()
, and replace it with ranges.getSize()
.insertValueAtIndexAndFixForward
method. We start with the nextMin
and nextMax
methods.
ranges.get(i + 1)
section in one of them and perform “Extract Variable” on it to a new variable called nextRange
. Replace both occurences.nextMin
and nextMax
. They should have had two occurences each.currMin
and currMax
: These should both be obtainable from range
, via range.getMin()
and range.getMax()
instead. As this will slightly change the semantics of the code, the system will not do this automatically for us, we’ll have to do it manually, and we’ll do it one step at a time.
currMin
and currMax
in the call to rangesOverlap
with range.getMin()
and range.getMax()
respectively, and check your tests.currMin
inside the Math.min
and the currMax
instead the Math.max
, and check your tests.range
as new Range(currMin, currMax)
. The values for those need to be the ones from the lines above, so replace the currMin
in the constructor call with its value, Math.min(range.getMin(), nextRange.getMin())
. Do the same for the currMax
, then check your tests.mins
and maxs
using currMin
and currMax
. Now that our range
variable has updated values, replace the currMin
in that mins.set(i, currMin)
with range.getMin()
and similarly for currMax
, then check your tests.currMin
and currMax
be grayed out. Use the Alt-Enter intention menu to “Remove Redundant Assignment” on those two lines.insertValueAtIndexAndFixForward
method. You should now be able to inline the currMin
and currMax
local variables there, with one occurrence each.currMin
and currMax
from this method; everything goes through the various ranges now.rangesOverlap
method. Find where it is being used, and perform “Extract Method” on it to obtain a new rangesOverlap
method that takes two ranges as parameters, instead of four doubles. Then inline and remove the old method, with the four parameters.rangesOverlap
method so that it is a method of its first parameter (probably called range
), then rename the remaining parameter to range
and change the method name to overlapsWith
.overlapsWith
method has these two strange local variables, min1
and min2
. Inline them both. The resulting expression is fairly long, don’t worry about it yet. We’ll clean it up.mins
and maxs
lists. We have three kinds of methods: two kinds of add, a set, and a remove. Make sure that you do NOT delete one of the ranges
operations as you do these steps.
set
calls to both mins
and maxs
, and check your tests.remove
calls. Then the add
calls. Check your tests.mins
and maxs
variable declarations near the top should appear grayed out. Perform the “Safe Delete” refactoring on them.insertValueAtIndexAndFixForward
. Right now it is an extremely long call to the Range
constructor with suitably computed endpoints.
new Range(....);
call to a new method called mergeRanges
.mergeRanges
method to be an instance of its first parameter (probably called range
), we then rename the remaining parameter to range
, and we rename the method to be called mergedWith
.addRangeInternal
, there is a call range.getMax() < range.getMin()
. Perform “Extract Method” on it into an isEmpty
method. Make sure to tell it to “Keep the original signature”. Then it move to an instance method of its parameter, range
.range.getMin() <= ranges.get(i).getMax()
. This seems to compare ranges to see if one precedes the other. We’ll extract it to a method but we need to prepare it first:
other
out of ranges.get(i)
. This is a temporary extraction to get the right parameters to our methods.doesNotFollow
out of range.getMin() <= other.getMax()
(keep the original signature).doesNotFollow
to be an instance of its first argument, and change the remaining parameter to range
.other
that we created.Range
class, we see the isRangeOrderValid
method. The conditional seems to check exactly whether the range at index i+1
does not follow the range at index i
, so replace that conditional with ranges.get(i+1).doesNotFollow(ranges.get(i))
, and check your tests.printRanges
we find the use String.format("%.2f--%.2f", ranges.get(i).getMin(), ranges.get(i).getMax())
.
range
out of ranges.get(i)
(replace both occurrences).String.format(...)
expression to a method getSimpleFormat
with parameter range
, then move it to be an instance method of range
.printRange
method, perform the “Replace with foreach” intention to replace it with a simpler for-each loop.getMin
and getMax
methods altogether and feel better about the fact that the rest of the application does not need to know about min and max.overlapsWith
method. Thinking about it, two ranges will overlap as long as they don’t follow each other, so replace the return value with: this.doesNotFollow(range) && range.doesNotFollow(this);
and check your tests.while
loop in insertValueAtIndexAndFixForward
. The nextRange
variable is used in two places but it is a simple list lookup, so inline it.while
loop no longer had to worry about the range
variable: Once we insert the value in the i
-th index, we should be able to use ranges.get(i)
instead. In order to do that, let’s see what the code currently does with range
: We merge it with the next range, then put the result into the i
-th index. So do the following changes:
range
in range.overlapsWith(...)
with ranges.get(i).overlapsWith(...)
.range
in range.mergedWith(...)
with ranges.get(i).mergedWith(...)
.range
in ranges.set(i, range);
with its value from the previous line, namely ranges.get(i).mergedWith(ranges.get(i + 1))
.range = ...
line which is now grayed out.fixForwardFromIndex
.insertValueAtIndexAndFixForward
method that is currently doing very little.fixForwardFromIndex
, we can see that instead of breaking out of the else case, we can add the test in the conditional as part of the while
loop’s condition. Then we don’t need the if conditional at all. Do that.