Sneaky Long Parameters, Hiding in the Hashes
Sneaky Long Parameters, hiding in the Hashes
First off, welcome to all new readers. Enough of you have joined in the past two weeks that I feel I should restate the central premise of this whole endeavor: I have no idea what I’m doing. I talked about this in an earlier newsletter about practice, should you want to read more about how I am dumb.
That done, let’s get into the next code smell from Fowler & Beck’s list: Long Parameter List.
Long parameter lists are hard to understand, because they become inconsistent and difficult to use, and because you are forever changing them as you need more data.
Honestly, I think this is largely accepted as a given. At least in the Ruby community. This is driven by testing, I suspect. Brittle tests that require extensive changes every time you change the parameters in your method? No thanks! I only rarely see methods with an arity greater than 3.
In fact, I see a lot of methods with an arity of 1 or 2. They look like this
def be_awesome(options = {})
foo = options[:bar]
baz = options.fetch(:bat, false)
#... etc, for many many lines
end
Who knows how many parameters are hiding in options
. Yes, even with arity 1 a method can still have Long Parameter List. Yes, using a hash saves you the trouble of remembering the exact order of your parameters. And it lets you choose what to do with missing parameters. But on the other hand, a hash isn’t that different from an elevator in a spooky story – there’s always room for one more.
And each parameter you stick in there is totally hidden. I can’t look at the signature for that method and tell what parameters are required, which are optional or even what’s allowed. And the complexity only increases if your method accepts two parameter hashes.
Long Parameter List usually points to an unidentified collaborator. If you haven’t looked at this week’s Design is Refactoring post, the final code solution shows one fix to this problem: extract those parameters out to to an object. I only went so far as extracting out the configuration to an OpenStruct, but you could go as far as moving config into its own independent class. You probably should, since OpenStruct responds to everything, which sometimes introduces weird surprises.
My approach most closely matches the Introduce Parameter Object pattern in Refactoring (p. 295). I took all of the parameters, bundled them into their own object and simply passed the object to the method. But that’s not the only way to solve the problem. I could have solved my beer problem this way:
class BeerSong < SimpleDelegator
def initialize
__setobj__ (SongFactory.build(self))
end
def max_bottles
99
end
def min_bottles
0
end
#...and so on
end
This approach is the Preserve Whole Object pattern (p. 288). Instead of passing in a new object with the parameter methods, I’ve added the parameter methods to myself and can just pass myself in.
There’s even a third fix to Long Parameter List, Replace Parameter with Method (p. 292). If my BeerSong code did:
class BeerSong
def initialize
SongFactory.new(self, drink_name)
end
def drink_name
self.class.to_s.sub("Song","")
end
end
class SongFactory
def initialize(song, drink_name)
@song = song
@drink_name = drink_name
end
end
There’s no good reason to pass drink_name
as a parameter. In Replace Parameter with Method, we’d change the code to:
class BeerSong
def initialize
SongFactory.new(self)
end
end
class SongFactory
def initialize(song)
@song = song
@drink_name = @song.drink_name
end
end
Short parameter lists! Good for you, good for your code, good for anyone who has to work with your code. Think about it, won’t you?
Links?
Nope. I wish I had exciting links for you this week, but I do not. I think 3 different plagues have visited my house in the last week, so I have not been finding the fun links that I normally would.
Until next time, true receivers.