Words and Code

One writer’s journey from words to code.

Delegating All of the Things With Ruby Forwardable

It was the best of times. It was the worst of times. It was…refactoring time! Well it was for me yesterday, at least.

Refactoring your own code has a great payoff at the end, but boy, does it take some work to get there. Something I’ve noticed about my own code recently is that I’m now able to know that something needs to be refactored pretty easily. I’ve been having a lot of gut feelings about parts of my code that just feel wrong, inefficient, and repetitive. The problem is, even though I know where my code is weak, I don’t usually know how to go about making my code better.

And this is where making effective use of resources (read: The Art Of Effective Googling) comes quite in handy. Yesterday, however, I used even better resource – a more experienced developer! We took a look at my code and came up with some ways I could refactor it. I learned about a pretty interesting module that could save me lines of code and keep my application DRY. And now I get to share it with you! This module is called Forwardable, and trust me when I say that it’s going to make you want to delegate all the things.

Infatuation With Delegation

Before we even get to Ruby’s Forwardable module, let’s first make sure we understand delegation. So, whut exactly is delegation? It’s probably exactly what you imagine it to be. In plain English, when you delegate something to someone else, you divide up responsibilities amongst yourselves. For example, if I had someone to delegate all these blog posts to, I wouldn’t have to write all of them myself! But I digress; back to programming.

Delegation in programming is not too different. When an object has a lot of responsibilities and things to do, it’s generally easier to give some of those responsibilities to another object – a “helper” object – to avoid repetition and keep things working efficiently. Let’s put this in some technical context for a hot minute: we can use a technique called encapsulation to pack a bunch of functionality into a single object’s class and instance methods.


Ok, maybe you’re not a fan of technical jargon. Maybe you’d much rather prefer a real-life example of delegation? Alright, here you are:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Product
  def sku
    # Returns a sku specific to a
    # particular instance of a product.
  end
end

class Book < Product
  def author
    # Returns an Author object
    # associated with that book.
  end
end

Yup, that’s right. Class inheritance, a concept I’ve written about a couple times that you’re probably pretty familiar with at this point, is a type of delegation.

Since a Book object inherits from a Product object, it has both an author method an a sku method. When you ask a Book for its sku, it first looks in the Book class, and when it doesn’t find the method in there, it delegates up to its parent class, which is the Product class. Instead of making the Book responsible for all the functionality, we’re using the Product object to take care of doing the logic and finding and returning the correct sku.

See, you’ve already worked with delegation! Nothing to fear here. Now let’s apply delegation to the Forwardable module.

Put Your Best Foot Forward

The best way to see Ruby Forwardable in action is by using it to actually refactor something. So, let’s take a look at what our raw code looks like right now:

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
class Book
  def language
    "English"
  end

  def year
    "1926"
  end

  def author
    "Ernest Hemingway"
  end

  def title
    "The Sun Also Rises"
  end
end

class Product

  def initialize
    @book = Book.new
  end

  def language
    @book.language
  end

  def year
    @book.year
  end

  def author
    @book.author
  end

  def info
    @book.title
  end
end

Ugh. You’ve probably already recoiled in horror. We have a Book object, which has a language, year, author, and title. And we have a Product object, which creates a new instance of a Book object, and then pretty much repeats all those methods again, using the instance it creates in the initialize method.

We already know this code is bad. But how to go about refactoring it? Use Forwardable, obvs, and do some forwarding! We’ll delegate all the handling of information to the Book object. Our Product class doesn’t need to worry about that!

Cool. So how do we do this? Like so:

  • Let’s first get rid of all of those methods in the Product class. We’ll keep our initialize method, since that’s how we’ll create a new instance of Book in order to have something that we can call methods on. Now our class looks pretty empty:
1
2
3
4
5
class Product
  def initialize
    @book = Book.new
  end
end
  • We’ll add the Forwardable module, part of the Ruby standard library, by extending it in the first line of the class:
1
extend Forwardable
  • Now we’ll specify the methods that we to call on a Book object through our Product class by using the def_delegators method, available through Forwardable:
1
def_delegators :@book, :language, :year, :author
  • We also want to get the title as well, but we want to rename that method as info. We can use def_delegator in order to do that:
1
def_delegator :@book, :title, :info

Now we’ve cut down these two classes a lot. Our refactored code looks like this:

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
class Book
  def language
    "English"
  end

  def year
    "1926"
  end

  def author
    "Ernest Hemingway"
  end

  def title
    "The Sun Also Rises"
  end
end

class Product
  extend Forwardable

  def_delegators :@book, :language, :year, :author
    def_delegator :@book, :title, :info

  def initialize
    @book = Book.new
  end
end

What can this refactored code actually do? Well, it lets us call info on a Product and get back the title of the book! And it allows us to call year directly on an instance of Product:

1
2
3
4
5
6
p = Product.new
 => #<Product:0x007feb2183ea78 @book=#<Book:0x007feb2183e9d8>> 
p.year
 => "1926"
p.info
 => "The Sun Also Rises"

But wait – we didn’t write a year method in the Product class! Well, okay, we kind of did. We used def_delegators, to tell the Product class that it should respond to three methods: language, author, and year. And, we’re telling the Product class to respond to each of these methods by calling it on an instance of @book.

And how did we rewrite that title method, exactly? We used def_delegator (singular, not plural!) to tell the Product class to respond to a method called info by calling title on @book. The reason that this works is only because we already have a title method defined on all instances of the Book class.

Delegate Like You Mean It

Using the Forwardable module comes in handy not just for refactoring, but also for your initial structuring of an application. Anytime you have an object handling lots of functionality, think about whether you can encapsulate that functionality into another class, and delegate the methods that aren’t directly required into that “helper” class.

There are some great blog posts with examples of how to use the Forwardable module effectively. Here’s an implementation on a Reading List class (think Goodreads):

1
2
3
4
5
6
7
8
9
10
11
class ReadingList
    extend Forwardable

    def_delegators :@books, :map, :size, :<<, :shuffle
    def_delegator :@books, :shift, :remove_book
    def_delegator :@books, :push, :add_book

    def initialize
        @books = []
    end
end

There’s some pretty bomb stuff happening in such few lines. The ReadingList class gets initialized with an empty array, which we save as an instance variable, @books. Then we’ve also our def_delegators, which delegate map, size, <<, and shuffle to the @books. And we have two methods that we (kinda) wrote: remove_book and add_book.

That’s a lot of stuff for 9 lines of code! So what can this do, exactly? Well, let’s see our reading list in action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vaidehis_list = ReadingList.new
 => #<ReadingList:0x007feb21a6d0b0 @books=[]> 
vaidehis_list.add_book('For Whom The Bell Tolls')
 => ["For Whom The Bell Tolls"]
vaidehis_list.add_book('The Old Man And The Sea')
 => ["For Whom The Bell Tolls", "The Old Man And The Sea"]
 vaidehis_list.add_book('To Have And To Have Not')
 => ["For Whom The Bell Tolls", "The Old Man And The Sea", "To Have And To Have Not"]
vaidehis_list.size
 => 3
vaidehis_list.shuffle
 => ["For Whom The Bell Tolls", "To Have And To Have Not", "The Old Man And The Sea"]
vaidehis_list.remove_book
 => "For Whom The Bell Tolls"
vaidehis_list.size
 =>

Dayummmmm. Pretty sweet, right? We get to call all these methods directly on our ReadingList class! But what are they actually getting called on? Well, by using def_delegators and def_delegator, we’re telling our ReadingList class to call methods like shift and shuffle on our @books instance. And here’s where it gets pretty cool: our @books variable is…an ARRAY.

Just in case you’re not as enthused about this fact as I am, let me explain what this means. It means that we can have access to every single method available on an array instance. Just in case you’re wondering, all instance of Arrays have 113 methods available to them (not including the 54 methods available to all instance of Object)! All we’d have to do is add a method (literally, you can choose any method available on an array) like flat_map to our def_delegators line, and tada! It’s ours to use on our ReadingList object.

You can see how this can get pretty powerful, pretty fast. In just a few lines of code, we’re exercising the functionality of an entire plain old ruby object (PORO), simply by delegating methods through Forwardable.

Okay, that was a lot of refactoring magic. I told you, right? Lots of effort, but lots of payoff! Now, if you’ll excuse me, I apparently have some books to read.

tl;dr?

  • Delegation is the idea that an object can delegate a task to an associated “helper” object.
  • The Forwardable module uses def_delegators to delegate methods to another Ruby object, and def_delegator to rename a method that’s being delegated to another object.
  • For another example of this module, read this incredibly thorough blog post on implementing Forwardable.
  • Curious about delegation patterns in Object-Oriented Programming? This post has gotcha covered.