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
Product
class. We’ll keep ourinitialize
method, since that’s how we’ll create a new instance ofBook
in 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
Book
object through ourProduct
class by using thedef_delegators
method, 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_delegator
in 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 Array
s 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, anddef_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.