Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[Rocky Mountain Ruby 2024] A Brewer's Guide to ...

[Rocky Mountain Ruby 2024] A Brewer's Guide to Filtering out Complexity and Churn

Abstract

Mechanical coffee machines are amazing! You drop in a coin, listen for the clink, make a selection, and the machine springs to life, hissing, clicking, and whirring. Then the complex mechanical ballet ends, splashing that glorious, aromatic liquid into the cup. Ah! Delicioso!

There’s just one problem. Our customers also want soup! And, our machine is not extensible. So, we have a choice: we can add to the complexity of our machine by jamming in a new dispenser with each new request; or, we can pause to make our machine more extensible before development slows to a halt.

Alan Ridlehoover

October 07, 2024
Tweet

Video

More Decks by Alan Ridlehoover

Other Decks in Programming

Transcript

  1. ALAN Hello! Welcome! We call this talk “A Brewer’s Guide

    to Filtering out Complexity and Churn.” Or, “The Coffee Machine Talk” for short. Our goal today is to show you how to remove the bitterness caused by complexity and churn from your applications. Over the next 30 minutes, we will show you: How complexity sneaks into a code base; How to recognize complexity before it becomes painful; and, How to remove it permanently. Let’s introduce ourselves and get going…
  2. ALAN I’ll go first. Hello! My name is Alan Ridlehoover.

    I use the pronouns he/him. And, I have 13 years experience with Ruby. Also, I grew up in Seattle, so there’s coffee in my veins. My favorite is a sugar free vanilla oat milk latte. Mmm! FITO
Hi! My name is Fito von Zastrow. I use the pronouns he/him. And, I’ve also been using Ruby for 13 years. I’m from Asunción, Paraguay. And, if there’s one thing I love as much as Ruby, it’s coffee! My favorite is a dark chocolate mocha. Mmm! Alan and I work together at a company you might not expect, given that this is a conference for Ruby developers. We work for Cisco Meraki—the largest Rails shop you've probably never heard of. But, we’ve been friends for years. In fact, we’ve worked together at three different companies over the last ten years. And, we’ve seen a wide variety of code bases. Plus, we spend time together on the weekends, writing code and drinking coffee. Alan, you grew up around coffee, didn’t you?
  3. ALAN Yeah, I did! In fact, back when I was

    a kid, nothing fascinated me more than the mechanical vending machines at my dad’s office. You dropped in a coin, listened for the clink, and made your selection. The machine would spring to life: hissing, clicking, and whirring. When the automatic ballet ended, the final sound was that glorious, aromatic, black liquid splashing into the cup. C’est Magnifique! These days, I’m more fascinated by the inner workings of software. (We both are.) And, like that coffee machine, there are all kinds of hidden complexity in code. But, software doesn’t start out complex, does it? Show of hands: how many of you have worked on a greenfield or brand new application? How did that feel? Ok. How about legacy applications? How many of you have worked on one? How did that feel? In our experience, greenfield development is enjoyable. It feels fast. There’s no existing code to work around. But, developing in a legacy applications feels harder. Why is that? We believe it has to do with complexity. We think that at some point a code base crosses a complexity threshold, after which we have two choices:
  4. ALAN You can live with the complexity as it grows

    and development slows down more and more over time under the illusion that it will go away on it’s own. Or, you can pause temporarily to reorganize the code and accelerate development again. We’ve seen organizations go down both paths. Invariably, when you take the path of living with the complexity, engineers end up frustrated.
  5. ALAN And, in our experience, they sometimes even begin to

    blame Ruby and start looking for alternatives. But, it’s not Ruby fault. It’s the complexity. So, what we’re going to do is show you how to take that second path, remove the complexity…
  6. ALAN …and fall back in love with Ruby. <pause> Alright,

    let’s dig in! Fito, you wanna build a coffee machine?
  7. FITO So, how does complexity sneak into software? The answer,

    of course, is one commit at a time. Let’s take a look. Now, I’m going to move through these slides pretty fast, just to show you the shape of the code as it grows. And, as the code gets longer, the font size will get smaller. Don’t worry about trying to understand the actual code, it’s made up anyway. Also, we’re skipping tests here for the sake of time. But, in reality, we would be doing this with tests.
  8. FITO Here is the first commit in our coffee machine.

    At this point the machine does one and only one thing. It serves coffee. First it dispenses a cup. Then it heats the water, prepares the grounds, and dispenses the hot water. Finally, it disposes of the grounds. It works great, but not everyone likes coffee. So, to increase our sales, let’s add tea…
  9. FITO Here we added a conditional to determine whether to

    serve coffee or tea. And in the process of doing that, we added some duplication. 
 The dispense_cup, heat_water, and dispense_water steps are all duplicated between the two beverages. So, let’s DRY it up…
  10. FITO Here’s the DRY version of the code. With both

    coffee and tea in production, we’re starting to get feedback. The most frequent request is to add sweetener. So, let’s do that…
  11. FITO Here, we’ve added sweetener just after dispensing the hot

    water. Of course, not everyone wants sugar, so let’s make it optional. We pushed this out. Customers like it. Now, they want cream…
  12. FITO Since we already have a pattern for optional ingredients,

    let’s dispense cream right after we dispense sugar. For our next feature, it turns out that some folks don’t like coffee or tea. So, let’s offer them something else, like cocoa…
  13. FITO Here, we followed the existing pattern and added cocoa

    to the main if statement. But, there’s no need to add milk or sugar, since cocoa is already sweet and creamy. So, let’s exclude those optional ingredients when the customer requests cocoa. Finally, who doesn’t like whipped cream on their cocoa? Heck, I even like it on my coffee! So, let’s add it…
  14. FITO Ok. So, whipped cream is an optional ingredient that

    no one wants on their tea. So, let’s add it after the other optional ingredients and exclude tea. So, here we are…
  15. FITO 7 commits into this codebase, and we’ve already got

    9 conditionals in one method. At this point, it’s still relatively simple to understand and work with, if you’re the only one working on it. But, if you are part of a team, procedural code like this won’t scale. Future developers will just keep adding more conditionals with each new feature, causing complexity to sky rocket. And… Our little coffee machine has been so successful that it was just purchased by a BIG national soup chain. They want us to add soup to our machines. That’s going to add a lot of complexity to our code. So, let’s pause here and evaluate where we are before trying to add any more features. Alan, can you take us through it?
  16. ALAN Sure! So, we’ve reached an inflection point in the

    life of our little coffee machine. But, how can we tell? What is it that tells us to pause and restructure? Well, the first hint is that we had to start reducing the font size to display the whole method on one slide. Method length is definitely an indicator that things are getting complex. Sandi Metz — author of Practical Object Oriented Development in Ruby — has a rule about it. She says methods can only have five lines of code. In addition to method length, we also look at method complexity. This is a quantitative measurement of how difficult it is to understand a piece of code. Our preferred metric is called the Assignments, Branches, and Conditionals (or ABC) metric. The higher the number, the harder the code is to understand. We use a gem called Flog by Ryan Davis to measure this for us. Flog calculates an ABC complexity score for each method in an application. But, how do we know a good score from a bad score?
  17. ALAN Well, all the way back in 2008, a guy

    named Jake Scruggs (who wrote the metric_fu gem) wrote down these numbers for the flog score of a single method. Over the years, we’ve used these numbers as our guide. And, they’re actually quite effective at helping us drive our code toward simpler solutions. So, how does our little coffee machine fare? Let’s go back through the commits and watch complexity over time.
  18. ALAN So, our first commit weighs in at a complexity

    of 5.0. That’s awesome. The churn number is the total number of commits to this file. That’ll become important later.
  19. ALAN Adding the conditional with duplicated code really shot up

    the complexity. At 13.5, we’re no longer in the Awesome zone. But, we’re still below 20, so that’s good enough.
  20. ALAN After removing the duplication, the complexity drops back down

    to 10.0. This may seem like a good thing. But, this is actually where things really start to go wrong. Notice how we’ve intermingled the two algorithms in a way that makes it harder to see what it takes to brew coffee or steep tea. It also sets a precedent for future developers to extend this code by adding new beverages into the mix using more conditional logic. But, from Flog’s perspective, complexity went down. It can’t tell that we intermingled two algorithms. It’s just doing the math. This is a valuable lesson. There is no magic metric that can light the way in every situation. Rather, there are tools that can inform our decision making. So, pay attention to how hard it feels to add new features to your application. If your intuition is telling you that it’s getting slower, then you might want to pause to reflect on your design.
  21. ALAN Next, we added sweetener. This is our 4th change.

    And, complexity has risen to 12.3.
  22. ALAN Adding cocoa pushes complexity all the way up to

    19.6. That’s just under the “good enough” line. Let’s see where one more feature puts us.
  23. ALAN If we look at the trend line, we can

    see that the complexity has reached a point where the line is curving upward. Plus, we are over the “good enough” line of 20.
  24. ALAN So, there you go, three ways to know when

    it’s time to pause and reflect on a method: 1. Method length. Anything over 5 is forbidden by Sandi. So, keep it short. 2. Method complexity. Anything under 20 is good enough. Anything over 60 is getting dangerously complex. 3. And, how does it feel? If new feature development is slowing down, it might be time to pause and reflect on your design. So, that’s how we knew that we’d reached an inflection point. It’s time to start thinking about reorganizing this method before we try to add more features to it. And, that’s what Fito is going to do right now…
  25. FITO So… We broke Sandi’s rule. We crossed over Jake’s

    good enough line. And, we intermingled three algorithms. Sounds pretty dire. But, is it? Can we turn this code around? Yes. Absolutely. Let’s look at how…
  26. FITO Here’s the method as we left it a moment

    ago. Code that is DRYed too early can lead us in the wrong direction. So, let’s un-DRY this code — or add back the duplication — to see if there are any missing abstractions hiding in plain sight. We call this practice “rehydration.” Now, before I show you what that looks like, it’s important to note that you can’t do any of this without tests. For the sake of time, we won’t be writing them from scratch here, but ensuring there’s good test coverage is the first step towards reducing complexity.
  27. FITO We really like this tool called SimpleCov. We use

    it to ensure that we’ve tested every line and branch of code in our applications. As you can see here, we have 100% line coverage. That’s great! We also have 100% branch coverage, which means that we’re testing both sides of every conditional in the code. Getting to 100% branch coverage is really important before rehydrating code. It gives you confidence that you’re not inadvertently changing behavior in the process of refactoring the code.
  28. FITO Alright. So, this is where we left the code

    a moment ago. Since we’ve confirmed that our tests are backing us up, we’re now ready to rehydrate the code. That looks like this…
  29. FITO <PAUSE> Obviously, this increases duplication. But, that’s what we

    need to do to find the missing abstractions. Now we can clearly see each recipe. And, since there’s no overlap in the algorithms anymore, we can safely extract each one into separate, polymorphic classes, like this…
  30. FITO <PAUSE> Now, as you can see, we moved each

    recipe into its own class: one for coffee, tea, and cocoa. This structure has a couple of big advantages: Each algorithm is now separate from the others. That means if you should ever need to modify one of them (to fix a bug, for example), there’s a much lower risk of you introducing a regression in another one of the algorithms. Plus, the vend method is much simpler.
  31. FITO Now, you may have noticed that there’s duplication between

    the classes. The calls to “dispense_cup”, “heat_water”, and “dispense_water” are all present in every class. We actually want that duplication. It makes understanding the complete algorithms much easier since the whole algorithm for each beverage is present in each beverage class. So, we do not want to DRY up the algorithms, per se.
  32. FITO Rather, what we want to do is to ensure

    that there is only one implementation of each of those methods. That’s what’s meant by Don’t Repeat Yourself. It is perfectly ok to call a method multiple times. It is preferable to only implement that method once. Ruby provides multiple options for doing this. We could include a module, use composition, or introduce inheritance and put the methods in a base class. In this case, because we’re using polymorphic classes, and because we’re unlikely to need these methods elsewhere in the application, we’d probably go with inheritance. We’re almost done. There’s just two remaining problems. First, the vend method has multiple responsibilities. And, second it is not open/closed, meaning that you have to modify the code to extend it. Let’s take a look at its responsibilities first. Its only real responsibility should be preparing the beverage. But, right now, it’s also picking which class to instantiate. That’s the job of a factory. So, let’s introduce one.
  33. FITO Here, we’ve pulled class instantiation out into a factory

    class. It’s only job is to choose which class to build based on what drink was selected. Now the vend method only has the one remaining responsibility — to prepare beverages. And, it is now open/closed as well, meaning that we’ll never have to modify the vend method again to extend the functionality of the coffee machine. The vend method is now open for extension, but closed for modification. However, the open/closed problem just moved to the factory. And, we introduced another issue. The build method in the factory might return nil, causing the CoffeeMachine’s vend method to throw an undefined method error. We can solve that by introducing the Null Object pattern, like this…
  34. FITO As you can see, the factory returns a NullBeverage

    by default. The NullBeverage class is simply a class with a prepare method that does nothing. As for the second problem with the factory, it’s still not open/closed. To add a new beverage, we’ll have to modify the if statement in the build method of the factory class. We can solve that by using a different kind of factory.
  35. FITO So, here, we’ve converted the if statement into a

    hash lookup. Now, the build method is open/closed, and adding a new beverage only requires us to add an entry to the beverages hash. Plus, because the factory is now hash based, it goes from O(n) to O(1) when looking up a beverage class. Ok. Let’s take a look at where we are with complexity…
  36. FITO Let’s start by revisiting this graph. Here’s where we

    left off after adding whipped cream. The next thing we did was to rehydrate the code.
  37. FITO Look at how much more complex that was than

    the DRY solution. But, remember, the DRY solution was hiding the fact that there was a missing abstraction. Next, we pulled the algorithms out into their own polymorphic classes. This dropped complexity significantly.
  38. FITO And, now, the complexity is lower than it’s ever

    been. We’re now well back in the Awesome range. And, the vend method will never need to change again.
  39. FITO Now, let’s take a look at all the other

    classes in the app. There are six. CoffeeMachine has settled down to a very low complexity of 2.2. Coincidentally, the factory also weighs in at 2.2. And, the three beverages are all more complex. But, well within the “good enough” zone. So, there’s really nothing more to do here.
  40. FITO Except that there was a reason why we did

    all that. We needed to add soup to our coffee machine. And, we wanted to do so without making it more complex.
  41. FITO There you go! Adding soup did not require us

    to change any of the existing methods. We just added the new class and updated the beverages hash.

Now, some people consider this “open/closed” because the only change to the factory was to the configuration data in the beverages hash. We’re a little squeamish on calling this “open/closed.” Our preferred method of doing this is to have the beverage classes register themselves with the factory. That looks like this…
  42. FITO So, we’ve extended the BeverageFactory class to include a

    register method that the beverage classes can call when they are loaded. This method adds the class to the factory. And, now, there’s no longer a separate hash to maintain. In fact, this solution is now truly “open/closed”. We can extend the functionality of the coffee machine without modifying any existing code. Like this…
  43. FITO Alright! We added apple cider, and nothing else changed.

    And, if we want, we can hide the registration behind a little DSL. Let’s do that…
  44. FITO Here’s what that looks like. We introduced a Beverage

    base class. It has a “prepares” class method that does the registration. And, now the individual children of the Beverage class can call “prepares” instead of BeverageFactory.register. We really like this pattern. It’s more declarative. And, to us, it’s easier on the eyes. <pause> So, that’s a look at a very small, green field application. But, your applications are obviously a whole lot bigger and a whole lot more complex than that. So, how will you know where to start when you get back to work?
  45. ALAN I’m glad you asked! So far, we’ve really only

    talked about method complexity and how watching it as the method changes over time can help you prevent complexity from becoming painful and slowing you down. But, you can also use complexity with churn to find problems across your entire codebase. As we mentioned, complexity is the measurement of how hard it is to understand a bit of code. Churn is a measurement of how many times that bit of code has changed. We like to think of it like this: complexity represents how much pain you will experience the next time you touch a file, while churn represents how often you are inflicting that pain on yourself. Let’s look at how to use churn and complexity together to evaluate your whole codebase.
  46. ALAN To find the areas that need the most attention

    in your application, plot file complexity and churn like this using a tool like CodeClimate or the RubyCritic gem. This kind of churn vs. complexity chart was first proposed by Michael Feathers, author of Working Effectively with Legacy Code. We use this kind of chart to locate the areas of our code that need an intervention. To understand the chart, let’s break it into quadrants, and take a look at each one…
  47. ALAN The lower left quadrant is the pain free zone.

    These files are easy to change, and easy to understand. In a healthy application, the majority of files live here.
  48. ALAN The upper right quadrant is the painful zone. These

    files are hard to understand, hard to change, and prone to regressions. Being aware that these files are in this quadrant will help you make decisions about where to add new code. You probably don’t want to add more code to these classes. In fact, if you have to touch them, prefer extraction over addition. This will create simpler classes with low churn scores, putting them in the pain free zone!
  49. ALAN Down in the lower right are low complex files

    that change frequently. It’s possible that these files are actually configuration data masquerading as code (e.g. JSON hiding in a .rb file). If so, try to move the ever changing configuration data out of the code and let the code read it from a file as needed.
  50. ALAN And, finally, the upper left are high complexity files

    that rarely change. These are likely what Sandi Metz refers to as Omega Messes. An Omega Mess is a file with a big, scary algorithm that never needs to be changed. Sandi’s advice in this case is that you should leave these files alone. They’re not causing any continued pain. And, mucking about in them could lead to bugs. So, just leave ‘em be. These quadrants are helpful to think about. But, reality actually looks like this…
  51. ALAN The red line represents the pain threshold. Anything in

    the pink zone will be resistant to change and prone to regression. This file <<ANIMATE>> is in need of the most attention. It’s super complex. And, it is being modified all the time. Extracting hidden abstractions from this class will help simplify the entire application. But, there’s no need to tackle all that complexity at once. Try improving the code a little bit each time you touch the file. Also, you may not want to start with that file. It’s super complex code. Maybe start with something over here. <<ANIMATE>> It’ll give you a chance to practice some of the techniques we showed you without the pressure of working on some of your most complex code.
  52. ALAN So, that’s the story of our little coffee machine.

    How complexity snuck in. How we recognized it. And, how we removed it. Let’s wrap up with some take aways and a bit of homework…
  53. ALAN First, complexity WILL sneak into your code. It happens

    one commit at a time. So, be vigilant. Pay particular attention to conditionals in your code. They could represent objects trying to escape your method. And, remember that DRY is about method implementation, not invocation. As my friend Josh Clayton says, “Don’t make your code so DRY it chafes.”
  54. ALAN Second, you can recognize complexity before it becomes painful.

    Keep methods short. Watch your complexity. And, pay attention to your intuition. If your methods are longer than 5 lines, or your flog scores are over 20, or it just feels slower than it used to, then it’s probably time to pause and reflect on your design.
  55. ALAN And, third, you can back away from painful complexity.

    Leverage polymorphism and factories to enable you to add new features to your application without having to change any existing files. Rehydrate your code — or reintroduce some duplication — to help identify missing abstractions that can be refactored into polymorphic classes.
  56. ALAN So, those are the three take aways. Now, here’s

    what we want you to do with that information.
  57. ALAN Second, find out which file has the most churn

    in your application using the Churn gem
  58. ALAN Third, find out which class needs the most attention.

    That’s the file with the highest churn and complexity. We bet you already know which class that is. But, go ahead and confirm your suspicions. And, finally, let us know what you find.
  59. ALAN Here’s how to reach us. Feel free to write,

    tweet, or give us a toot! (Actually, you’re more likely to get an answer if you toot. Neither one of us are on Twitter much anymore.) You could also subscribe to my blog, if you’re so inclined. Also, if you want to walk your way through the little coffee machine application, you can find it (with tests!) on GitHub. Plus you can check out our other personal projects while you’re there.
  60. ALAN One of which is a VS Code extension that

    shows you the flog score for selected text, the current method, or the average score for an entire class.
  61. ALAN One last thing before we go… As we mentioned,

    we’re from Cisco Meraki, the largest Rails shop you’ve never heard of. We don’t build coffee machines. But, we do build Internet machines for coffee lovers. <<ANIMATE>> Both Starbucks and Pete’s Coffee use Meraki devices to connect their stores and customers to the Internet. Our 17 year old Rails monolith has over 2 million lines of Ruby code. It’s old, yes. And, it’s super complex, yes. And, it handles billions of requests per day, supporting a multi-billion dollar business. If you’re interested in solving really hard problems related to code complexity, software design, and large scale Rails deployments, come chat with us. We’re always looking for more great Rubyists.
  62. ALAN Here’s a list of our references and influences. (Take

    a picture if you’d like!) If you have any questions, come find us or drop us a note. Thanks! And, thanks to Spike and Bekki for allowing us to share our story with you!