RubyConf 2018 - Inheritance, Composition, Ruby and You by Cody Stringham

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
("Kandi Funky House" by Bobby Cole) - All right, welcome to my talk. If you haven't guessed by now, it is on inheritance and composition in Ruby. So, a little background on this talk, why did I write this? So, when I started looking into these concepts, I found a lot of information that was kind of geared around more experienced developers. Maybe it was developers coming from another language that already knew these concepts, they just wanted to know how to do them in Ruby, but I didn't find a ton of material that kind of combined these two ideas in context so that you could compare and contrast them. So, I wrote this talk for people like me, in that scenario, that maybe you're new to programming, maybe you're new to Ruby, maybe you're new to object-oriented programming, maybe you've just done functional, and you wanna understand these things, and understand why people tell you things like prefer composition over inheritance, so let's get started. So, inheritance is a little bit of a sensitive subject for a lot of programmers, especially when you talk to seasoned programmers, it's kind of interesting, the feedback you'll get about it, and I think that this is mostly due to a lot of people getting burned by inheritance. I think a lot of people have bad experiences with it, being implemented poorly or not really fitting the problem that it was used to solve, and I'd love to troll my Elixir friends by talking about inheritance, so, that's where that comes from. So yeah, when you talk about inheritance, you get a lot of angry people. You know, they just don't wanna deal with it, super mad about inheritance, or maybe just sad, you know. So, the goal of this talk is really to give you guys a firm understanding of what object inheritance is and what object composition is, so that you can understand the tradeoffs of both. I'm a firm believer that there's not an absolute solution for problems that you're trying to solve, at least in programming, so we really need to understand the problem space that we're trying to work in and understand the tools that we have available so that we can solve that problem effectively, so that's what this talk is hopefully gonna do, is give you that understanding of these two tools. So, before we can talk about inheritance, we need to talk about Ruby, and I'm just gonna go into some actual demo code. Can everybody read that okay? Bigger? Okay, so, this is about as simple of an example as we can get, so puts Hello World, this is probably one of the first things that you saw wrote in Ruby, but has anyone ever thought, like, how does this actually work? Where does puts come from? How come I can use it in a top level instantiation like this? I hadn't thought about it until I wrote this talk, so maybe I'm alone there, but it's actually really interesting, so, we've got some different examples here. If I run this file, obviously it works. It's gonna output Hello World! That's gonna work top level, it's gonna work if we put it into a class method, so this one, we're just, we're defining a new class, Hello2, we have a class method, using the self keyword, so we're gonna call it on the class itself, called greet, and then we're gonna put just a message in there. You can see that when we run this, we get Hello from a class method, just like we would expect. So, when we start to get into the internals of Ruby, what we're gonna start, we're gonna find out, is, when we start to have these naming collisions, you get some interesting behavior, so, what we're talking about here, oops, is method lookup, so, when we get into something like this where we have a class and this class actually defines a puts method, and we're just accepting a message, what's gonna happen is when we call puts on line 10, it's not gonna use the normal puts that it usually does, it's actually going to use the puts method on line four, so we can see this working. Instead of actually outputting hello from an instance method, you can see in our puts method, we're just ignoring that message, and we output, What is the password? So this is kind of the very basics of inheritance for a method lookup, and I'm not gonna go into full method lookup, that's not the point of this talk, but I really just wanna give you an idea of how this stuff works, so, what happens is, in a Ruby file, in a class, when you call a method like puts, it's gonna check to see if that class has that implementation. If it does, it will use that implementation. If it doesn't, it will continue to look for it, and we'll explain where it goes to find those. So here in our next method, we've got another interesting example. We'll skip over the speaker for now, but right down here, we've got a class Hello4, and this little less than sign is telling us that we're gonna inherit from this speak class, so we have a speak method, we call puts, and you'll see from the output that we get Hello from the great unknown! So, it's stopping here, and we're putting, this is a global variable, standard outputs, and that's how the actual puts method works under the covers. Does anybody know why we can't call just puts here? - It'll call itself. - It'll call itself, perfect. So, if we were to instead put, well, get rid of that. So we get a stack level too deep error, and you can see this, we get from Hello4, and it just keeps looping over puts, so it keeps calling itself recursively. That's not what we wanna do. In our next example, so we have Hello5 here. This one is going to, it has an attribute on it, and it's gonna initialize that attribute with just a string and its value, so it also has another method called puts, and you'll notice my linter down here is actually telling us a little error, it's telling that we have a duplicate method so, this is kind of interesting because Ruby doesn't necessarily respond to messages with only methods. It can also respond with attributes, and that's why you see this here, so we're defining kind of a responder on puts on line four, and again, on line 11, so, when we call puts here, it's kind of a mystery, like, what we're gonna get back. Are we gonna get back the method, or are we gonna get back the attribute? So the method is set right now to accept a default value, or it doesn't need a value, it'll default to nil, so when we call this, we get What is the password? We don't actually get the string attribute, and why that is, is in Ruby, the kind of the last one wins, so if we move our attr_reader below this, you'll notice that my linter changes, right? So now it's telling me that this is the duplicate method, and you can imagine that when we run this, it's not actually gonna output anything because it's just returning a string. If we change that to actually call puts, we'll get the string attribute. Okay, so, on this example, we have another class with another method that you're used to seeing now, but now inside of speak, we have puts as a local variable. When we call this, we're gonna get that same thing. We're not gonna get a return value, unless we actually output it, oops. And then we get the local variable, so kind of the basics of this method lookup, is going to be, check inside of my method that I'm calling this from, if I have a local variable, cool, use that, if I don't, go into my class, do I have a method? Use that, if I don't, go to my parent class. If I have a method, use that, so, to answer the question, where does this puts come from, we've got our last example here, and I get undefined method puts, so you might notice something kind of shady about this file requiring the setup file, so I'm sure that's where a lot of you are drawing your attention, so if we look at this setup, you can see that module kernel, we have removed the method puts. That's why we get that error. So, in Ruby, we'll go over this in the next few slides, every object that you create will have an inheritance tree. That's just how Ruby is built, and in that inheritance tree is a kernel, and that's where methods that you probably take for granted or never thought about, like puts and sleep, are defined, so any time you call those, you're bubbling up that ancestor tree until you get to that kernel class, unless you've actually defined them. All right. So, now that you know a little bit about the method lookup, we can start to talk about inheritance, so what is inheritance? So this is from Wikipedia. I won't read it verbatim, but the basic idea is that, you have a child object, and when you inherit, that child object will acquire all the properties and behaviors of the parent that it inherits from. So you might have heard it described that inheritance should be used for an is a relationship, so an example, a duck is a bird, where a duck would inherit from bird, so you could set up a base class of Bird and give it common attributes that a bird would have, like wings, feet, whatever you want, and then you could make a child class of Duck that would inherit all of those features and then specialize it for stuff that only ducks have, right, like a buoyancy level or something like that. So inheritance in Ruby, we have a couple different types and tools available to use it, so the first one that we talked about and one that you've probably seen all over the place, especially if you're a Rails developer, is this little less than sign, so, in action controller, active model, stuff like that, you're gonna see a lot of inheritance in those framework-y stuff. So, this method down here, so I've got at the end, Child.ancestors, is this method, this ancestor tree that I was talking about, so, with nothing else, we have a child and its next ancestor is a parent. A parent's ancestor is an object, and then it goes to kernel, and then to basic object, so any class that you create outside of this parent inheritance will have these kind of, it inherits from object, it inherits from kernel, it'll inherit from basic object. We also have multiple inheritance in Ruby, so, there's more than one way to do this. We're only gonna talk about include. The other two, there's also extend and prepend, but they're kind of outside of this talk, so, we have the option to include multiple parents, so you can see that we have in our ancestor tree a child, and the first thing that comes after child is the last thing to find, so in this case, it's StepParent. If you were to switch those two, parent would, you'd actually look in parent first, and then step parent. And then, again, we've got object, kernel, and basic object. So, multiple inheritance, any dangers that you've heard about single inheritance is kind of just exacerbated in multiple inheritance. There's a lot more moving pieces and a lot more chances for you to miss something. And then the last thing I wanna touch on is namespacing, so a lot of people will see, include a module, and they'll kind of think, oh, well I've seen classes wrapped in modules, is that inheritance? That's actually not, this is, it's literally just kind of qualifying and creating a namespace around your class, so you'll never refer to Child without that Parent colon colon. You'll always have to access it that way, so it's literally just giving it a different name, so that you can group things together. All right, so, when should you use inheritance? And the answer is, when you wanna specialize an object. A child should use every single attribute and method of a parent, if you really wanna use it correctly, and you should never use inheritance as a way to kind of clean or dry code up. There's much better, much better ways for doing that, we'll actually talk about one here in a second. So, the pros of inheritance are, you can make code reusable and extendable, but there's a pretty big caveat on this, and that is, if it's done correctly. If you don't use inheritance correctly, neither of these things are true. It actually makes it really hard to reuse code and really hard to extend code. The cons, when you inherit from a parent class, you are literally creating a coupling between the two. That's usually a word that's synonymous with bad behavior. You get reduced readability, so, if you have a method in a parent class that's not in a child, but the child calls that, a developer that's looking at this fresh that's never seen it before, is obviously gonna have to do some digging to figure out where that method comes from. It's easy to abuse, so, I would, I would argue that inheritance and its benefits are really tied to properly naming things, so you can create these taxonomies or these hierarchies in your code, where you can create kind of a visualization of how your application works. You've probably heard before that one of the hardest things about programming is naming things, so I would argue that a framework around naming things is probably gonna be hard, too. I think that's why a lot of people get it wrong, is it's hard to define those domains in our systems. And the last point that I wanna bring up is that it can complicate unit testing, so if you're inheriting, it's pretty hard to isolate a child and only test child behavior, because you get all of that parent's behavior for free, and I'll have an example to show that. So, you might be feeling like Tina here, a little bit, like, there's only one pro and there's a bunch of cons, so, why are we even talking about it? Should you just never use it? I think there is a use case for it, but, we'll see how limited that can be. Okay, so, next I wanna talk to you about the alternative to inheritance, composition. So, what is composition? So again from Wikipedia, it is a way to combine simple objects into more complex objects. So you might have heard it described as a has a relationship, for instance, a car has an engine, where a car would be composed with an engine. So composition in Ruby. We have, this isn't really talked about in Ruby, but it's worth bringing up so you know about it, so there's functional composition, which is really just creating a method from other methods, so again, you're not gonna see this in Ruby, but if you look at Javascript examples, or examples in other languages, you'll see this quite a bit, and then we have object composition, so, this example is okay, but I've actually, I've got a different one that we'd wanna show, so we go back here, and I don't have it. Okay, so, I kind of wanna just explain what composition is and how you get to it with a concrete example that you can actually see, so, we're gonna, we have a very simple application for explanation purposes, and this is gonna have one method where we're gonna call an API, and we're gonna save the result, so, there's a couple of problems with this type of programming. Right now, the structure of this is very rigid. It'd be very hard for us to come in here and actually change anything with confidence, knowing that we're not gonna break something. Now, this is very small and easy to understand, but, you know, imagine this in a much larger ecosystem. So, what are some things we can do to make this better? So the first thing is, we can start to break out these dependencies, right? So instead of having this method know what its dependencies are, we can move them out of that method and just allow the method to access them, so this is kind of step one in composition. Step two is gonna be, we actually wanna inject those dependencies into this class, so we don't want the class to hold the knowledge of what its dependencies are. We want the class to only receive that and know that it can do things with them, so, this might be new to some people, it's just a key value argument, and these are just default values, so we're passing in a DB, which is this Repo class, and an API, which is that API class. We're setting those to attributes, and then we're using them in a method, so the benefit here is, we're now able to update and change our application, without changing the code, the internal details of this method, so, as long as we know that if we create a new API object, all we need to know is that that API object needs to respond to this get method, so we're able to interchange anything that we want so long as it responds to this method, which gives us a lot of flexibility. Same thing with the repo, and you can do this with anything that you see possibly changing in your application. Blake likes that. (scattered tittering) Okay, so, when do we wanna use composition? As much as we possibly can. Composition kind of pushes you towards making smaller classes and methods. That's a great thing, that's literally never a bad thing, so, whenever you can use composition, unless you know it's like prototype code or you're gonna throw it away, but it gives you a lot of flexibility and there's not that much work, so pros that we have, it's flexible, like you've seen, you can interchange things and swap things very easily, you can update your app pretty painlessly. It's readable, you're not guessing where things are coming from, it's very explicit, it's all in front of you. It's easy to unit test. It's really easy to isolate each part of the system that you're passing in, and test it in isolation, which is what you wanna do in a unit test, and all the cool kids are talking about it. So the cons, you have to write a little bit more code, that's a con. Okay, so, we're gonna dig a little bit deeper and start to look at some examples of this stuff. Okay, so the first example that I wanna show you guys, and this is to kind of contrast, or to talk about why inheritance can complicate testing, so I have a pretty simple user class here. We have an after_create method where we're going to just populate an attribute which is a GUID. Other than that, we've got some trivial methods like asking if there's a first name, a last name, and a method to change the first name, so we get a new feature request that comes in, that we need some sort of administrator in our system. We don't wanna necessarily write all of this logic. We wanna use what we've got for user, but we do wanna do some things a little bit differently, so, we decide to make an Admin class that inherits from User, and this class, right now, only has one extra bit of functionality, which is to populate this admin true flag. So, let's talk about testing, so when we test our user class, or unit test our user class, it's pretty simple to see, we're just gonna test all of our methods that we have available. Pretty simple, pretty straightforward. When we go to look at our admin class, we're gonna test its method, so there's a huge problem with this. When we inherited from the parent class of User we inherited all of those methods, so, how this can complicate testing, is if we don't test all of those parent methods in Admin, somebody can go in and change our user, and not know that we're dependent on those methods in Admin, right? So someone can go in, they can look at User class, and say, hey, this method's really dumb. I don't think we should use it anymore. I'm gonna delete it. They delete it, they run the test suite, their user test breaks, so they go in, they say, of course it broke, I just deleted it, they fix the test, deploy the code, everything's good. Later on, you figure out that that Admin class is actually using that method, but you don't have a test for it now because it was tied into your user, so, in order to really be a responsible developer, you're gonna have to duplicate all of the tests in your parent object, in your child, to know that you're actually covered, right? If you were to do that, you would have another failing test in Admin where the original changer probably could have seen, oh, this is used in a little more places. I need to actually go do some investigation, so it's easy to overlook those little details like that. I've got another one. So, flexibility, so we're gonna check out another example. Oh. Okay, so, we're gonna talk about how composition can help you when you're trying to actually isolate and test, so this is kind of the contrast to that problem. So, we have a pretty simple SignUp class here. It's got a couple attributes. The ones that are interesting are that persisted and invoice. User payment processor and user repo are just for our initializer so that we can pass things into it, and that's the boilerplate that I was talking about, so you're gonna have to set these classes up, if you're gonna use composition, but it's really not that much more work. We have a branch in here, so if we can create a user, we're gonna set persisted to true, and then we're gonna set invoice to true, if that payment processor can send an invoice, and if it can't, we're gonna set both of those to false, so, it's a pretty contrived example, but what we can do when we're testing this, is we can test each one of our logic branches however we want, right, so we know that as long as we pass in a, when we're testing this, as long as we pass in a user repo, that responds true when you try to call the create method, you can get to this true logic, right? So you can stub out that actual saving and persisting because that's not really what you're trying to test here, right, you should have separate tests for your database actually making sure that you can save things, and then same thing with the payment processor, so in this test, we shouldn't really be testing if the payment processor can send an invoice. We just wanna test that when they can, we actually mark it as invoiced, so you're able to isolate the bits of logic that you actually care about in this class, and allow your other classes to test the logic that they should be concerned with, so you can pass in, you know, you could create a new mock class, actually I think, yeah. So, this is a simple example where instead of passing in the real payment processor, or the real repo, you could create new classes that hold that, and that mock processor could have a create method that just returns true. You could also create an open struct, so as long as you create something and pass it and it can respond to that message, and you can change that, you can isolate your tests a little bit better. All right, so, the funny thing is, when I wrote this talk, I was really trying to defend inheritance. That was kind of my main goal. I know it's in the Ruby language for a reason. I know that there's some good in inheritance, and I really wanted to find it. It was pretty difficult, there's a lot of times where it just, it doesn't solve the problem that you think you're trying to solve with it. But I did think of one that hopefully is a little bit helpful. So, if you have an API where you have common things that you're always gonna share across all of your API calls, like an API token, a key, you know, any type of, kind of setup stuff that you have to do with your API provider, I think that inheritance is a great tool for that. What you're able to do is you're able to actually have these headers defined in one place. If you have very similar error handling for an entire API in different routes, you can put all of that error handling in one place, and then, sometimes you get data back from APIs that isn't exactly what you want it to look like, and if it's consistent, you can also kind of clean that API response in one place, so what you're able to do, is create more specialized versions of this class, so this one is a product class. It inherits from that base object, so I can just call get with the route that it needs and then I don't have to worry about any of my authorization or credentials or anything like that in this class, and then if I need to do something special to clean it, like maybe the index on this one API returns in just a really weird way, I can specialize that call and clean it up the way that I need to, without all of the implementation details. All right, so, if you're a seasoned developer, you might be thinking, what about STI, which is single table inheritance? I didn't write this talk for single table inheritance, but I'll just kind of give the same warning is, it's basically inheritance taken to a database level, so you get all of the same issues that you can get with class inheritance, and I haven't had to do this personally, but I've heard that splitting them apart after the fact is a lot harder, so just make sure that you really have a good use case for it before you consider it for an option. That's all for my talk. This is my contact information. That's my dog, her name is actually Ruby, 'cause I love Ruby so much. (audience cooing) She's very dorky, but yeah, if you have any questions, that's my Twitter handle, or send me an email, or come up and talk after. I'd love to hear any questions you've got offline. I work for a company called Nav. We're in Salt Lake, really cool company. We're growing a ton, so, and even considering remote work, so if you wanna work remoter, come check us out. If you have any questions, come talk to me. Cool, thank you. (audience clapping) (whooshing)
Info
Channel: Confreaks
Views: 4,179
Rating: undefined out of 5
Keywords:
Id: _f2LYPpueAY
Channel Id: undefined
Length: 27min 47sec (1667 seconds)
Published: Fri Nov 30 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.