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
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
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
Ugh. You’ve probably already recoiled in horror. We have a
Book object, which has a
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
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
Productclass. We’ll keep our
initializemethod, since that’s how we’ll create a new instance of
Bookin order to have something that we can call methods on. Now our class looks pretty empty:
1 2 3 4 5
- We’ll add the Forwardable module, part of the Ruby standard library, by extending it in the first line of the class:
- Now we’ll specify the methods that we to call on a
Bookobject through our
Productclass by using the
def_delegatorsmethod, available through Forwardable:
- We also want to get the title as well, but we want to rename that method as
info. We can use
def_delegatorin order to do that:
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
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
1 2 3 4 5 6
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:
year. And, we’re telling the
Product class to respond to each of these methods by calling it on an instance of
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
@book. The reason that this works is only because we already have a
title method defined on all instances of the
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
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
shuffle to the
@books. And we have two methods that we (kinda) wrote:
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
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_delegator, we’re telling our
ReadingList class to call methods like
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
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.
- Delegation is the idea that an object can delegate a task to an associated “helper” object.
- The Forwardable module uses
def_delegatorsto delegate methods to another Ruby object, and
def_delegatorto 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.