A Guide to Method Chaining
Method chaining is a very handy technique for invoking multiple methods at once. Not only does it improve the readability of the code, but it also reduces the amount of code needed when interacting with a class or an instance of a class.
You have probably already interacted with method chaining in one way or another, especially if you have used ActiveRecord. Remember doing something like this Model.where(…).order(…)
? Yep, that is method chaining. In this article you will learn how to apply the same features to your own code.
This article will be divided into two simple parts:
- Introduction to method chaining
- Method chaining at the class level
The first part will cover the basics of method chaining, with step-by-step examples; and the second part will explain how to do method chaining at the class level, be it via explicit method declaration of class methods or by using and extending an additional module.
Are you ready to embark into this awesome journey? Let’s go!
Introduction to Method Chaining
For the introductory part we are going to be designing a class that will contain a few methods that can be eventually chained together, and one method that outputs a message. Let us start with our basic class structure:
class Person
def name(value)
end
def age(value)
end
def introduce
end
end
Nothing much here but this will serve as the base for this example. As you can see, it contains three methods: name()
, which will take an input value; age()
, which also takes an input value; introduce
, it will not take an input value. With this basic information at hand, let us implement the next step.
We are now going to store the input values for name()
and age()
in instance variables so that we can access them later down the road:
# ...
def name(value)
@name = value
end
def age(value)
@age = value
end
# ...
Great, with this addition to our methods, let us try method chaining and see what happens:
> person = Person.new
# => #<Person:0x007fb18ba29cb8>
> person.name('Baz')
# => "Baz"
> person.name('Baz').age(21)
NoMethodError: undefined method `age' for "Baz":String
As expected, it does not work. The reason being is because the methods are just returning a value – a string in this case. We need to modify our methods once again, and this time, we are going to be returning something else: self
.
By returning self
we are allowing instance methods to be chained together, just like that! Easy huh?! Okay, it is time to modify our methods again:
# ...
def name(value)
@name = value
self
end
def age(value)
@age = value
self
end
# ...
Fantastic, what will happen if we try to chain our method calls?
> person = Person.new
# => #<Person:0x007ff202829e38>
> person.name('Baz')
# => #<Person:0x007ff202829e38 @name="Baz">
> person.name('Baz').age(21)
# => #<Person:0x007ff202829e38 @name="Baz", @age=21>
It worked! Congratulations, you have just implemented method chaining in Ruby! Now, we need to modify our introduce
method and have it do something:
# ...
def introduce
puts "Hello, my name is #{@name}, and I am #{@age} years old."
end
# ...
With all of our code in place, what do you think will happen when we call introduce
in our method chain?
> person = Person.new
# => #<Person:0x007fd079085ba0>
> person.name('Baz').age(21).introduce
# => Hello, my name is Baz and I am 21 years old.
Yep, it worked as expected. Notice that the introduce
method needs to be at the end of the chain because it does not return an instance of self
.
Here is the complete code for this example, which I have also contributed to Wikipedia under the Method chaining article:
class Person
def name(value)
@name = value
self
end
def age(value)
@age = value
self
end
def introduce
puts "Hello, my name is #{@name} and I am #{@age} years old."
end
end
As an exercise for the reader, I encourage you to modify the introduce
method and make it output the text based on the data available. For example, if only @name
is available, you would print the sentence without the age part. If only @age
is available, you would print the sentence without the name part.
Method Chaining at the Class Level
We have just learned how to chain our method calls when invoking methods on an instance of a class. But what about chaining the methods at the class level, similar to Model.where(…).order(…)
? It is pretty much the same process, except that our method declaration will be at the class level instead of the instance level.
Let us go ahead and do just that, let us design a class that contains class methods that can be chained together. The basic structure for this example will be something like this:
class Speaker
class << self
def say(what)
@say = what
self
end
def drink(what)
@drink = what
self
end
def output
"The speaker drinks #{@drink} and says #{@say}"
end
end
end
Pretty simple, right? Notice that all of these three methods are class methods. I could have declared them individually with def self.say()…
but for multiple declaration of class methods I feel that it is more concise to have a single class << self
declaration and declare all of the class methods inside of it.
Great, let us play with it now:
> Speaker.say('hello').drink('water').output
# => The speaker drinks water and says hello
And there you have it, worked like a charm.
Another way to accomplish this without having to explicitly declare the class methods would be to use a module and extend
it in our class. By doing this we expose the methods in the module as class methods in our Speaker
class:
module SpeakerClassMethods
def say(what)
@say = what
self
end
def drink(what)
@drink = what
self
end
def output
"The speaker drinks #{@drink} and says #{@say}"
end
end
class Speaker
extend SpeakerClassMethods
end
And it should work exactly as having explicitly declared the class methods:
> Speaker.say('hello').drink('water').output
# => The speaker drinks water and says hello
Great, it worked. As another exercise for the reader, I encourage you to modify the output
method and make it output the text based on the data available, similar to the previously suggested exercise for the reader.
Conclusion
This article has demonstrated how simple and easy it is to incorporate method chaining in Ruby, be it at the instance level or the class level. It also showed the different ways that we can accomplish the same results by using different techniques when adding class-level method chaining. And I hope that it has provided you with enough information for you to be able to apply the same techniques to your own code.