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

[Tropical.rb 2024] A Brewer's Guide to Filterin...

[Tropical.rb 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

May 12, 2024
Tweet

Video

More Decks by Alan Ridlehoover

Other Decks in Programming

Transcript

  1. Ola! Obrigado por nos hospedar! Bem-vindo! Welcome! We call this

    talk “A Brewer’s Guide to Filtering out Complexity and Churn.” It’s also known as, “The Coffee Machine Talk.” Our goal is to show you how to remove from your applications the bitterness caused by complexity and churn. 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…
    ALAN
  2. I’ll go first. Hello! I’m Alan. I have 13 years

    experience with Ruby. And, I’m from Seattle, so there’s coffee in my veins. My favorite is a sugar free vanilla oat milk latte. Mmm! 
Hi! I’m Fito. I also have 13 years of experience in Ruby. 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 Ghirardelli dark chocolate mocha. Mmm! Alan and I work together at a company you might not expect, given that this is a conference for Rails developers. We work for Cisco Meraki—the largest Rails shop you’ve 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?
    ALAN & FITO
  3. Yes, 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. Ah! Magnifico! 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. 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:
    ALAN
  4. 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 its 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.
    ALAN
  5. 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, and fall back in love with Ruby.
    ALAN
  6. We’re going to build a little coffee machine. We’ll add

    several features, then go back and look at how complexity snuck into the program with each commit. Finally, we’ll reorganize the code to enable us to add new features without increasing complexity. Ok, are we ready?
    ALAN
  7. ALAN
  8. Yes! Let’s do it! 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.
    FITO
  9. 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…
    FITO
  10. 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…
    FITO
  11. 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…
    FITO
  12. 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…
    FITO
  13. 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…
    FITO
  14. 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…
    FITO
  15. 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…
    FITO
  16. 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?
    FITO
  17. 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?
    ALAN
  18. 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.
    ALAN
  19. So, our first commit weighs in at a complexity of

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

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

    10.9. 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.
    ALAN
  22. ALAN
  23. Adding cream causes the complexity to rise again, to 16.0.

    Still in the “good enough” zone.
    ALAN
  24. Adding cocoa pushes complexity all the way up to 21.3.

    That’s just above the “good enough” line. Let’s see where one more feature puts us.
    ALAN
  25. ALAN
  26. 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.
    ALAN
  27. 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…
    ALAN
  28. 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…
    FITO
  29. 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.
    FITO
  30. 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.
    FITO
  31. 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…
    FITO
  32. 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…
    FITO
  33. 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.
    FITO
  34. 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.
    FITO
  35. 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.
    FITO
  36. 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…
    FITO
  37. 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 factory class. We can solve that by using a different kind of factory.
    FITO
  38. So, here we’ve replaced the conditional-based factory with one based

    on a convention and a little bit of meta-programming. The advantage of this approach is that it is mostly open/closed. There might come a day when we’d want to introduce a class name that doesn’t follow this convention. At that point, you’d need to modify the convention, which would violate the open/closed principle. There are other approaches that are open/closed. And, we’d be happy to discuss them offline. But, that’s beyond the scope of this talk. So, we’ll stick with our convention-based factory for now. At this point, let’s take a look at where we are with complexity.
    FITO
  39. 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.
    FITO
  40. 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.
    FITO
  41. FITO
  42. 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.
    FITO
  43. 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 3.9. The factory is next at 6.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.
    FITO
  44. 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.
    FITO
  45. FITO
  46. There you go! Adding soup did not require us to

    change any of the existing files. It just works. As long as the Soup class is loaded into memory, it will be available to the factory. And, in the future, adding another beverage will be as simple as adding another class. (And, its tests.) Let’s look at that again.
    FITO
  47. FITO
  48. And, here’s the code with soup. Nothing else changed. 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?
    FITO
  49. Yeah, but does it scale? How to apply this to

    a large, complicated code base.
  50. 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.
    ALAN
  51. 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…
    ALAN
  52. 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.
    ALAN
  53. 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…
    ALAN
  54. ALAN
  55. 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.”
    ALAN
  56. 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.
    ALAN
  57. 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.
    ALAN
  58. So, those are the three take aways. Now, here’s what

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

    your application using the Churn gem
    ALAN
  61. 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.
    ALAN
  62. 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.
    ALAN
  63. 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.
    ALAN
  64. That’s it! Thank you so much for coming! 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. Obrigado!
    ALAN