Removing Argument Order Dependency in Ruby

15 OCTOBER 2013 | @ahsan_s | Software | Software | 1500 | 2
Chess B - Flickr.com Image
 Chess B © 2010 DavidR_

About fifteen or so years ago when I first read the bible on Refactoring (Refactoring: Improving the Design of Existing Code, by Martin Fowler, Kent Beck, et al.), the first chapter had a couple of pages of example code. It was pretty innocuous looking, something a reasonably experienced developer would write, I thought at that time. Nothing jumped at me that could be an obvious red flag. As I kept on reading, the authors revealed about 23 or so different items that could be improved to make the program better. I was floored! (Following few days were marathon reading of the rest of the book!)

Recently I was reading Sandi Metz' excellent book Practical Object-Oriented Design in Ruby, where I came across a class that at first glance looked just fine to me, but the author went on to improve the class by removing argument order dependency, which reminded me of my first reading of the Refactoring book above. I also came across somewhat similar concept in Avdi Grimm's eBook Confident Ruby. This article, which is heavily influenced by the above mentioned authors, is to convey the techniques I learned.

As the title says, our topic is removing argument order dependency. Let's start with our very innocent looking Employee class from our last post, with a new field bonus with a default value.

Employee - v1 [original]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# A simple Employee class
class Employee
  attr_reader :first_name, :last_name, :title, :age, :bonus

  # everyone gets a 5% bonus unless specified otherwise
  def initialize(fname, lname, title, age, bonus = 5)
    @first_name = fname
    @last_name  = lname
    @title      = title
    @age        = age
    @bonus      = bonus
  end

  # A string representation of the Employee object
  def to_s
    "#{first_name} #{last_name}, #{title}, #{age}, #{bonus}%"
  end
end


Whenever we're developing classes, our goal should be to reduce coupling between them. In other words, the less a class knows about other classes, the smarter it is considered to be. But then, a class that lives in isolation, oblivious of the rest of the world is not very useful either.

Did we just contradict ourselves? Not really. Albert Einstein once said "Everything should be made as simple as possible, but no simpler.” Same principle applies to class design.

In order to create an Employee object, one would have to know not only what it is composed of (fname, lname, title, etc.), but also in what order they appear:

employee = Employee.new('Anita', 'Baker', 'President', 48, 20)


But we'll soon see that even though it is necessary to know the arguments, the order in which they appear is an unnecessary detail that can be eliminated.

Below we'll rewrite our Employee class, but replacing the arguments with a hash:

Employee - v2 [hash]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# The Employee class with the arguments replaced by a hash
class Employee
  attr_reader :first_name, :last_name, :title, :age, :bonus

  # everyone gets a 5% bonus unless specified otherwise
  def initialize( args )
    @first_name = args[:fname]
    @last_name  = args[:lname]
    @title      = args[:title]
    @age        = args[:age]
    @bonus      = args[:bonus] || 5
  end

  # A string representation of the Employee object
  def to_s
    "#{first_name} #{last_name}, #{title}, #{age}, #{bonus}%"
  end
end


# Create an Employee object
employee   = Employee.new(
    :fname => 'Anita',
    :lname => 'Baker',
    :title => 'President',
    :age   => 48,
    :bonus => 20)


Chess Silhouette - Flickr.com Image
 Chess Silhouette © 2008 Satya!!!

Where hash keys are used to extract the values from the argument (args). Also notice that we used Ruby's || method (line 11), which works as 'or' condition. But we'll get back to this soon. Let's review what we gained (or lost) by replacing the argument list by a hash:


  • It removes argument order dependency. Creating an Employee object no longer requires the knowledge of the order of the arguments.
  • The Employee class is now free to add new arguments and defaults without breaking any code that depends on the class.
  • It adds verbosity. But it is a small price to pay compared to the benefit of reduced risk that changes in this class will break dependent code.
  • Even though it removed having to know the argument order, but now one is required to know the exact argument names (i.e., one has to know that 'first name' is fname, 'last name' is lname, etc.). But this serves as documentation of the code, which future maintainers of the code will appreciate.


Have you noticed a potential future problem with using the || method for defaults? It is good for simple non-boolean defaults. For a missing :key, args[:key] returns nil, and the || method substitutes the supplied default value instead. But when (e.g., for boolean values) we need to distinguish between nil and false, this technique will not work (as the || method does not distinguish between nil and false, and returns the supplied default in both cases). Consider the following line of code:

@boolean_var = args[:boolean_key] || true

If :boolean_key is missing in args, it'll set @boolean_var to true, as expected. But it'll do the same if :boolean_key is provided but set to false.

One solution to the problem is to use the fetch method to set the default, as shown below:

1
2
default_value  = true
@boolean_var   = args.fetch(:boolean_key, default_value)

fetch will correctly extract any value (even false, or nil) passed using the :boolean_key in args, and only use the default_value if :boolean_key is missing. (If fetch is called without a default value, it'll still fetch the value associated with the key as long as it is found. Otherwise it'll raise a KeyError exception).

Armed with this information we rewrite our Employee class below:

Employee - v3 [fetch]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# The Employee class that uses 'fetch' to extract keys with defaults
# (Notice how @bonus is initialized in line 12 below)
class Employee
  attr_reader :first_name, :last_name, :title, :age, :bonus

  # everyone gets a 5% bonus unless specified otherwise
  def initialize( args )
    @first_name = args[:fname]
    @last_name  = args[:lname]
    @title      = args[:title]
    @age        = args[:age]
    @bonus      = args.fetch(:bonus, 5)
  end

  # A string representation of the Employee object
  def to_s
    "#{first_name} #{last_name}, #{title}, #{age}, #{bonus}%"
  end
end


# Create an Employee object (same as before)
employee   = Employee.new(
    :fname => 'Anita',
    :lname => 'Baker',
    :title => 'President',
    :age   => 48,
    :bonus => 20)

This is perfectly fine the way it is. However, Sandi Metz shows in her book how to further improve a class like this. One can separate the default values in to a separate method and merge it with the incoming hash. The example is shown below, where merge has the same effect as fetch. The defaults get merged only if the corresponding keys are missing from the hash.

Employee - v4 [merge]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# The Employee class that specifies defaults
# by merging args with defaults hash
class Employee
  attr_reader :first_name, :last_name, :title, :age, :bonus

  # everyone gets a 5% bonus unless specified otherwise
  def initialize( args )
    args        = defaults.merge(args)
    @first_name = args[:fname]
    @last_name  = args[:lname]
    @title      = args[:title]
    @age        = args[:age]
    @bonus      = args.[:bonus]
  end

  # keys with defaults separated as a hash
  def defaults
    { :bonus => 5 }
  end

  # A string representation of the Employee object
  def to_s
    "#{first_name} #{last_name}, #{title}, #{age}, #{bonus}%"
  end
end


# Create Employee objects
employee1  = Employee.new(
    :age   => 48,
    :fname => 'Anita',
    :lname => 'Baker',
    :title => 'President',
    :bonus => 20)
employee2  = Employee.new(
    :title => 'Business Analyst',
    :lname => 'Hamilton',
    :fname => 'Helen',
    :age   => 42)

# Show what they look like
employee1.to_s
employee2.to_s

# Output:
#  => "Anita Baker, President, 48, 20%"
#  => "Helen Hamilton, Business Analyst, 42, 5%"
Checkmate - google.com


This technique of isolating the defaults is particularly useful when the defaults are more complex than simple strings or numbers. Obviously the order in which the key/value pairs appear in the hash is irrelevant.


Unless defaults are more complex, choosing between versions 3 and 4 above is mostly a matter of style and personal preference.






References:

 
   1

Refactoring: Improving the Design of Existing Code by Martin Fowler, Kent Beck, et al., 1999
 
 
   3