Passing hashes instead of method parameters

I see that in Ruby (and dynamically typed languages, in general) a very common practice is to pass a hash, instead of declaring concrete method parameters. For example, instead of declaring a method with parameters and calling it like this:

def my_method(width, height, show_border)
my_method(400, 50, false)

you can do it this way:

def my_method(options)
my_method({"width" => 400, "height" => 50, "show_border" => false})

I'd like to know your opinion about it. Is it a good or a bad practice, should we do it or not? In what situation using this practice is valid, and it what situation can it be dangerous?

51717 次浏览

It is a good practice. You don't need to think about method signature and the order of the arguments. Another advantage is that you can easily omit the arguments you do not want to enter. You can take a look at the ExtJS framework as it is using this type of argument passing extensively.

I think this method of parameter passing is much clearer when there are more than a couple of parameters or when there are a number of optional parameters. It essentially makes method calls manifestly self-documenting.

It is not common practice in Ruby to use a hash rather than formal parameters.

I think this is being confused with the common pattern of passing a hash as a parameter when the parameter can take a number of values e.g. setting attributes of a Window in a GUI toolkit.

If you have a number of arguments to your method or function then explicitly declare them and pass them. You get the benefit that the interpreter will check that you have passed all the arguments.

Don't abuse the language feature, know when to use it and when not to use it.

Ruby has implicit hash parameters, so you could also write

def my_method(options = {})


my_method(:width => 400, :height => 50, :show_border => false)

and with Ruby 1.9 and new hash syntax it can be

my_method( width: 400, height: 50, show_border: false )

When a function takes more than 3-4 parameters, it's much easier to see which is what, without counting the respective positions.

It's a trade-off. You lose some clarity (how do I know what params to pass) and checking (did I pass the right number of arguments?) and gain flexibility (the method can default params it doesn't receive, we can deploy a new version that takes more params and break no existing code)

You can see this question as part of the larger Strong/Weak type discussion. See Steve yegge's blog here. I've used this style in C and C++ in cases where I wanted to support quite flexible argument passing. Arguably a standard HTTP GET, with some query parameters is exactly this style.

If you go for the hash appraoch I'd say that you need to make sure your testing is really good, problems with incorrectly spelled parameter names will only show up at run time.

Both approaches have their own advantages and disadvantages, when you use an options hash replacing standard arguments you lose clarity in the code defining the method but gain clarity whenever you use the method because of the pseudo-named paramaters created by using an options hash.

My general rule is if you either have a lot of arguments for a method (more than 3 or 4) or lots of optional arguments then use an options hash otherwise use standard arguments. However when using an options hash it is important to always include a comment with the method definition describing the possible arguments.

I'd say that if you are either:

  1. Having more than 6 method parameters
  2. Passing options that have some required, some optional and some with default values

You would most probably want to use a hash. It's much easier to see what the arguments mean without looking up in the documentation.

To those of you saying it's hard to figure what options a method takes, that just means that the code is poorly documented. With YARD, you can use the @option tag to specify options:

##
# Create a box.
#
# @param [Hash] options The options hash.
# @option options [Numeric] :width The width of the box.
# @option options [Numeric] :height The height of the box.
# @option options [Boolean] :show_border (false) Whether to show a
#   border or not.
def create_box(options={})
options[:show_border] ||= false
end

But in that specific example there's such few and simple parameters, so I think I'd go with this:

##
# Create a box.
#
# @param [Numeric] width The width of the box.
# @param [Numeric] height The height of the box.
# @param [Boolean] show_border Whether to show a border or not.
def create_box(width, height, show_border=false)
end

I'm sure no one using dynamic languages cares, but think about the performance penalty your program is going to be hit with when you start passing hashes to functions.

The interpreter may possibly be smart enough to create a static const hash object and only reference it by pointer, if the code is using a hash with all members that are source code literals.

But if any of those members are variables then the hash must be reconstructed each time it is called.

I've done some Perl optimizations and this kind of thing can become noticeable in inner code loops.

Function parameters perform much better.

The benefit of using an Hash as parameter is that you remove the dependency on the number and order of arguments.

In practice this means that you'll later have the flexibility to refactor/change your method without breaking the compatibility with the client code (and this is very good when building libraries because you can't actually change the client code).

(Sandy Metz's "Practical Object-Oriented Design in Ruby" is a great book if you're interested in software design in Ruby)

In general we should always use standard arguments, unless it's not possible. Using options when you do not have to use them is bad practice. Standard arguments are clear and self-documented (if properly named).

One (and maybe the only) reason to use options is if function receives arguments which does not process but just pass to another function.

Here is an example, that illustrates that:

def myfactory(otype, *args)
if otype == "obj1"
myobj1(*args)
elsif otype == "obj2"
myobj2(*args)
else
puts("unknown object")
end
end


def myobj1(arg11)
puts("this is myobj1 #{arg11}")
end


def myobj2(arg21, arg22)
puts("this is myobj2 #{arg21} #{arg22}")
end

In this case 'myfactory' is not even aware of the arguments required by the 'myobj1' or 'myobj2'. 'myfactory' just passes the arguments to the 'myobj1' and 'myobj2' and it's their responsibility to check and process them.

Hashes are particularly useful to pass multiple optional arguments. I use hash, for example to initialize a class whose arguments are optional.

EXAMPLE

class Example


def initialize(args = {})


@code


code = args[:code] # No error but you have no control of the variable initialization. the default value is supplied by Hash


@code = args.fetch(:code) # returns IndexError exception if the argument wasn't passed. And the program stops


# Handling the execption


begin


@code = args.fetch(:code)


rescue


@code = 0


end


end