RubyConf 2015

"Refinements" como alternativa al "monkey patching" en aplicaciones Ruby

James Adam  · 

Presentación

Vídeo

Transcripción

Extracto de la transcripción automática del vídeo realizada por YouTube.

(country music) - Ah, that’ weird. (laughs) I just had a run through and I hit 45 minutes exactly. So, I'm just going to get started, so that you can all have a nice coffee break after this. So, chances are that you've heard of refinements, but never used them.

The refinements feature has existed as part of Ruby, for around five years, first as a patch, and then, subsequently, as an official part of Ruby, since Ruby 2. 0, and yet, to most of us, it exists only in the background, surrounded by a haze of opinions about how they work, how to use them, and indeed whether or not using them at all is a good idea.

I’d like to spend a little time looking at what refinements are, how to use them, and what they can do. But don't get me wrong, this is not a sales pitch for refinements. I'm not going to try and convince you that you should be using refinements and that they’re going to solve all your problems.

The title of this presentation is Why is Nobody Using Refinements?, and that's a genuine question. I don't have all the answers. My only goal is that by the end of this session, both you and I will have a better understanding of what they actually are, what they can actually do, and, why they might be useful, when they might be useful, and why they’ve lingered in the background for so long.

So, let's go. Simply put, refinements are a mechanism to change the behavior of an object in a limited and controlled way. And, by change, I mean add new methods or redefine existing methods. And, by limited and controlled, I mean that by adding or changing those methods it does not have an impact on other parts of our software which might interact with the same object.

So let's look at a very simple example. Refinements are defined inside a module, using the refine method. This method accepts a class, a string in this case, and a block which contains all the methods to add to that class when the refinement is used. You can refine as many classes as you want within a module, and you can define as many methods as you want, within each block.

To use a refinement, we call the using method, with the name of the enclosing module. And, when we do that, all instances of that class, which is string in this case, within the same scope as our using call, will have the refine methods available. Another way of saying this is that, the refinement has been activated within that scope, however, any strings outside the scope are left unaffected.

Refinements can also change methods that already exist. When the refinement is active, it is used instead of the existing method, although the original is still available via the super keyword, which can be very useful. And, anyway the refinement isn’t active.

The original method gets called, exactly as before. And that’s really all there is to refinements. Two new methods, refine and using, however there are some quirks, and if we want to properly understand refinements we need to explore them a little bit. And the best way of approaching this is by considering a few more examples.

So, now we know that we can call the refine method within a module to create refinements, and that is actually all relatively straightforward, but it turns out that when and where you call the using method can have a profound effect on how the refinement behaves with our code.

We’ve seen that invoking using inside a class definition works. We activate the refinement, and we can call refine methods on string instances in this case. But, we can also move the call to using to somewhere outside the class, and still use the refine method as before.

In the example so far, we’ve been calling the refine method directly, but we can also use them within methods defined in the class. And, again this also works, even if the call to using is outside of the class. But, this doesn’t work. We cannot call our shout method on the string returned by our method even though that string object was created within a class where the refinement was activated.

And, here’s another broken example. We’ve activated the refinement inside our class but when we re-open the class, and try to use the refinement, we get NoMethodError. If we nest a class within another where refinement is active, it seems to work, but it doesn't work in subclasses unless they're also nested classes.

And, even though nested classes seem to work, if you try and define them, using the double colon, or the compact form, the refinements will have disappeared again. And even blocks seem to act a little bit strangely. Our class uses the refinements, but when we pass a block to the method in that class, suddenly it breaks.

It’s as if the refinement has disappeared. So, what's going on here? For many of us, especially those relatively new to Ruby, this is going to be quite counterintuitive. After all, we’re used to being able to reopen classes or share behavior between super and subclasses, but it seems like that only works intermittently with refinements.

It turns out that the key to understanding how and when refinements are available relies on another aspect of how Ruby works, which you may have already heard of, and possibly even encountered directly. The key to understanding refinements is understanding about lexical scope.

And, to understand about lexical scope we need to learn about some of the things that happens when Ruby parses our program. So let's look at that first example again. As Ruby parses the program, it is constantly tracking a handful of things to understand what the meaning of the program is.

And exploring all of these in detail would take far more time that I actually have but for the moment, the one that we’re interested in, is called the current lexical scope. So, let’s pretend to be Ruby, as we walk through the code, and see what happens. When Ruby starts parsing the file, it creates a new structure in memory, a new lexical scope, which holds various bits of information that Ruby uses to track what’s happening at that point.

When we start processing, we create this initial one, and we call that the top-level lexical scope. And when we encounter a class definition or a module definition as well as creating the class and everything that that involves, Ruby also creates a new lexical scope nested inside the current one.

And we can call this lexical scope A, just to give it an easy label. It doesn’t actually have a name. But, visually, it makes sense to show them as nested, but behind the scenes, the relationship is modeled by each scope linking to it’s parent. So, A’s parent is the top-level scope, and the top-level scope has no parent.

As Ruby process all the code within this class definition, the current scope is now lexical scope A. When we call using, Ruby stores a reference to the refinement within the current lexical scope. We can also say that within lexical scope A, the refinement has been activated.

You can now see there are no activated refinements in the top-level scope, but our shouting refinement is activated for lexical scope A. So, next we can see there’s a call to the method shout on a string instance. Jamie Gavern, who sat there, is going to talk a lot more about what method dispatch does, but one of the things that happens at this point is that Ruby checks to see if there any activated refinements in the current lexical scope that might affect this method.

In this case, there is an activated refinement for the shout method on strings, which is exactly what we're calling. So, Ruby then looks up the correct method body within the refinement, rather than the class, and invokes that instead of any existing method.

And there, we can see that our refinement is working as we hope. So, what about when we try to call the method later? Well, once we leave the class definition the current lexical scope becomes the top-level scope again and then we find our second string instance, with a method being called on it.

And, once again, when Ruby dispatches for the shout method, it checks the current lexical scope for the presence of any refinements and in this case there are none. So, Ruby behaves as normal which is to invoke method missing, which raises an exception and that's why we get our NoMethodError.

Now, if we had called using shouting outside the class at the top of the file, or something like that, we can see our refine method works both inside and outside the class perfectly. And this is because we're activating the refinement for the top-level lexical scope.

[ ... ]

Nota: se han omitido las otras 4.221 palabras de la transcripción completa para cumplir con las normas de «uso razonable» de YouTube.