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.