Ruby Internals

Extremely Defensive Coding

Extremely Defensive Coding

by Sam Phippen

In the RubyConf 2015 talk "Extremely Defensive Coding," speaker Sam Phippen discusses the principles of defensive programming specifically tailored for Ruby development, with a focus on techniques implemented in the RSpec framework.

  • Introduction to Defensive Programming: Phippen defines defensive programming as an essential concept that helps developers write resilient code, particularly in Ruby, a language prone to dynamic changes by users.
  • Importance of Experience: He highlights the significance of community interaction and feedback, noting his role in the RSpec core team, and expresses that newcomers to Ruby and Rails contribute to the vitality of the programming community.
  • Method Overriding Concerns: Phippen shares an anecdote about a challenging question posed by a junior developer regarding overriding methods on the Ruby Object class. He concludes that overriding methods should strive for consistency with Ruby's expected behaviors rather than introducing inconsistencies.
  • Criteria for Good Gems: He outlines several criteria that define a good gem, emphasizing the importance of a user-friendly interface that simplifies developers' tasks while being functional across various Ruby interpreters. The complexity of a gem's internal workings is deemed less critical compared to how well it integrates with applications.
  • RSpec Feature Evaluation: Utilizing RSpec stubbing as an example, Phippen discusses the feature that allows users to replace methods temporarily during tests while ensuring the original methods can be restored post-execution. This is illustrated with a practical example of the allow method in RSpec.
  • Handling Method Redefinitions in Ruby: He addresses challenges presented by users who redefine core methods, affecting how the RSpec framework handles method availability. He outlines the strategy of utilizing 'trust but verify,' leveraging Ruby's inherent method capabilities to manage these risks effectively.
  • Compatibility Across Interpreters: Phippen stresses the need for RSpec to be compatible with multiple Ruby interpreters, detailing the specific challenges faced with each interpreter's unique characteristics, especially concerning method retrieval on user-defined objects.
  • Conclusion: He concludes that defensive programming is vital in ensuring a gem can withstand unforeseen user behaviors while maintaining reliability across different Ruby platforms. Phippen reiterates the importance of good design in balancing simplicity with complexity, urging developers to consider these trade-offs when choosing libraries for their applications. He invites the audience to ask questions, further engaging them in the topic.
00:00:14.879 Welcome to the talk on Extremely Defensive Coding. My name is Sam Phippen. You can find me on Twitter at @samin and on GitHub. If you're interested in knowing more about me, take a look at my profiles. On my GitHub account, you'll see that I spend most of my time committing as a member of the RSpec core team. It's important for the RSpec community to be well represented in events like this one so that users of the framework can connect with people actively working on it. If you have questions or feedback about RSpec—whether you love it or hate it—please let me know, as personal feedback is crucial for influencing the future of the framework. We just released RSpec version 3.4 this week, featuring some powerful new enhancements worth checking out. Additionally, I work for a consultancy in the UK called Fun and Plausible Solutions, where we help clients optimize their Rails applications and perform database analysis. If that interests you, I'd love to find a way to work together.
00:00:58.760 Before diving into the talk, I'd like to get a sense of the audience's experience at the conference. Who is enjoying themselves so far? Is everyone having a great time? I have a relevant question: How many attendees are here for their first time at a Ruby or Rails conference? That's a significant number! Given that you're all having fun, I want to give a shout-out to the scholarship and mentoring program, which I've participated in this year for both RailsConf and RubyConf. This program effectively encourages newcomers, who make up the majority in this room, to join our community, be welcomed, and have a fantastic time. If you have enjoyed this conference and plan to attend RailsConf or RubyConf next year, I highly recommend volunteering to become a mentor. The scholarship program has been praised by many other communities for its excellence, so I encourage everyone to get involved.
00:02:50.400 With that introduction out of the way, let’s get into coding! I want to start this talk with a story from a previous question-and-answer session during another talk I gave about the internal architecture of how RSpec mocks work. Towards the end, I discussed that much of the code was there to help us manage complex user objects that override default methods in Ruby. As I began answering questions, I was asked by a junior developer a question that completely stumped me. Their inquiry made me realize I couldn’t provide a reasonable or complete answer. This encounter highlighted something I genuinely appreciate about working with less experienced developers: they compel me to explain things in detail, forcing me to clarify my own understanding. The question was about when it is acceptable to override methods on the Object class.
00:04:06.360 I struggled to answer, stating that sometimes it’s okay, and at other times, it’s not, but I failed to provide clear criteria for when to do so. This illustrates a common experience: when asked straightforward questions about topics we believe we fully understand, we often find it hard to articulate our knowledge concisely.Eventually, I concluded that overriding methods on Object should be driven by the goal of consistency—only do it when it makes your object more consistent with existing Ruby behaviors, not less. For instance, consider a custom collection object you've implemented, similar to a hash or an array. If you don’t override the equals method, it defaults to object identity comparison, which isn’t very Ruby-like. When there is a clearly defined collection of items, the Ruby standard library typically checks whether each item in the collection matches all other items. Therefore, overriding the equals method makes your custom object consistent with the expected Ruby behavior, which is an appropriate case for method overriding.
00:06:01.600 As we proceed through the talk, I invite you to keep this story in mind, and we'll revisit it at the end. Now, I want to pose a question: What makes a gem good? This is a topic of lively debate in our community, and while I can’t provide a universal answer, I can share my perspective on what makes a gem valuable. First, does it matter how complex the internals of a gem are? In my opinion, it does not. Most of us frequently use complicated libraries without ever delving into their source code. I suspect that many of you in this room aren't intimately familiar with the inner workings of popular libraries like Ruby on Rails, Sinatra, or RSpec.
00:06:40.360 So, it seems clear that the complexity of a gem's internals doesn't significantly influence our daily decisions about which gems to use. However, how about the interface? Personally, I believe this is crucial. A good gem has an interface that integrates seamlessly with our applications and offers convenience. A gem should be more convenient to use than implementing the equivalent features in our own applications. Many of us are capable Ruby developers and can solve arbitrary problems within a reasonable timeframe. So, if we choose to use a pre-packaged solution, it must provide tangible time savings. This convenience must endure as the application evolves, because as our architectural patterns shift and our team dynamics change, a gem that initially seemed advantageous might later lead to dissatisfaction.
00:08:25.880 Moreover, teams are often heterogeneous in skill level, comprising senior developers, juniors, and individuals who have transitioned from other programming languages into Ruby. Thus, a gem should offer a user-friendly interface that is powerful enough for senior developers while remaining accessible for junior team members. Some of you may operate applications across different Ruby interpreters, such as JRuby or Rubinius. Therefore, when selecting a gem, it is essential to ensure compatibility with various Ruby codebases and interpreters. I have outlined numerous criteria for evaluating gems, but it's only fair to compare these expectations against the gem I develop and maintain to see how it measures up.
00:10:08.360 I could claim that RSpec is always convenient, but that would be dishonest. I’m sure that everyone in this room who has used RSpec at some point has felt frustration with it. So, let’s examine a specific feature to evaluate how it upholds our expectations. To do this, I'll frame it with a user story. As an RSpec user—roughly 70% of you in this room, according to last year's Ruby survey—I want to stub a method on an object but still have the original method remain intact after the test, ensuring that my object isn't broken by the test suite. Let’s break this down.
00:10:58.200 For those of you unfamiliar with RSpec, stubbing an object typically involves using the allow method or, if using legacy RSpec, using the `stub` method. Stubbing replaces the original method with mock code, allowing you to avoid executing the specific instance's implementation. However, I believe that once the stubbing is complete and the test wraps up, the original method must be restored. This is crucial since we want our object to remain consistent with how it would behave outside the testing environment, preventing it from being left in an inconsistent state.
00:12:20.760 Now, how exactly does this work? As a refresher for those who aren’t doing this every day, the syntax in RSpec for stubbing a method typically looks like this: `allow(cat).to receive(:meow)`. Here, we're dealing with a hypothetical object called 'cat' that has a 'meow' method. The process involves removing the original 'meow' method from the cat and saving it before executing your test. Then, once the test execution is complete, the original method is reinstated onto the cat object. To explain how methods can be saved and restored in Ruby, we need to look closely at Ruby's method handling.
00:14:04.360 In the Ruby standard library documentation, there is a method called 'method,' which allows us to retrieve a method associated with a symbol. In essence, it’s a reference to the method that includes information about its closure, letting us handle the method object as we would any other Ruby object. In RSpec, this capability is invaluable. By saving the method object, we can replace it during test execution and restore it afterward. Unfortunately, things are not that simple because Ruby is capable of allowing users to redefine core methods, which places significant responsibility on us as gem developers.
00:15:18.760 As RSpec developers, we need to design abstractions over common tasks in Ruby. For the remainder of this talk, I will discuss a method in RSpec called 'aspect_support_method_handle4,' which serves as a wrapper for the method method designed to handle various user error cases. Merely invoking the method method on user objects is insufficient in many cases, so we need to examine our evolution of 'aspect_support_method_handle4' through our Git history. I will explain the significant changes we've made, focusing on how we address various challenges that arise with user-generated errors.
00:16:35.680 The first implementation of 'aspect_support_method_handle4' found in RSpec’s source history looked simple enough. The method accepts an object and a method name, invoking the method method on the object using that name. However, we soon encountered issues since users sometimes redefine core methods. Many of you are familiar with HTTP in Ruby; for instance, within almost any HTTP library, the method method might conflict with user-defined elements. This means that directly invoking the method method won’t yield the expected results because users may redefine core methods, leading to situations where we cannot retrieve the correct method object.
00:17:58.360 One solution to this involves defining a constant for 'Kernel.method', which references Ruby's standard implementation. We'll wrap the kernel method method in our functionality. This allows us to acquire the correct method object without conflict, maintaining a reliable interface for our users. We must remember: users will redefine core methods at any time, but this behavior can be managed effectively if we grab the kernel implementation and use it as needed.
00:19:02.320 Unfortunately, Ruby interpreters present additional challenges. As we diversify our support for different Ruby interpreters, we encounter edge cases where some objects fail to include Kernel in their inheritance chain. These situations prompt us to devise robust solutions to ambiguity caused by the variety of Ruby environments. At RSpec, we ensure compatibility across MRI, JRuby, and other interpreters. However, it is worth noting that RSpec does not yet support Rubinius due to ongoing compatibility issues. But in general, our aim is to foster a gem that performs efficiently across various Ruby platforms.
00:21:20.680 Let’s discuss the implementation of user delegations and their interaction with our method handling. In this context, we may encounter edge cases where users redefine the respond_to and method methods, which can complicate method delegation within user-defined objects. Ultimately, our implementation needs to work robustly even against user-defined behaviors. We achieved this through transparent and reliable checks within the framework, ensuring that we can catch unexpected cases while maintaining overall functionality.
00:23:10.160 To summarize, to manage user-defined method handling effectively, we utilize a strategy of 'trust but verify'. This approach involves trying to apply our kernel method method trick first; if it fails, we gracefully fall back on invoking the method on the user object to check if it returns a valid method handle. If everything works out, we can restore the original methods after the tests, ensuring the integrity of the object. This process demonstrates significant thought and consideration behind method handling in the RSpec framework.
00:25:02.240 In conclusion, this exemplifies why I appreciate RSpec. The framework strives to provide assurances about its behavior. Whatever modifications users apply to their objects, RSpec functions reasonably, maintaining the integrity of the tests. This level of reliability is crucial in legacy code scenarios, helping users mitigate issues caused by user-driven changes. If you find yourself struggling with RSpec and aren’t certain whether it’s an internal issue, feel free to submit a bug report. We aim to assist however we can, and if it isn’t a problem within RSpec, we will take steps to clarify and improve our documentation.
00:27:30.640 Ultimately, your gem should exhibit defensive characteristics due to the unpredictable nature of user behavior. Users can redefine core methods at any given moment, introducing variations and unexpected behaviors in their code. Users also run scripts across various Ruby interpreters, each exhibiting particular behaviors that may differ from MRI. In this respect, users are often eccentric, just like the Ruby community itself. Therefore, trust and verify what they do when building your gems. Revisiting the initial question on what makes a gem good, I contend that the extensive detail provided earlier should indicate that understanding a gem's internals may not be critical for its effective use.
00:28:55.560 The true value lies in the interface. A successful gem allows you to engage with its power just by executing simple commands—like calling the allow method on an object. When you initiate this command, the vast capabilities of the underlying framework are activated without requiring knowledge of the complexities involved. Most users won’t realize these intricacies until they are explained, and that’s perfectly fine. A gem should primarily focus on offering convenience and usability without bogging users down in the intricacies of its implementation.
00:30:53.760 To conclude, a gem’s purpose is to provide a solid abstraction between the desired functionality and its underlying complexity. Defensively built gems ensure that unexpected user behaviors, such as overriding fundamental methods, do not compromise their performance. During this talk, we focused on the incredible capabilities of Ruby and emphasized the need to adopt defensive programming styles to prepare for future challenges. The balance between simplicity and complexity is fundamental in software design; while I showcased an extensive framework with many intricacies, simpler libraries, like Minitest, demonstrate their worth as well. The key takeaway is to thoroughly consider the trade-offs before adopting different libraries. I appreciate your time; if you’re interested in my ongoing project, a SaaS called Browser Path—focused on load testing and front-end performance—please check that out.
00:33:10.600 Now, I’d like to open the floor for questions. The first question pertains to the handling of methods when Kernel is in the inheritance chain. In that scenario, can’t you just retrieve the method from Kernel directly? The answer is: it depends on the Ruby interpreter you've chosen to utilize. If Kernel is absent from the inheritance chain of an object, some interpreters restrict re-binding methods taken from Kernel onto those objects. This highlights the importance of our approach to method handling—it ensures reliability across various Ruby environments.