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 | |
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 | |
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
Productclass. We’ll keep ourinitializemethod, since that’s how we’ll create a new instance ofBookin 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:
1
| |
- Now we’ll specify the methods that we to call on a
Bookobject through ourProductclass by using thedef_delegatorsmethod, available through Forwardable:
1
| |
- We also want to get the title as well, but we want to rename that method as
info. We can usedef_delegatorin order to do that:
1
| |
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 Product:
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: 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 | |
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 | |
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_delegatorsto delegate methods to another Ruby object, anddef_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.