Two refactorings I can’t live without are Introduce Parameter Object and Move Instance Method.
I often find myself introducing new classes to separate concerns using them in a little dance I call “chunking”.
In IntelliJ and Rider or Resharper, these are available as automated refactorings, which saves some time. (In Rider, Introduce Parameter Object is helpfully called “Transform Parameters” for no good reason.)
But when I’m working in dynamic languages – which suffer from a lack of type information – I have to do these refactorings by hand.
Half of the courses I run, I’m either demonstrating in Python or JavaScript, so this comes up a lot. I thought it might be helpful to document these manual refactorings for future reference.
In this example, I’ve been asked to change this code that generates quotes for fitted carpets so that rooms can have different shapes, meaning that there will be different ways of calculating the area of carpet required.
class CarpetQuote:
def calculate(self, width, length, price_per_sq_m, round_up):
area = width * length
if round_up:
area = math.ceil(area)
return area * price_per_sq_m
My solution would be to introduce a class for calculating the room’s area that knows its dimensions. (If you were just thinking “switch statement”, give yourself a wobble.)
I want to introduce a parameter to the calculate method for the room. And I want to do it in teeny, safe steps.
Step #1 – Add a new room parameter
class CarpetQuote:
def calculate(self, width, length, price_per_sq_m, round_up, room=None):
area = width * length
if round_up:
area = math.ceil(area)
return area * price_per_sq_m
By giving room a default value, this code still runs and passes the tests.
Step #2 – Instantiate room in the client code (the tests) as a new class
class Room:
pass
class CarpetQuoteTest(unittest.TestCase):
def test_quote_for_carpet_no_rounding(self):
quote = CarpetQuote()
self.assertEqual(122.50, quote.calculate(3.5, 3.5, 10.0, False, Room() ))
def test_quote_for_carpet_with_rounding(self):
quote = CarpetQuote()
self.assertEqual(130.0, quote.calculate(3.5, 3.5, 10.0, True, Room() ))
Step #3 – Pass in width and length as constructor parameters of Room
class Room:
def __init__(self, width, length):
pass
class CarpetQuoteTest(unittest.TestCase):
def test_quote_for_carpet_no_rounding(self):
quote = CarpetQuote()
self.assertEqual(122.50, quote.calculate(3.5, 3.5, 10.0, False, Room(3.5, 3.5) ))
def test_quote_for_carpet_with_rounding(self):
quote = CarpetQuote()
self.assertEqual(130.0, quote.calculate(3.5, 3.5, 10.0, True, Room(3.5, 3.5) ))
Step #4 – Assign width and length to fields (member variables) of Room
class Room:
def __init__(self, width, length):
self.length = length
self.width = width
Room is now ready to be used in the calculate method.
Step #4 – Replace reference to calculate‘s width and length parameters with references to room‘s fields
class CarpetQuote:
def calculate(self, width, length, price_per_sq_m, round_up, room=None):
area = room.width * room.length
if round_up:
area = math.ceil(area)
return area * price_per_sq_m
We can now do a little cleaning up.
Step #5 – Remove unused width and length parameters from calculate (Safe Delete)
class CarpetQuote:
def calculate(self, price_per_sq_m, round_up, room=None):
Step #6 – Remove redundant default value for room parameter
class CarpetQuote:
def calculate(self, price_per_sq_m, round_up, room):
Okay, that’s some hanging chads dealt with. Let’s look at moving the area calculation to where it now belongs.
Step #7 – Extract area calculation into a separate method
This involves cutting the calculation code and pasting it into the new method as a return value, and replacing that code with a call to the new method.
class CarpetQuote:
def calculate(self, price_per_sq_m, round_up, room):
area = self.area(room)
if round_up:
area = math.ceil(area)
return area * price_per_sq_m
def area(self, room):
return room.width * room.length
We can now see that the area method has very obvious Feature Envy for room.
Step #8 – Move the area method to the Room class
First, I cut the area method and paste it into Room. I then switch the target of the call to area from self to room.
class Room:
def __init__(self, width, length):
self.length = length
self.width = width
def area(self, room):
return room.width * room.length
class CarpetQuote:
def calculate(self, price_per_sq_m, round_up, room):
area = room.area(room)
if round_up:
area = math.ceil(area)
return area * price_per_sq_m
Then I switch the references to room.length and room.width to self.length and self.width. Remember, room and self are the same object.
class Room:
def __init__(self, width, length):
self.length = length
self.width = width
def area(self, room):
return self.width * self.length
The room parameter is now unused. Let’s delete it.
class Room:
def __init__(self, width, length):
self.length = length
self.width = width
def area(self):
return self.width * self.length
class CarpetQuote:
def calculate(self, price_per_sq_m, round_up, room):
area = room.area()
if round_up:
area = math.ceil(area)
return area * price_per_sq_m
Now there’s no need to expose the width and length fields.
Step #9 – “Hide” width and length
Let’s rename these fields to indicate that they should not be accessed from outside Room.
class Room:
def __init__(self, width, length):
self._length = length
self._width = width
def area(self):
return self._width * self._length
Now it’s easy to substitute different implementations of room in the CarpetQuote‘s calculate method. Job done!
One final note: every code snippet here was taken after I’d seen it pass the tests. That’s 13 test runs – and 13 commits – to do this refactoring.
…In case you were wondering what I mean by “small steps”.
(Of course, in IntelliJ or Rider, it would have been a lot fewer steps. That’s the pay-off for automated refactorings, and why I’ll choose my IDE with that in mind.)


