Testing

Inheritance, Composition, Ruby and You

Inheritance, Composition, Ruby and You

by Cody Stringham

In this talk titled "Inheritance, Composition, Ruby and You" presented at RubyConf 2018, Cody Stringham explores the concepts of inheritance and composition in the Ruby programming language, providing a comprehensive understanding of their implications in object-oriented programming (OOP). The session is primarily aimed at beginners, those new to Ruby, or anyone curious about the ongoing debate between the two concepts.

Key Points Discussed:
- Definition and Importance: Stringham begins by emphasizing the need to understand both inheritance and composition as crucial tools in programming. He highlights that programming does not have absolute solutions, and developers must grasp their problem spaces to choose the right tools.
- Method Lookup and Inheritance: The speaker illustrates the method lookup mechanism in Ruby, explaining how methods like 'puts' are inherited and how naming collisions can occur. He provides examples to show how inheritance allows child classes to acquire methods and attributes from parent classes.
- Challenges of Inheritance: Stringham discusses the complexities that arise from inheritance, particularly concerning unit testing and maintaining code readability. He notes that inherited classes risk coupling that complicates testing and introduces hidden dependencies.
- Composition as an Alternative: The presenter advocates for composition as a more flexible and maintainable approach than inheritance. He showcases how composition allows for clearer dependency management and easier testing by avoiding the pitfalls associated with deeply nested inheritance structures.
- When to Use Inheritance: While acknowledging that inheritance can be beneficial in specific contexts, such as API structures, Stringham warns against over-relying on it for code simplicity. He suggests that while inheritance simplifies some tasks, it also brings potential complications and should be customized carefully.
- Single Table Inheritance (STI): The talk touches on STI, noting its readability benefits but also its challenges, similar to those encountered with class inheritance.

In conclusion, while Stringham acknowledges the significance of inheritance in Ruby, he ultimately argues for a preference towards composition as a more robust method for structuring code that minimizes complexity and maximizes maintainability. The discussion is grounded in practical examples, making it a valuable resource for Ruby beginners and experienced developers alike.

To wrap up, Stringham invites attendees to connect with him for further discussions and shares a personal touch with a picture of his dog, Ruby, relating to his passion for the Ruby programming language.

00:00:15.470 Alright, welcome to my talk. If you haven't guessed by now, it is on inheritance and composition in Ruby.
00:00:19.949 A little background on this talk: why did I write this? When I started looking into these concepts, I found a lot of information that was geared toward more experienced developers. Maybe it was developers coming from another language who already knew these concepts and just wanted to know how to implement them in Ruby. However, I didn't find much material that combined these two ideas in context, where you could compare and contrast them. So I wrote this talk for people like me in that scenario—perhaps you're new to programming, new to Ruby, new to object-oriented programming, or just curious about the topic and why people say things like 'prefer composition over inheritance.' So let's get started.
00:00:48.840 Inheritance is a somewhat sensitive subject for many programmers, especially seasoned ones. It's interesting to note the feedback you'll receive on it. I think this is largely due to many people having bad experiences with inheritance—implementations that were poorly handled or didn't really fit the problems they were used to solve. I love to tease my Elixir friends by discussing inheritance, which sheds light on just how heated the topic can be.
00:01:07.380 So, when you talk about inheritance, you tend to get a lot of angry responses. People get upset and frustrated by it. The goal of this talk is to provide a firm understanding of what object inheritance and object composition are, so you can understand the trade-offs of both. I'm a firm believer that there isn't an absolute solution to the problems we encounter in programming. We need to grasp the problem space we're working in and understand the tools we have available to solve it effectively. That's what this talk aims to achieve—giving you this understanding of these two tools.
00:01:27.600 Before we delve into inheritance, we need to talk about Ruby. I'll jump into some actual demo code now. Can anybody read that? Should I make it bigger? Okay, let's proceed with this simple example: puts 'Hello, world!'. This is probably one of the first things you saw or wrote in Ruby. But has anyone ever wondered how this actually works? Where does puts come from, and how can I use it at a top-level instantiation like this? I hadn't thought about it until I prepared for this talk.
00:01:36.990 So, we have a few different examples here. When I run this file, it outputs 'Hello, world!' as expected. It works at the top level and also if we define it inside a class method. In this example, we're defining a new class called Hello. We have a method using the self keyword to define a class method called greet. When we run this, we see the expected output: 'Hello from a class method.' As we start to learn about the internals of Ruby, we can encounter interesting behavior with naming collisions.
00:02:20.520 When we define a class that also defines a puts method and we call puts, it won't use the normal puts method. Instead, it will use our custom puts method. So, if we output what is the password in our puts method, we get that instead of 'Hello from an instance method.' This illustrates the basics of inheritance and method lookup, and while I won't dive deeper into method lookup today, I want to give you an understanding of how this works.
00:02:47.250 When you call a method like puts in a Ruby file within a class, it first checks if that class has its own implementation of the method. If so, it uses that. If not, it will continue searching in the ancestor tree until it finds the method or reaches the root class. In our next example, we have a class Hello, which inherits from a speaker class. The speak method here uses puts, and we notice from the output that we get 'Hello from the great unknown.' This shows how inherited methods work.
00:03:18.000 Now, why can't we just call puts here within the inherited class? If we try that, we'll encounter a stack level too deep error. What this means is that calling puts inside the puts method creates an infinite loop, as it calls itself recursively.
00:03:50.800 In another example, we have an object with an initial attribute that initializes with a string value. This object also has a method called puts. As we discovered, Ruby responds to messages not only with methods but also with attributes. Clashing methods can lead to strange behavior, as seen here with our two puts methods. When we invoke puts, it'll depend on which one was defined last.
00:04:21.760 Currently, I have a class with a local variable named puts. When we call this, it will output the local variable rather than invoking the method itself unless we explicitly call the method. This illustrates the importance of understanding the hierarchy of method lookup in Ruby. The order of priority is local variables, class methods, parent class methods, and so forth. This means that we need to keep track of what variables we are using to avoid calling the wrong function.
00:05:22.520 Now, if you observe this last structure, we are getting an undefined method for puts because our setup file has removed that method from the module kernel. Understanding that every object you create in Ruby follows an inheritance tree, with all methods defined stretching upwards to the kernel, is crucial.
00:05:40.350 Now that you have a basic understanding of method lookup from Ruby, let's dive into inheritance. Inheritance allows a child object to acquire all properties and behaviors from its parent object. You may have heard it referred to as an 'is-a' relationship where, for instance, a duck is a bird. The duck class can inherit attributes and methods from the bird class, such as wings and feet, and then can introduce features specific to ducks.
00:06:11.740 In Ruby, we have different types of inheritance and tools available. This typical implementation can be seen with the less than symbol in different frameworks, especially Rails. The line 'child.ancestors' gives you a look at the ancestor tree. In any Ruby class, you end up with an ancestry that eventually leads back to kernel and basic object, granting inherited features and functionality.
00:06:51.100 Multiple inheritance is also a possibility in Ruby, allowing you to include various modules in your classes. However, we will only explore the include option today, as extend and prepend are outside the scope of this discussion. By including multiple modules, we see how Ruby structures the ancestor tree where the last module included will take precedence.
00:07:19.800 However, be cautious: while multiple inheritance expands flexibility, it also increases complexity. There are more moving parts, and thus, more chances to overlook or mishandle behavior. Next, I want to address the aspect of namespacing as some may confuse including a module with inheritance. Remember that namespacing is not about inheriting but organizing a class into a distinct space that can be referenced without ambiguity.
00:08:00.700 When should you prefer to use inheritance? It makes sense when you want to specialize an object that needs to access every attribute and method from the parent. However, it should never be a go-to for cleaning or 'drying' code up. There are better ways to achieve more maintainable code structures. Inheritance can create reusable and extendable code, but only if it's done correctly. If handled improperly, it can make reuse and extension incredibly difficult.
00:08:39.740 In contrast, when you inherit from a parent class, you create a coupling between your object and that class—often a potential problem. You might end up with reduced readability; if a child class calls a method from a parent or grandparent class, developers unfamiliar with the hierarchy will have to dig to locate that method. It’s easy to render yourself covered in a complicated mess of code because of naming difficulties. This innate difficulty in structural language is largely tied to how we define and name the elements within our systems.
00:09:05.900 Another downside is that it complicates unit testing. If you're inheriting classes, isolating behaviors of child classes can be quite challenging, as these inheritances include all behaviors from parent classes.
00:09:37.050 To illustrate this, let’s explore a practical example about how inheritance can make tests more complex. Here we have a user class with an after-create method populated with a basic attribute. However, we decide to create an admin class that inherits from the user class, only modifying minor functionalities. The challenge arises when we test our admin class, as we need to thoroughly understand that we're not only testing the specifics of the admin class but also need to account for inherited functionalities, which can lead to overlooked dependencies.
00:10:04.220 Should changes be made to the user class, the admin class could be affected, without any direct indication due to shared dependencies. This adds a risk of failing tests in the admin class silently if changes in the parent class break inherited functionalities—a situation that future developers on your project may not easily track.
00:10:33.830 On the other hand, when we explore composition, we see a different structure emerges, allowing us to isolate and test our logic without overlap and risk. Using a sign-up class as an example, we can pass in necessary dependencies without tightly coupling our user class. This manner of dependency injection allows us to mock or stub our responses when it comes to testing.
00:10:58.220 With composition, you can construct each piece of logic to independently communicate with its neighbors without inheritance concerns. Everything is much more explicit, making the testing process clearer and devoid of context issues—which is a significant advantage.
00:11:29.530 As a closing note on this topic, I had set out to somewhat defend inheritance while preparing for this talk. I acknowledge its existence within the Ruby language for good reason, but I also recognize its limitations. While I did find a few instances where it could be useful, particularly in API setups sharing common elements across calls, such as authorization tokens, I would argue that overall composition presents a more flexible and maintainable architecture.
00:12:00.900 To elaborate, when dealing with standardized interactions with APIs, employing inheritance can streamline common tasks while allowing specific functionalities to be built into specialized child classes. For instance, if you have an API returning data that requires cleaning or standardizing, group those adjustments in one primary location, leading to easier maintenance.
00:12:29.570 Single Table Inheritance (STI) also surfaces frequently in discussions. It’s worth mentioning that while it can provide clear readability, its complexities in testing and risk of side effects remain comparable to those of class inheritance. Transitioning them out after development tends to be a more time-consuming challenge.
00:12:52.750 That's all for my talk today. If you have any questions or want to discuss further, feel free to reach out to me. Also, I’ve included a picture of my dog, Ruby, who I love dearly and is aptly named for my appreciation of Ruby! If you want to connect, here are my social media handles. Thank you!