Peter Bhat Harkins

Lessons of Liskov

wroc_love.rb 2016

00:00:11.580 Hello, thank you. I want to show you a violation of the Liskov substitution principle.
00:00:17.200 We have a post model, just an Active Record model, that can get published. This sets a flag and maybe it does some other work, but I'll keep all my examples very simple because I want to talk about the code and not some other stuff.
00:00:27.759 Later, we add a gallery post to inherit from post and add image galleries because the ad guys love that. It takes so many clicks, and we can charge more money. The gallery post has a special publish function because it has to email BuzzFeed to tell them to come and copy and paste the article onto their site.
00:00:41.110 There's a bug here that violates the Liskov substitution principle. The subclass doesn't remember to set the published flag to live up to the contract that the superclass has, which means some code further down the line might get confused.
00:00:52.690 The fix in this case is to call super and then do our special thing. However, there are still problems with this code, and trying to understand these led me to recognize that I was seeing bugs that were very similar to Liskov violations but not actually Liskov violations.
00:01:03.070 Before we can talk about all that, we've got to define the term Liskov substitution principle. Show of hands— I won't call on anyone who feels they have a good understanding of what it is already. Half of the audience, okay. For those who do know what it is, do you know how many definitions there are? There's a surprise.
00:01:25.869 This talk is about the lessons of Liskov, and it has four parts. First, I will define the Liskov substitution principle and give some examples. Second, we'll expand our view and explore some related concepts. Third, we'll broaden our definition of the Liskov substitution principle to recognize more potential bugs and write better code. Finally, we'll discuss how to achieve that.
00:01:51.700 I am your speaker, Peter Bhat Harkins. I'm a Rails consultant from Chicago, and I work for a company called Dev Mind, who has graciously helped me come out here to talk. I've been practicing Rails for years and years. I tweet at @pushCX and I blog at valentdo, mostly about the principles at work in real-world Ruby code.
00:02:19.480 I'm really interested in the intersection between abstract computer science and actually putting the website up on the internet. The last slide, which I'll leave up throughout Q&A, has a URL for downloading all the slides and my script because I have a script. I get scared on stage. You can download everything if you want to later, so you don't need to take a picture of the screen.
00:02:31.120 There is one slide towards the end that will have five links on it, and you'll want to take a picture of that one, but that is just right on the page, so it's easy to catch. Part one is about the Liskov substitution principle— what is it? And what does it mean that there are multiple definitions?
00:02:59.620 The Liskov substitution principle comes from the SOLID set of object-oriented principles for designing good code and first showed up in the mid-90s. I forgot to mention; actually, there are three substitution principles. When I looked into it, I also found a fourth to add just to complicate things.
00:03:40.030 The first one started in 1987, in an excerpt from a paper by Barbara Liskov which states, 'The intuitive idea of a subtype is objects that provide all the behavior of another type—the supertype plus something extra.' This is similar to how our gallery post provides all the features of a post plus additional functionality. Liskov’s intuitive idea was that the use of a subclass shouldn't break our code, but formalizing what 'shouldn't break' means, while allowing for useful changes, is challenging.
00:04:13.239 Despite the mathematical discussions in her paper, it's mainly two sentences that she does not elaborate on when evaluating designs. I don't think she was trying to create a principle for programmers to follow for writing good software. She was simply showing off a rule of thumb from a project she completed. However, seven years later, Liskov, along with co-author Jeannette Wing, formalized this concept and published a paper defining a subtype requirement for program safety. It contains 19 pages of dense abstract math explaining computation, hierarchy, types, and correctness.
00:05:02.120 , coined the SOLID term, and many believe he is responsible for popularizing the term 'Liskov Substitution Principle.' Under the influence of C++ conventions, he used words like 'principle' instead of 'requirement,' and 'class' instead of 'type.' In this talk, I will stick to the popular convention and call it the Liskov substitution principle (or LSP). I believe it could be better named the 'Martin substitution principle' because he popularized it.
00:06:18.680 Returning to our example, the buggy code violates the Liskov substitution principle because it inherits from post, but its publish method does not perform all of the work that the superclass method does. This violation is why we care about Liskov substitutions in the first place.
00:06:44.110 Is everyone with me so far on this? If you have any questions, now is a good time to wave at me. I will happily take questions during the talk. However, if this part confuses you, the entire talk may not be interesting.
00:07:15.330 Let's discuss three symptoms that I see repeatedly with code that violates the Liskov substitution principle. Here’s a simple controller action that fetches a post, but if it’s not published yet, it redirects the user to the index of posts without allowing them to view it. Because of the violation, which never sets the published flag, we add a conditional check. The first symptom of LSP violations is extra conditionals, especially those that use 'is a' kind of checks, where they have to interrogate the object they’re working with to verify compliance with their principles.
00:08:05.000 Here, we say galleries are always visible. In Rails apps, especially when dealing with single table inheritance, this is fertile ground for LSP violations. In this case, the conditional for resolving the LSP violation resides inside the class, indicating that a gallery post doesn't need to have content.
00:08:30.000 The second symptom appears in this template where we render text posts and galleries differently. We name the class according to the post type, such as 'post' or 'gallery_post,' to generate the correct partial. The concept of 'kinases' comes into play here: two pieces of code must change together. If we rename gallery_post to image_post, we also have to remember to change the name of that partial, or we'll encounter a bug.
00:08:58.000 This brings us to the possibility of a third symptom—a bug. Here’s a scope for searching posts. It will never find any gallery posts because they lack a body. This query wasn't updated to join against images and check image captions or perform other useful functions. This third possibility of bugs is quite common; while it may come off as snarky, it’s often the right option if circumstances mean we’ll never trigger or care about the issue.
00:09:48.000 Bugs can be tricky to introduce and hard to recognize. In fact, the first example I fixed still had a dangerous LSP violation.
00:10:10.000 Let's put the original superclass publish method next to the inherited one. When we compare them, we see that the original publish returns what save does, which is either true or false based on whether it saves successfully. The second one, however, always returns an action mailer delivery job, which is not useful information.
00:10:30.000 This means the information of whether or not it saved is entirely lost. It’s an easy enough fix, but it's very easy not to realize what expectation we have to meet since someone depends on this value.
00:10:44.000 Now, I think everyone can agree that Rails base classes share something in common. Can anyone guess what this fun piece of trivia might be? They cannot even agree on whether to use the word 'action' or 'active.' All of them utilize callback systems; active model has its own while the others reuse one standard callback system. This distinction is interesting because it raises questions about why they are formulated that way.
00:11:20.000 Last year at RailsConf, I gave a talk exploring the other half of this, which is that Rails does not provide tools for managing side effects concerning when models should be created, saved, or deleted. Therefore, if you want code to execute when a post is saved, you must connect with the original save method, or else someone may forget to call it. This convenience is beneficial for prototypes and small systems but can create serious issues in larger systems.
00:11:49.000 Callbacks do save you a lot of boilerplate code needed to avoid an LSP violation. Specifically, in this instance, you only want to send an email if the save was successful. This can be tedious to write: you’ll never achieve something else with that boolean alone, yet we must have exactly this code to avoid creating the bug again. It’s essential to recognize that, if we never override save in the first place, we avoid introducing the possibility of creating the violation.
00:12:18.000 Moving into part two, we examine what’s happening. Why is the Liskov substitution principle so easy to discuss and so easy to misapply? Why do we ignore it in Ruby, even when we are susceptible to violations? When I find myself puzzled, I often explore how different systems handle this problem. I’ve found five different systems worth discussing.
00:12:59.000 First, there’s duck typing, a key characteristic of Ruby. We say, 'if it looks like a duck, swims like a duck, and quacks like a duck, it's probably a duck.' The requirements for passing one object to another are ad-hoc; there’s no static typing or interface listing of required methods. Even if there were, objects wouldn’t have to implement everything completely, only what they need for their usage.
00:13:38.000 Alan Kay noted that one defining feature of object orientation is extreme late binding, and there isn’t a more extreme example than if a user doesn’t call a method, we don’t create other methods. However, the downside to duck typing is we get no guarantees. For example, is my cat a boot? If it’s just a matter of fitting in a shoebox, then yes. But if you try to put your foot in there, you’ll experience a painful runtime exception. We might not discover what's expected from a duck until our code is in production for a month and we encounter unusual user data.
00:14:14.000 Next up are interfaces, which Sandy Metz discusses in her excellent book, 'Practical Object-Oriented Design in Ruby.' In Chapter 6, she talks about creating an abstract superclass to define the interfaces that subclasses must implement. She explores the significance of depending on abstractions and suggests a helpful pattern: for example, we have an abstract bicycle class that sets up an object but doesn’t implement the method default tire size, as there’s no reasonable default for all bicycles.
00:14:53.000 Subclasses, like recumbent bikes, are required to implement default tire size. However, Ruby doesn't provide a mechanism for the superclass to indicate what must be implemented.
00:15:31.000 So, Sandy Metz suggests that the superclass explicitly warns users about methods that subclasses must implement. This makes it easier to spot requirements and provides informative error messages if they are missed.
00:16:08.000 Now, suppose we’re writing a recumbent bike and we face the requirement to implement default tire size. It’s unclear what we’re supposed to return. Is it an integer? Is there a special tire size object we should provide? What if default acts as a verb here? This creates ambiguity that requires deeper exploration of your code.
00:16:48.000 The third system I'd like to touch upon is static typing. For Ruby developers, static typing can be like a cross for vampires, but it does provide real value. In languages like C# and Java, the flexibility of polymorphism occurs at compile time rather than runtime, which we see again in the bicycle example. C# allows you to define exactly what the bicycle interface is.
00:17:41.000 A line defining default tire size serves as a type signature, saying it takes no arguments and returns an integer, whereas Ruby does not have such type signatures to guide what we need to implement.
00:18:26.000 Now let’s consider the fourth system, which is Haskell, renowned for its error handling. Type signatures here might look different from those in C or Ruby. Haskell's type signature represents a function named maximum that finds the maximum element of a list, with constraints outlining what type the function accepts.
00:19:08.000 The maximum function specifies the expected input and the constraints needed to be followed. Therefore, if a type is orderable, there must be a method named compare which takes two values and provides an ordering based on these three values: less than, equal, or greater than.
00:19:48.000 This explicit methodology emphasizes that our type meets the interface and separates it from the class definition. It creates a balance of flexibility and strictness.
00:20:24.000 Now, let’s talk about Ruby’s mixin modules. These screenshots come from the Enumerable documentation. If you supply the spaceship operator, known as the comparison operator, you can utilize the standard functionality that Enumerable offers.
00:20:52.000 Ruby’s Enumerable requires you to implement the each and spaceship operator, which grants you access to several standard methods, resulting in a vast interface.
00:21:44.000 Haskell emphasizes composing tiny abstractions, while Ruby provides a more relaxed and flexible approach, which reflects the uniqueness of Ruby.
00:22:25.000 Returning now to the Liskov substitution principle, we can develop code that will fail—but not due to an LSP violation or due to our fault. Here's our setup: we have that post again, which belongs to an author, and we will summarize a selection of posts.
00:23:10.000 This collection could be an array or Active Record query relation, so we will get the title of the first one and how many others.
00:23:54.000 The bug here is that both an array and an Active Record relation respond to count, and they say, 'I have 23 posts.' Yet a grouped relation will give you back a hash instead of an integer. If the relation was grouped, it becomes difficult for our code to understand that relationship.
00:24:37.000 We have the same LSP violation symptoms, where we either have a conditional check for the return type, a coupling to know a grouped relation will never get passed in, or a potential bug arises. This is why I want to broaden the Liskov substitution principle to include a fourth definition.
00:25:29.000 I want to shift from a focus solely on inheritance or interface alone to substitutability. What is the expected contract from this object? What does it mean for this class or method to be substitutable? This concept is at the core of the five other ideas.
00:26:17.000 If it weren’t for the inconsistency in count, the method for summarizing doesn’t care if it receives an array or a relation or any object that implements first and count. Summarize considers these to be substitutable.
00:27:01.000 There are no formal interfaces or type class definitions. Thus, the heart of duck typing means this is the fundamental role of every object.
00:27:42.000 We’ve experienced substitutability numerous times throughout these examples. For instance, a gallery post can't fulfill the role of a text post because it lacks a body, and count returns fundamentally different objects.
00:28:34.000 Another example is when we inherit from the Standard Library string. The string class has 118 methods available, and when we inherit from it, we're exposing a massive interface to callers, most of which do not make sense or can invalidate data.
00:29:25.000 The issue arises with nil itself. I personally detest nil, as it is rarely the correct thing to return and causes significant problems. Every method returning nil violates substitutability; it undermines our attempts to write reliable programs. Yet nil's presence is widespread in Ruby and Rails.
00:30:16.000 The Ruby language introduces conveniences to obscure nil's failure as a substitutable value, often incorporating nil checks at the top of our inheritance hierarchy. Consequently, nil may be viewed as a special case and leads to repetitive and non-reformative checks throughout the system.
00:31:03.000 I want to take a moment to reflect on our findings thus far. We’ve defined substitutability as a new tool for recognizing when code may go wrong.
00:31:55.000 In the pursuit of preserving substitutability, the simplest approach is to implement multiple methods with different guarantees. For example, Active Record relations can maintain existing behavior while adding another method to extend the contract. I strongly recommend this—a simpler code is often much easier to comprehend. Furthermore, when dealing with errors, raising exceptions instead of returning nil can enforce the caller to manage error handling.
00:32:50.000 Utilizing this method enhances clarity and reduces confusion. Consider implementing types like 'maybe' or 'either'; while typically resulting in confusion due to Ruby’s legacy code, they can offer deeper insight when creating reliable Ruby code.
00:33:48.000 We see other options for ensuring substitutability. The null object pattern can replace an object representation with a non-functional version. However, the null object pattern has limitations because it cannot utilize nil's special cases.
00:34:20.000 For example, it cannot be used in conditionals nor replaced by convenience methods available in Ruby. The best practices emphasize being suspicious of such conveniences as they often indicate the potential for violating substitutability.
00:35:08.000 To summarize everything discussed today, our ultimate goal is to uphold substitutability. This allows callers to have a seamless experience without encountering nil errors or subtle object differences. By encoding and employing predictable interfaces, we achieve reliable programming in Ruby. Thank you for your time and attention.
00:35:52.000 Now I'd like to open the floor for questions.
00:36:09.000 One question I have for you is, how can we obtain more feedback while writing Ruby code to ensure safety?
00:36:25.000 Indeed, in frameworks like Haskell, you receive compile-time feedback. In Ruby, we won’t ever achieve that level—its beauty comes from its flexibility, allowing for method definitions at runtime.
00:36:59.000 Further, should we introduce static type signatures into Ruby or gauge preconditions for arguments? Some languages like Eiffel explore this concept, emphasizing formal conditions. This practice minimizes potential bugs.