00:00:09.519
Sandi Metz is a bicyclist who also codes, and she's from Durham, North Carolina. She and I share a bond of being old-time small talkers, having learned object-oriented programming the right way. She has written an outstanding Ruby programming book of the year, which I'm sure she'll mention in her talk. We actually have a couple of copies that we're going to give away tomorrow in a random drawing. Now, enough of me talking; Sandi Metz is going to talk to you about making a mess and cleaning it up.
00:01:07.439
Before we get started, I have just a couple of things to address. Is the echo as bad for you as it is for me? No? Okay, good, I’ll just deal with it. This is a 45-minute talk, and I know you all are post-lunch and experiencing low blood sugar. So, those of you who did not already get a snack, you're not going to get one until 39 minutes from now—just reconcile yourself to that right now. I'm from the East Coast, and if I were at my house, I would be having a beer right now, so I don’t feel all that bad for you.
00:01:42.079
So, let’s discuss the life cycle of a Rails app. Here’s a story I believe you all know: You love life, you love Ruby, and you love Rails. You wrote an amazing app, and everyone thinks you're a genius. Then, the customer asks for one little change. You step away, open the code base, and make that change. It turns out you have to make another change, but it’s not that bad. You check all the code and deploy it to production, still feeling happy and smart. Then the customer changes their mind again—they didn't really want that; they want this other thing instead. So, you go off, grab the code, and open your text editor. Suddenly, it turns out you need to make about 50 changes, and all the tests are broken. It takes a long time to fix everything, but you do it. You're back in production, and everyone's happy until, of course, they ask for a new feature, and they need it tomorrow. Everyone’s hair is on fire.
00:02:27.040
You go in, but it’s impossible to do because the code wasn’t designed for that. You start hitting the code with a machete, making every possible hack and every bad decision. In the end, you're trying to figure out how you can get the code checked in without putting your name on it. One day, you wake up and realize that you hate this app, you hate Rails, you hate Ruby, and you hate your life. We all know this story. In the beginning, the app was a delight, and you felt great; you were getting so much done. We feel happy when we're doing our best work, but unfortunately, this feeling doesn't last. For many apps, it peaks around the time you deliver the first beta and then declines.
00:03:13.600
Things get worse: you were fast, now you're slow; you were happy, now you're sad; you were cost-effective, and now you’re a money pit. The app was amazing, and now it’s just a mess. This is not the time to look for a new job, because unless you do something different, you're going to end up hating every app you ever write. These applications are prisoners of their designs, holding you hostage. You can’t outrun the problem; you need to understand the mess. The problem with messes is that they are messy and hard to understand. Everything is tangled up together; they're like big tapestries woven together. There’s a warp and a weft running over and under each other, creating pictures in the fabric. Everything seems idyllic at first, but as soon as you start making changes, that illusion shatters. Your app isn't a pastoral scene with cute little lambs; it’s trying to kill you.
00:04:47.680
Things are so intertwined that you can't pull on a single thread without breaking many things. This coupling, this mess, is because of knowledge. Your app is made of objects, and objects know things—they always know things about themselves. Sometimes they know things about others, and it’s this knowledge of others that weaves objects together. When an object knows something outside itself, that's a dependency. We all know dependencies; they are things you know that, when they change, might force you to change in return. You can’t avoid dependencies—objects have to collaborate. They have to know things about each other, and these collaborations require that they communicate.
00:06:01.120
However, there are many different ways to solve any programming problem, and it’s easy to arrange your dependencies so that things turn out badly. Therefore, controlling dependencies requires understanding the stability of various pieces of knowledge in your app. Every bit of information an object knows can be ranked along a continuum from completely stable to completely unstable. It doesn’t matter how stable any individual thing is; it just needs to be more stable than the things that depend on it. Stability is relative. Things that may seem wildly unstable from some points of view can be dependable from others. Sometimes, you don’t know how stable things are—you’re uncertain, confused about how stable a bit of knowledge is—making it hard to depend on something.
00:07:02.000
This is where object-oriented design gives us hope. Object-oriented design is like the ultimate dispenser of programming optimism. It allows you to write code that your future self will appreciate, even in the face of confusion and uncertainty. It knows how to separate the stable from the unstable, allowing you to depend on the former and hide the latter. It explains confusing things—like a light shining in the dark corners of your app—and provides you with a way to organize the mess. By doing so, it enables you to write apps that are enjoyable to maintain. Design helps you stop worrying and learn to love the mess.
00:08:03.440
Now, I’m going to show you some code. We’re going to look at three examples. The first one is just a setup; it's a very simple example that we’ll talk about for a while. I demoed this talk to someone who was disappointed that the code didn’t change during example one, so don’t get your hopes up! We’ll just look at one piece of code and discuss it for a few minutes. I’m going to diagram the knowledge and give things names; this will take around ten minutes. In example two, I’m going to ruin it by introducing a change that will break the code. In example three, a miracle occurs—we’ll apply some object-oriented design principles to eliminate all the dependencies.
00:08:59.360
Example number one: imagine we have a game for racing bicycles, because all my examples are bicycle-related. Players can purchase parts, including a bike shock. If you've ever ridden a bike with a shock, you know that bikes without shocks tend to be stiff and unyielding. Mountain bikes typically include shocks, and in this game model, the customer has indicated that there’s only one shock, but others will probably be introduced in the future. Before purchasing a shock, a player wants to inquire about its cost. The code already exists; a player can send the message for shock cost to the game, which then creates a new instance of shock and sends a cost message.
00:10:04.320
Here’s what the cost method looks like: I didn't even bother implementing it here; assume it’s a hundred lines of complex math. This is a completely convoluted method, but it possesses certain qualities: it lacks dependencies, meaning no change inside this code can force a change anywhere else in your app. Furthermore, there are no dependencies here; nothing you can modify outside this method will necessitate any change to this code. The mess is entirely self-contained. It doesn’t matter how embarrassing the code is; if you're brave enough to leave this ugly code as it is, you can simply leave it there. Because of its special qualities, I'm naming this mess an omega mess.
00:10:56.560
So, that’s all well and good, but we already know the customer has mentioned that more shocks are coming. We have this large, unwieldy calculation, which we know will change. Now the question arises: should we leave it unchanged, should we modify it, or should we anticipate future changes? This method serves as a precarious solution that will inevitably lead to failure. You must decide the consequences of walking away from it now. Let’s hold on to that question for a moment; I want to analyze this code from another perspective. Let's discuss a sequence diagram, a visualization tool that represents the objects and messages passed between them.
00:11:51.200
The sequence diagram shows how objects are represented in boxes at the top and bottom. Each box can contain instances of classes or the classes themselves. The boxes connect with a vertical dashed line. When a message is sent, it creates a horizontal line with a filled-in triangle on the end. In the diagram, a player sends the shock cost message to the game while the game processes that message. As the game does this, a vertical gray bar indicates the processing, with the return message shown on a dashed line featuring a filled-in arrow.
Many of you may not have seen a diagram like this before, so your lives are about to improve dramatically! I use web sequence diagrams to illustrate all the diagrams you'll see in this talk.
00:12:49.520
This picture, this sequence diagram, represents the code you just saw. It accurately captures the code’s dependencies and the messages exchanged. I will annotate it using different shapes and colors: the solid line represents external knowledge that someone else possesses, and the dotted line indicates what I know about myself. The colors signify stability—unstable things are in red, really stable ones are in green, while those in between are neutral. The game knows the name of the shock class and the cost method. However, the shock class knows it implements the cost method and understands how it does so; it has knowledge but no dependencies, in contrast to the game, which depends on numerous aspects from shock.
00:13:47.920
Shock is just a shock; it doesn’t require any other objects. It is easy to reuse and test, though the game is tightly coupled with shock. If you tug at a thread in shock, it may cause the entire game to unravel. When testing the game, you often run code in shock. This sequence diagram offers a truer representation of the code’s relative importance, reducing the code's bulky hundred-line complexity to a small gray bar, effectively concealing it behind the message. Thus, this view of reality can be misleading.
00:14:54.480
It might seem as if larger amounts of code should correlate with greater importance, which may work ideally in a perfect world. However, in practice, this isn't always the case. The cost implementation is akin to that boisterous guy at a party who monopolizes the conversation while quieter introverts serve as an eager audience. At first glance, this dominating figure may seem crucial, but the interesting stories often belong to those in the background, just harder to hear. Don’t be misled; it’s not about the code itself. It’s about the running, living, breathing objects acting in memory—it's about the messages sent.
00:15:50.640
Now that you've seen a bit of code, let’s take a moment to step back. Let’s ponder the knowledge we've just discussed. We’ll plot every bit of knowledge in this space. The horizontal axis represents stability—things that are stable belong on the left and the unstable on the right. The vertical axis represents whether the knowledge is your responsibility or not. For every object, is this knowledge important to your purpose or perhaps unrelated to it? Each quadrant illustrates certain behaviors. The top left quadrant highlights stable things within your purpose that define your public API; stable things that reside outside of that need to be minimized.
00:17:35.240
Private behavior needs to be hidden, and only minimal stable dependencies should be considered. Likewise, unstable things outside your purpose should not be part of your code; they need to be removed. In the sequence diagram we have, shock knows certain things, and the game knows others. Let’s place these bits of knowledge in their respective quadrants. Shock knows it implements the cost method, which falls into the upper left corner, as it’s stable and part of its core responsibility. The calculation itself is wildly unstable but still within shock’s responsibility—it should be hidden.
00:19:55.560
Now, game knows the cost message, which, though it's stable, is part of shock's stable API. It is essential to get things done, signifying that shock implements cost, which is likely to remain stable. However, game has other knowledge about shock, like the name of the class and how to create the objects responsible for receiving the cost message. Those types of knowledge are indeed bound together, but the stability of their presence remains uncertain, meaning they're probably safe to depend on.
00:20:13.840
Now let’s think about something crucial: what are the consequences of doing nothing? If the future cost is the same as the present cost, waiting will always provide you with better information. There’s no need to worry about future requirements right now. When you know that costs are Omega messes with no dependencies, it gives you a powerful criterion for leaving them alone. I’d still advise against leaving it alone entirely; instead, I would inject that dependency because it’s free and saves time in the long run.
00:21:16.000
After very minor tweaking, I would refactor the code to create little methods with intention-revealing names. I find that if I can simplify the code now, it will pay off later, potentially saving me time when I return to it. This aligns directly with a strategy that has consistently proven profitable: performing refactoring that simplifies code is akin to entering a lottery rigged in my favor. It pays off so frequently that the winning strategy is performing refactoring whenever I can—this can save an hour rather than ten minutes later on.
00:23:12.840
So now that we’re done with Example One, let’s make things more interesting and break it. Imagine this: what happens when there's more than one shock, and they vary in cost? In case you're unaware, there are generally respective shocks: the front one on your front wheel, the rear shock that allows your seat post to move up and down, and a special type called a lefty which has only one arm instead of two. A diligent programmer has modified the game so that it takes the player’s chosen type and passes it to the shock.
00:24:04.640
Shock is now aware of that type, and the cost method has had a case statement added to switch on the type for calculating the proper cost. Let's analyze the sequence diagram for this new code, observing its differences. Previously, the cost method was an Omega mess; now it has dependencies due to the case statement. Initially, it only had to understand a single calculation but now understands four, in addition to four symbols representing shock types and the mappings between them.
00:24:58.560
The impact of this dependency explosion is significant: what will happen if a new shock type is added or if any calculation changes? The case statement has resulted in a surge of dependencies; now, instead of being self-contained as it once was, it is woven into various sections of the app, which means changes to any part of this could force you to change the cost method. The case statement has marred an otherwise perfect Omega mess—this is unfortunate but completely avoidable. You never have to accept these dependencies if you know how to organize this code using object-oriented design.
00:26:12.600
The guiding principle is simple: I would rather send a message than implement behavior. However, there was no one to whom I could send a message because shock is at the end of the line and already knew how to calculate a cost. The naive programmer asked where the code should be placed. While logically, adding it to the cost message seems correct, this view prompts the wrong questions. These dependencies, acquired by asking which objects should know things, can be eliminated by switching to the perspective of which message should be sent. Once you understand what message to send, an object can be created if necessary. Therefore, we move to Example Three.
00:27:30.800
In Example Three, we'll look at composition—just to illustrate the idea, I will demonstrate a simple refactor to address the dependencies we previously created with the case statement. For each type with its own calculation, I will create a new class that we can use for calculating costs. By shifting the entire calculation into a method called compute, we can re-establish an Omega mess: its cost method will have no dependents and no dependencies now, making it stable.
00:28:41.360
With the new class structure, shock is now composed of these cost classes. This new setup may feel unsatisfactory, as if we’ve only complicated the original situation. However, it’s imperative to understand what we've achieved: we’ve re-isolated the calculations that were previously intertwined with other logic. Now, every time a calculation needs to change, it can do so in a dedicated class instead of within the cost method directly. While these new class definitions can seem initially burdensome, they ultimately contribute to a more manageable and modular design.
00:29:51.879
Next, we see the pattern where a symbol indicating shock type needs to correspond with its respective class. So, if we decide to leverage the names directly, we can establish a convention that allows us to produce the appropriate class dynamically. By utilizing meta-programming techniques, such as leveraging constantize methods, we can systematically map symbol names to class names, maintaining our organized structure. Similar to a factory design pattern, this method of organization enables efficient class handling.
00:30:58.560
By incorporating this factory method, I can strip away the previous case statement, eliminating the need for shock to know various class names or type mappings. Shock only needs to understand the conventions established. The implementation of this code ensures that modifying or adding new shock types doesn’t break the overarching logic; it allows for sparkling separation of concerns. So, we execute this composition to smooth out the dependencies and elevate the functionality.
00:32:02.520
In summary, we have observed how we started with a class with a single cost method; then we muddled it with four different calculations. We repositioned those calculations within new classes, subsequently defining an interface and attaching them to each piece of logic. This characteristic of composition allows for the interface to remain constant regardless of underlying changes, facilitating ease of evolution in the design.
00:33:35.440
This interface is framed as a duct type—meaning that the cost method from shock does not need to know the class of the object it sends a message to; it only needs to trust that it fulfills the expected role. Adopting this perspective, where collaborators are viewed as roles instead of tightly coupled types, enables greater flexibility. Object-oriented design encourages you to remove those interdependencies, keeping your code self-contained and hidden behind stable interfaces. This framework of object-oriented design is a compilation of tools that allows you to make informed decisions on how to arrange your code.
00:35:38.320
The knowledge of those tools emerges from experience over time, teaching you how to arrange your code in a manner that is both comprehensible and efficient. You may not know everything initially, but with each project and each piece of code you write, you will steadily learn and enhance your skills. While you may have heard of the Law of Demeter, remember this principle: Good judgment comes from experience, and experience often comes from instances of poor judgment. Object-oriented design teaches that stability is relative and guides you to depend on aspects that are more stable than you. A well-crafted application facilitates future growth and adaptiveness.
00:38:06.920
As we stand on the shoulders of giants in this information age, we can access the best ideas available that can guide us. While no design principle is flawless, we can learn how to become masters of design, growing from novices to experts in our craft. Object-oriented programming encourages the removal of dependencies and reorganization behind stable interfaces. With commitment and continued learning, great applications that provide enjoyment are just around the corner.
00:39:35.120
Thank you! And if there are any questions, please feel free to ask.
00:39:56.080
[Audience member asks question about large applications] Thanks for your question! Balancing class organization and complexity can be challenging, especially once applications grow. Embracing good tests creates some insurance against the complexity that arises from adding many classes. The key lies in trusting a well-established interface to manage your application's structure efficiently without needing to comprehend every single dependency throughout.
00:41:10.960
[Second audience member asks a question regarding creation of separate classes for different types of shocks] That's a different approach, and it certainly could have been beneficial. When considering whether to compose or inherit classes, I take into account how similar, repetitive, and relevant the classes are. Some scenarios warrant the use of inheritance due to small variations of similar classes, while composition allows for the flexibility of differing behaviors.