• No results found

Removing Arguments

In document 99 Bottles (Page 191-196)

“ The ideas of testing, and of testing first, have won the hearts and minds of

5. Separating Responsibilities

5.2.4. Removing Arguments

Now that the old Bottles class fully uses BottleNumber, the existing tests serve

as a safety net for changes to the new class. This means that you can now undertake improvements in the new code.

Although BottleNumber works, parts of it are annoyingly redundant. The

problem is that even though instances of BottleNumber know their number, its

methods continue to require number as an argument. To illustrate, here are

the two container methods:

Listing 5.15: Redundant Arguments

Line 4 above gets a new BottleNumber and asks for its container. Doing so

1 class Bottles 2 # ...

3 def container(number)

4 BottleNumber.new(number).container(number)

5 end 6 # ... 7 end 8 9 class BottleNumber 10 attr_reader :number

11 def initialize(number)

12 @number = number

13 end

14

15 def container(number)

16 if number == 1 17 "bottle" 18 else 19 "bottles" 20 end 21 # ... 22 end 191 5.2.4. Removing Arguments

requires two references to number. The initialize method (invoked by new

and defined on line 11) and the container method (line 15) both require a number argument.

The point of the Primitive Obsession/Extract Class refactoring is to create a smarter object to stand in for the primitive. This smarter object, by

definition, knows both the value of the primitive and its associated behavior. Because the new BottleNumber class holds the right number, the methods in

BottleNumber don’t need to take an argument, and invokers of these methods

could be relieved of their obligation to pass a parameter.

Now that BottleNumber is fully connected to Bottles, it’s safe to start making

these improvements. Notice that if you’re willing to simultaneously alter both the senders and the receivers every message, it’s easy to make this change. For example, you could fix the container method by changing line 4

above to remove the parameter being passed to container, while

simultaneously deleting the argument from line 15. If you make both of these changes at once, and then save and run the tests, the tests will pass. Keep in mind that is a multi-line change. Some problems are so simple that it’s easiest just leap in and make such a change, but others are so complex that it isn’t feasible to fix everything at once. In real-world applications, the same method name is often defined several times, and a message might get sent from many different places. Learning the art of transforming code one line at a time, while keeping the tests passing at every point, lets you undertake enormous refactorings piecemeal. This small problem is a good place to practice this technique, in preparation for later tackling bigger ones. Back in Chapter 3, you had to add an argument to a method that was already being called without one. This is the opposite problem: here you need to remove an argument from a method that’s currently being invoked with one. Whether arguments are being added or removed, the trick is the same; you must change the method definition to temporarily set the argument to a default. There are a several ways to accomplish this. The following technique is the most direct, but requires a short refresher on Ruby syntax. 192 5.2.4. Removing Arguments

Consider container, repeated again below. This method takes a number

argument. Remember, however, that the BottleNumber class itself responds to

the number message. Now answer this question: On line 4 below, does number

refer to the argument, or to the message?

Listing 5.16: BottleNumber Container Redoux

Ruby is perfectly happy to allow the same name to be used for different things, and to infer which you mean based on context. In the code above, the programmer clearly intends for number on line 4 to refer to the number

argument from line 3, and that’s exactly what Ruby does. The number on line

4 is interpreted as a reference to the method’s argument rather than as a send of the number message.

Armed with this knowledge, you can guess that removing the argument from the method definition would cause Ruby to interpret line 4 as a send of the

number message. This is your goal, but unfortunately, the Bottles class is still

sending container(number), so this change breaks the tests.

The trick to working your way forward under green, while making only one- line changes, is to alter the name of the argument to something other than number, and simultaneously give it a default. Line 3 below contains that change: Listing 5.17: Renamed Argument 1 class BottleNumber 2 # ...

3 def container(number)

4 if number == 1 5 "bottle" 6 else 7 "bottles" 8 end 9 end 10 # ... 11 end 1 class BottleNumber 193 5.2.4. Removing Arguments

Above, the number argument for container has been renamed to delete_me

and assigned a default of nil. That change turns the number reference on line

4 into a message send, which allows this method to depend upon a message sent to itself rather than an argument passed by someone else.

Now that the argument is optional, turn your attention to senders of

container. In this application there’s only the one in Bottles, shown here:

Listing 5.18: Forward With Redundant Arguments

Removing the number parameter from the container message invocation on

line 4 results in this code:

Listing 5.19: Forward Without Redundancy

2 # ...

3 def container(delete_me=nil)

4 if number == 1 5 "bottle" 6 else 7 "bottles" 8 end 9 end 10 # ... 11 end 1 class Bottles 2 # ...

3 def container(number)

4 BottleNumber.new(number).container(number)

5 end

6 # ... 7 end

1 class Bottles 2 # ...

3 def container(number)

4 BottleNumber.new(number).container

5 end

6 # ...

194 5.2.4. Removing Arguments

Once you have located and removed the parameter from all of its senders, the container method definition no longer needs to take an argument. You

can now return to BottleNumber and remove the delete_me argument and

default, as on line 3 below: Listing 5.20: BottleNumber Container Method Without Argument Here’s a recap of the steps for removing an argument using one-line changes. 1. Alter the method definition to change the argument name and provide a default. Start by changing the existing argument name to anything other than what it currently is. Using delete_me will help you remember to delete

the argument when you’ve updated all of the senders. The value of the default does not matter, so it’s common to use nil. In the example above:

became: 2. Change every sender of the message to remove the parameter. In the example: 7 end 1 class BottleNumber 2 # ... 3 def container 4 if number == 1 5 "bottle" 6 else 7 "bottles" 8 end 9 end 10 # ... 11 end

def container(number)

def container(delete_me=nil)

195 5.2.4. Removing Arguments

became:

3. Finally, delete the argument from the method definition. So, finally:

became:

As you can see, despite the length of the explanation, the technique is simple, and involves only three steps. Having practiced on container, the other

methods will easily bend to your will. You can now follow this process to remove the number argument from the remaining methods in BottleNumber.

If you do this refactoring yourself, you’ll find that quantity and action work

as expected, but that when you change pronoun, the tests begin fail.

5.2.5. Trusting the Process

In document 99 Bottles (Page 191-196)

Related documents