Summarized using AI

Extremely Defensive Coding

Sam Phippen • October 01, 2015 • Ghent, Belgium • Talk

Introduction

In the talk "Extremely Defensive Coding" presented at ArrrrCamp 2015 by Sam Phippen, the focus is on the importance of defensive programming in Ruby, particularly within the context of testing frameworks like RSpec. Defensive programming helps ensure code maintainability and robustness by anticipating and guarding against potential issues arising from external modifications and peculiarities of the Ruby environment.

Key Points

  • Understanding Defensive Programming:

    • Defensive programming involves anticipating edge cases and potential changes to the methods that might be redefined by users or libraries.
    • It emphasizes the need for consistency in overriding core Ruby methods to make objects behave as expected.
  • The Challenge of RSpec:

    • RSpec as a testing tool has complexities due to its necessity to adapt to various Ruby objects that might redefine core behavior, such as the method method.
    • A key question posed during the talk was about the appropriate scenarios to override methods on objects to maintain consistency.
  • Gem Convenience vs. Implementation Complexity:

    • Phippen argues that while a gem's functionality is important, its interface is crucial as it should allow developers to leverage the gem's capabilities without requiring them to understand its complex internal workings.
    • The effectiveness of a gem can depend on how well it integrates with different Ruby versions and environments.
  • User Examples in RSpec:

    • The talk illustrates the user's perspective through a user story related to stubbing objects in RSpec. Users expect that operations like stubbing should not alter the original methods on their objects after tests run, emphasizing the need for RSpec to handle method restoration seamlessly.
  • Defensive Coding Techniques:

    • Phippen elaborates on the technique of saving method objects and dealing with dynamic method redefinition by employing Ruby's metaprogramming capabilities.
    • The robustness of the RSpec framework relies on its ability to adopt variations in how Ruby interpreters handle method definitions and object behaviors, ensuring it functions correctly across different Ruby environments.

Conclusion

The talk concludes with a reiteration of the necessity of dealing with the unpredictability of user-defined changes in Ruby. The underlying message is that good gems are those that hide their complexity behind a simple interface, allowing users to work effectively without interference from the inner workings. Ultimately, Phippen emphasizes the importance of writing code that defends against potential issues to uphold code quality and reliability in the face of future changes in Ruby's dynamic environment.

Key Takeaways

  • Embrace defensive programming to anticipate and mitigate issues arising from method redefinitions.
  • Aim to create convenient interfaces for gems that mask internal complexities.
  • RSpec exemplifies adaptive behavior to accommodate the diverse Ruby landscape, making it a valuable tool for developers.

Extremely Defensive Coding
Sam Phippen • October 01, 2015 • Ghent, Belgium • Talk

Defensive programming is one of those abstract ideas that seems great. We all want to use these ideas to ensure the long-term maintainability of our codebases. It is, however, often unclear what we should be defending against and what form those defenses should take. We can find places where defensive patterns could be added by looking at the edge cases that occur in our system. Where it seems appropriate, we can then apply ideas and patterns from defensive programming. In this talk we'll look at some of the extremely defensive patterns that have been driven out in RSpec throughout the years. We'll look at building Ruby that works accross a wide range of interpreters and versions (including 1.8.7!). We'll investigate how you can write code that defends against objects that redefine methods like send, method and is_a?. We'll also see how the behaviour of prepend can occasionally confuse even the most mature source bases. You should come to this talk if you want to learn about inheritence and method resolution in Ruby, defensive programming techniques and cross interpreter Ruby patterns.

Help us caption & translate this video!

http://amara.org/v/H4Ob/

ArrrrCamp 2015

00:00:07.970 Alright, this is Extremely Defensive Coding. Let's get started. Phil introduced me, but to introduce myself, my name is Sam Phippen.
00:00:10.110 You can find me on Twitter as Sam Phippen and on GitHub as sumfin. If you're interested in finding out more about me, you can check out my profiles there.
00:00:19.250 If you check out my GitHub profile, you'll find that I spend most of my time committing as a member of the RSpec core team. That's one of my driving motivations for coming to conferences like this. I think it's really important that RSpec is well represented in the community at large, and I love hearing questions and taking feedback.
00:00:36.450 So if you have anything you'd like to say about RSpec to someone who spends a lot of time working on it, you should take this opportunity to do so. If you see me walking around the conference, feel free to grab me, and let's have a chat. I love talking about this stuff. I work for a company called Fun and Plausible Solutions. We're a consultancy based in the UK, focusing on testing and refactoring Rails applications, doing object-oriented design and testing, and even some machine learning.
00:01:02.940 If that sounds interesting to you, please come and have a chat with me. I'm a little bit ill, so you might be able to hear it in my voice, and this talk might sound a bit spaced out because I've had a lot of medicine. But I'm sure we’ll be fine.
00:01:27.420 With all that sort of front matter sorted, let's actually talk about the topic. I like to start this talk with a story. The story actually comes from a Q&A session from one of my other talks about the internal mocking architecture of RSpec. As the questions came, and I answered the deeper technical details that the senior developers in the room were interested in, I was left with a few hands raised.
00:01:42.090 I received a question that absolutely floored me. It wasn't easy for me to answer, and this question came from one of the junior developers in the room. What I found interesting was how relatively easy it was for me to respond to the questions from the more experienced developers, but a more genuine question from someone newer in our community genuinely stopped me. This is one of the reasons I absolutely love working with junior developers: they force you to make explicit things that you have in your mind that you may not be able to fully articulate until they ask a question.
00:02:40.480 The question was, 'When is it okay to override methods that Ruby provides on Object?' The basic thesis of my talk is that RSpec is complicated because we have to do things to defend against changes users make to Object. I said that this is why it can be hard and that you really shouldn't do this most of the time—except, of course, 'most' is like a wiggle word and doesn't give you a useful definition of when to do it and when not to.
00:03:03.099 When I was asked this question, I couldn't immediately provide an answer. I sort of hesitated and walked around, giving half-explanations because I wasn't sure I was doing the explanation justice. So, I stopped, took a breath, and really thought about it. I tried to come up with an answer that unified all of the examples and half-explanations I'd given. This is a really useful exercise. Again, it's one of the reasons I enjoy working with junior developers: they force me to explain what I'm talking about.
00:03:59.410 The answer that I came up with had to do with consistency. The idea is that you should override methods on objects precisely when they make the object more consistent with Ruby, not less. To give an example of what I mean: there are things in the Ruby standard library that override methods provided by Kernel to make them behave more as you'd expect.
00:04:14.380 A good example of this is `==` on a data object like a string or a collection. Because in that case, comparing object identities—which is the default behavior on an Object—doesn't really make sense, comparing the values inside the collection is going to be a lot more beneficial for everyone involved. The basic idea here is that by overriding `==`, you've altered some behavior of the object, but you've done so in a way that makes the object behave more consistently with all the other objects in a Ruby system.
00:05:01.850 I'd like you to hold on to this story as I go through the rest of the talk, and we'll come back to visit it at the end. Now, I want to ask the question: what makes a gem good? Your mind might immediately spring toward the internals of a gem—the core pieces of functionality that it provides, the things that actually make it do what it's supposed to do.
00:05:18.190 But the more I think about this, the more I find that the internals of a gem don't really matter. It's the interface that the gem provides that really makes me care about what the gem is going to do. The reason for this is that I like to think I'm a pretty capable Ruby developer, and if that's the case, I can basically implement anything a gem is going to provide me, given enough time and resources. So, if that's true, a gem has to be more convenient than if I simply implemented the same thing myself.
00:06:19.450 If I do the implementation myself, the code is going to fit in nicely with the existing architecture of my application. It's going to work how I want it to, and I'm going to understand what the API is. In order for a gem to be more convenient than if I did it myself, it needs to meet several criteria. One of them is that it has to remain convenient as my application continues to grow and gets built upon.
00:07:03.180 If a gem fits in immediately when I use it to get something done, that's great. But if, in six months, I find I can't easily modify my application because I've glued all of this code onto a gem, that's going to be a significant problem. It's reasonable to assume that most of us work on teams made up of developers with diverse skill ranges. If that's the case, a gem can only be used by developers who fully understand what's going on; it won't be nearly as convenient as if a gem can be used by everyone on the team.
00:08:02.469 Gems also need to work with a wide range of Ruby code bases. Not everyone in Ruby is just building Rails applications, and not everyone in Ruby is just running on MRI. A gem therefore needs to fit with all of these different scenarios that developers might encounter. That's a sizable shopping list, and it's only fair that I apply that to something that I work on and use every day. Now let's talk about RSpec.
00:08:32.880 I could say that RSpec is always convenient, but that would be an absolute lie. I'm sure everyone in this room has at some point encountered something in RSpec that has made them feel frustrated, angry or ready to smash their computer and log off for the day. I know this has happened to me, and if that's the case, we need to examine some pieces of RSpec in detail to see how they fit into this story.
00:08:52.840 To illustrate this, I'd like to explore a user story from inside RSpec. As an RSpec user, when I stub an object, I want the original method on that object to remain intact after the example so that my objects aren't broken by my test suite. This might be a bit fast, and I'm sure not everyone here is an expert in the language of mocking and stubbing inside RSpec.
00:09:20.260 So, let’s go through that again a bit more slowly. When I stub an object, I'm replacing a method on that object with a simple RSpec stub implementation to make it easier to reason about what that method is doing in my test. I want the original method to be on that object after the example so that when the test finishes executing, RSpec puts the method back on the object, completing it as it was before the test began.
00:10:24.390 This is important because nowhere in your production system are you going to have RSpec stubs floating around objects. So, it doesn't make sense to have RSpec stubs present on your objects during the execution of subsequent tests. What we're dealing with here is RSpec effectively moving methods around the system to provide convenience when you do stubbing or mocking.
00:10:43.810 You might reasonably ask, how does that work? Just to clarify, we're talking about the syntax from RSpec: the `allow` method that enables some object to receive some method. For instance, `allow(cat).to receive(:meow)`. What this does inside RSpec is save the `meow` method of the `cat` object somewhere else in the RSpec system. Your test will then execute, and once it's complete, the method is restored.
00:11:22.840 When the test execution is complete, the `meow` method will have its original implementation given back to the `cat` object. However, this raises the question: how do you save a method in Ruby? I suspect not everyone here spends their time moving methods around the system as if they were data, which is a relatively odd Ruby use case.
00:11:53.050 If you check the standard library documentation for Object, you'll find that there's a method called `method` which takes a symbol and returns a method object. The literal docstring reads, 'Looks up the named method as a receiver in self, returning the method object or raising a NameError.' The method object acts as a closure for an object's instance, meaning instance variables and the value of self remain available.
00:12:43.440 Now, that's not totally clear, but we're speaking here of something called a method object. In Ruby, every object that inherits from Kernel has a method called `method`, and when you invoke it with a symbol, it returns a method object. Method objects represent a single method on a single object somewhere in the Ruby system, and you can move them around as though they were any other object. This allows you to treat methods as data.
00:13:47.720 Method objects have a single public method called `call`. When you execute the `call` method, it's like invoking that method directly on the object. This capability allows us to effectively implement our feature in RSpec since we can take a method object, place it somewhere else, and restore it at the end of a test. However, I could tell you that that's everything you need to implement method saving in RSpec, but unfortunately, that's not entirely true. To convey the truth of the matter, we need to talk about something I'm going to label defensive coding.
00:14:50.220 We're going to specifically discuss a method from within RSpec called `rspec/support/method_handle`. This is essentially our equivalent of the `method` method, except that it actually works. In many cases, invoking the `method` method isn't sufficient. To explain why this is the case, let's quickly go through the history of how this method has evolved.
00:15:23.830 Some scenes have been altered and sequences adjusted, but this will provide an approximate view of its true representation given that I have slides and not the ability to travel in time. The first implementation of `rspec/support/method_handle`, which you'll find anywhere inside the RSpec history, takes an object and a method name and directly invokes the method method on that object.
00:15:43.460 This function calls the `method` method and, as I mentioned, is unfortunately not good enough. One reason it's insufficient is that users can redefine core Ruby methods. How many of you have worked with HTTP in Ruby? I know that's a bit of a stretch, but consider the issue. If you look inside any HTTP library in Ruby, you'll find there’s usually a definition for a method that returns something like `get`, `post`, or `put`, as a string or a symbol. This is fine; the term 'method' is significant within HTTP, and users should have the ability to define it.
00:16:36.020 Unfortunately, users can and will redefine methods at any time, for any reason. When this happens, the `method` method no longer reliably returns method objects as expected. Therefore, to solve this issue, RSpec has evolved to create a constant known as `Kernel.method_method`, into which we assign the result of the expression `Kernel.instance_method(:method)`. This allows us to obtain the implementation of a method without concern of redefinition.
00:17:30.960 The `instance_method` method essentially allows you to acquire the implementation of any method from any class without needing to focus on a particular target instance. Thus, what we've done is save the kernel implementation of the `method` method, and with that saved reference, we can call our `method_handle` method, binding the kernel implementation of `method` to the object using the method name to retrieve the results.
00:18:16.580 The key takeaway here is that users can and will redefine crucial methods in Ruby. This is something that the language explicitly allows. Therefore, we rely on the kernel's implementation of method retrieval because generally, no one tampers with Kernel.
00:18:36.560 However, Ruby interpreters can also present challenges. Some objects don’t inherit from Kernel within the inheritance hierarchy. While Ruby's object system is straightforward, there are objects in the standard library that inherit from BasicObject and include a duplicate of Kernel. That gives them access to Kernel's methods without explicitly incorporating Kernel into their inheritance chain. If we invoke our current implementation of `method_handle` and switch to JRuby, we might encounter an exception: 'bind argument must be an instance of Kernel.'
00:19:16.160 The process of rebinding module methods should work in all Ruby interpreters from version 2 onward, but in some cases, it simply doesn't function as intended. RSpec has to support essentially every Ruby interpreter available—this includes supporting Ruby 1.8.7 and up, along with all versions of JRuby and even Rubinius. The challenge is that the Ruby interpreter's behavior can differ significantly, complicating gem compatibility.
00:20:01.280 To ensure compatibility, we set up conditional method definitions based on Ruby interpreter characteristics during boot time. This is a performance optimization, as we could place the conditions inside the method, but that would be unnecessary. This feature tells us whether we can rebinding module methods, specifically if we're running on MRI Ruby version 2 or higher. If this process fails, we then capture the instance method object and attempt to bind it to objects that don't include that module.
00:20:35.310 This method also checks if the Kernel is present in the inheritance chain of the object. If so, we apply the rebinding method. If not, we resort to calling the `method` method regardless of the Ruby interpreter's behavior, knowing that it might yield different results based on the context and its particular quirks.
00:21:21.220 To ensure that RSpec continues offering support across various environments, we have developed a robust framework to tackle these issues. Importantly, we also support Windows environments through a cloud-based CI that resembles Travis—while it may be somewhat janky, it operates effectively.
00:21:49.060 Our goal is to embrace every Ruby interpreter and ensure that our test framework remains conducive to users. It requires us to develop intricate solutions for a myriad of situations, even when it may seem excessive. Our approach may include implementing a system that feels forced, as we navigate the complexities of various environments. RSpec comprises all the approaches we've explored, ensuring compatibility, but the catch is understanding how to support various methods and code bases.
00:22:18.890 One of the reasons I love RSpec is that we actively work to ensure that users with atypical redefining of the `method` method are supported. Users defining the method method to return method handles and working with objects that have broken inheritance chains are all accommodated. Our commitment to supporting Ruby code with our testing framework drives our dedication. If you can articulate your needs using the RSpec DSL, we aim to make it work for you. This is a very high bar for us to meet, but it's one that we aspire to achieve.
00:23:14.970 So please file bugs if you think RSpec isn't working for you. It may be our fault or may indicate some obscure bug deep within our engines, or potentially point to a documentation issue that we need to address. When writing a gem, you must be defensive: users can lie, redefining methods can emerge from various sources, and the `method` method may shift unexpectedly, causing challenges.
00:23:57.080 People may stub methods or respond to others in ways you might not anticipate. Your code must accommodate not only today's development conditions but also run seamlessly across all Ruby interpreters, especially within the various environments your application might operate in. We're supporting users who take unexpected approaches with their software.
00:24:31.480 Throughout this talk, I posed the question of what makes a gem good. What about the internals? While I argued that RSpec's inner workings might seem complicated, what's truly significant is that the user experience and the interface matter far more. You type `allow(cat).to receive(:meow)` and automatically get all that complexity behind the scenes activating with near perfection. And yet, that's not the most intricate part of RSpec.
00:25:04.720 The truth is, you get to leverage the workings of RSpec without needing to be aware of the intricacies below the surface, hidden behind a well-defined interface. In conclusion, the interface of a gem is what truly matters in ensuring it serves its purpose effectively.
00:25:45.510 The analogy stands: if an object is a barrier through which you can obscure code to facilitate abstraction, then a gem is a super-object—a much larger barrier where you can hide extensive blocks of code to form significant functionalities. Mistakes with abstraction can result in significant drawbacks, but when executed correctly, you can mask considerable complexities behind well-defined interfaces.
00:26:19.960 Defending against the unpredictable nature of software, particularly the changes your users could introduce, will enhance the convenience of the gem. Ultimately, your aim should be to prevent users from feeling compelled to look beyond the surface of your gem, preventing them from exploring the internals of `method_handle` implementations.
00:27:23.130 Do you remember the story I shared at the beginning of this talk? The question posed by a beginner is rooted in Ruby's power. Given that users can and will go ahead to redefine core methods for various reasons, we need to carefully consider the implications of that in the software we write.
00:27:39.620 We need to adapt our code today to defend against the mistakes we're likely to make tomorrow. That’s all I've got. Thank you very much, and let's have some questions.
Explore all talks recorded at ArrrrCamp 2015
+14