00:00:17.000
So you'd think that writing object-oriented code would be hard. All you have to do is look at our apps. Meanwhile, we write code that we almost always inevitably come to hate. The more I think about this, the more I realize that my job is to think about how to write better code. The problems we cause seem to have the same simple solution. When people ask me how to write object-oriented code, I tell them one small piece of advice: make smaller things. That's all there is to it. Make smaller classes, make smaller methods, and let them know as little about each other as possible.
00:00:39.399
Lately, I’ve been on a quest about conditionals. There’s a lot of code out there with nasty conditionals, and I’ve been wondering when I should replace conditionals with small objects, how I should do this, and what will happen to my code if I do. I was at RubyConf in Miami in November, and I shared this obsession with Jim Wuck, whom some of you probably know. He pointed me in the direction of the Gilded Rose, which is a well-known kata. I wanted to approach this problem as if it were a real production issue, so I checked it out of his repository and looked at it with great interest.
00:01:15.720
I have altered the code a little to make it easier to discuss, but this really is the Gilded Rose kata. Here’s how it works: there’s a Gilded Rose class structured with attributes for name, quality, and days remaining. These are set in an initializer, and then there’s a tick method. Well, actually no, that’s just the first half of it.
00:01:29.360
The rest is where it gets complicated. I know you can’t read this, so don’t try. Just understand that it’s a 43-line if statement, which seems really difficult to me. However, I have to consider that my subjective sense of how hard this is to understand may not be accurate. Instead, I used a complexity metric called Flog on it. A metric is a crowdsourced idea about something; it counts assignments, branches, and conditionals. Flog scored the Gilded Rose class a 50, and the tick method scored a 45. That’s complicated, right? But before we proceed, I want to introduce another subjective metric about complexity.
00:02:43.760
I often go to places and look at code I know nothing about. When I arrive, no one calls me if things are going well. They ask me to look at the most heinous bits of their applications, the code that has become complex and unmanageable. The explanations are long and confusing; they come complete with histories of the mess and how it became that way. There’s this moment in every explanation when I start feeling like that cartoon dog Ginger, where everything turns into a blah-blah conversation. Then, I snap back to awareness when someone asks, 'So what do you think we should do about this line of code?' This used to terrify me because I thought I had to understand everything to help, but I’ve learned there’s a simple thing I can do to identify code that may benefit from change.
00:03:19.760
I call it the squint test. Here's how it works: squint your eyes, lean back, and look at the code. I am looking for changes in shape and color. Changes in shape indicate nested conditionals, which are always hard to reason about. Changes in color imply that the code is at differing levels of abstraction, making it hard to follow the story it tells. In this code, there are 16 if statements, seven of which are not equal, two connect something with an 'and', and there are multiple magic strings and numbers that complicate understanding. Fortunately, at least it has tests.
00:04:05.600
The tests cluster around magic strings, and there are also six skipped tests. I suspect there’s something in an else branch that matters, so I pry open a test. The format seems pretty standard: given a Gilded Rose with certain attributes, when I tick, the quality and days remaining both decrease by one. It’s similar to selling items that expire at some date, like milk or eggs.
00:04:44.320
As I continue exploring, I come across six skipped tests, all related to something called 'conjured'. They follow the same pattern: given that when I tick, I see this change. It suddenly dawns on me that I am supposed to change this code now.
00:05:02.600
I tried to make changes obediently, but I was a miserable failure; I couldn’t do it. That 43-line if statement defeated me. Every time I’d pry open a test related to conjured, make a change to the if statement to pass the test, I would break something else. I struggled for hours; it was hard for me, and I believe it would be hard for you too. If changing that if statement was so hard, you have to ask: why did I try? What motivated me to attempt altering that incredibly complicated piece of code?
00:05:43.840
The answer lies in our natural programming behavior. When you write code and someone asks for a change, what do we do? We look around at the codebase for something that resembles the new thing we're trying to do and that's where we place our new code. Novices, in particular, are afraid to create new objects, so they add more code into existing pieces. If the existing code has an if statement, they simply add another branch. What often happens is that the natural tendency of code is to grow bigger and bigger until it gets to a tipping point. Once it reaches that point, the code is so large that you cannot envision placing code anywhere else.
00:07:06.080
If the existing pattern is a good one, the code improves, but if it's a bad one, we exacerbate the issue with oversized classes that no one adds a 10-line helper class to. Instead, they simply become larger. I could not follow the existing pattern. Instead, I decided to create a new pattern and refactor this code.
00:07:50.160
This is real refactoring, meaning changing the arrangement of code without altering its behavior. I will not try to add conjured just yet; instead, I'll focus on moving the code around to improve its structure. In refactorings like this, having your tests is invaluable. I will start with the normal tests.
00:09:00.680
The tick method is long and procedural, which goes against the principles of object-oriented design that focus on using many small, interactive objects that send messages to each other. This message passing creates seams that allow you to substitute different objects easily. As it stands, this code lacks those qualities. The first thing I need to do is create a seam by trapping normal and exiting at that point, so that tests fail as expected.
00:09:41.960
Four tests should fail, and they do. I’m not going to add more code to the tick method; instead, I will send a message to myself. Now that I believe I've caught that execution path, I’ll break open the first test and write code to make it pass. Here, quality goes down by one, which is easy to implement. Days remaining goes down by one, and that test passes.
00:09:56.160
Now, I think two tests should pass. Next, I focus on the case where the sell-by date has passed, which means quality goes down by two. I’ll ensure my previous tests continue to pass as I implement the necessary code for this test and find that two tests should succeed, but there’s additional unexamined failure in another test.
00:10:38.160
Despite not needing to understand this failure yet, I’ll move on to the next testing scenario. This test indicates that if quality is already at zero, then nothing should change. So I'll wrap my existing code in an if statement to bypass any alteration if quality is already zero.
00:11:33.440
Now I’m back to green. This code isn’t particularly sophisticated or clever, but getting to green is essential because it allows me to refactor the code while retaining its functionality. It appears that quality always decreases by one, so this observation leads me to simplify my code.
00:12:14.560
I can deduct quality and disregard the outer conditions if quality is already zero. Examining the remaining cases shows that I deduct one from quality and the one special case that occurs when exceeding the sell-by date. I can eliminate the overly complicated code and arrive at a more refined level of abstraction that makes the code simple to understand. I appreciate the story this code tells, which is now clear and concise.
00:12:55.360
We’re going to repeat this process much more rapidly than I did before. (Quickly rehashing:) I created a seam, sent myself messages, trapped all execution paths, wrote some necessary code, and got to green as fast as possible, allowing me to refactor to achieve a more understandable solution.
00:13:45.560
Now that normal conditions are handled, I can move through the remaining cases systematically. For 'Bree', there are multiple tests to account for, which I'm confident I can manage by turning it into a case statement. I already set up a basis for that; now it’s just a matter of writing the code in a straightforward manner, and after completing that, they should all be passing.
00:15:12.760
Interestingly, when handling all these different conditions, the similarities between normal and Bree become clear. However, I resist the urge to abstract these similarities at this moment. While I understand the DRY principle (Don't Repeat Yourself), I know it’s better to keep the duplication now, because I expect to uncover more information about this algorithm as I continue refactoring.
00:16:18.440
This leads me to an important point: it is often cheaper to keep duplication than to meddle with the wrong abstraction. We teach novices to avoid duplication, as they often struggle with other concepts, but as you advance, you can handle a bit of duplication while waiting for better abstractions to arise.
00:17:34.560
Next case involves sulfur, which has three tests. You would think placing an empty shim method in here would yield three test failures, yet strangely, they all pass. I realized examining the tests that all they faced was assertions that nothing changes if it’s sulfurous.
00:18:28.720
When it came to backstage, several issues needed to be addressed, and I placed it all under the case handling structure. This allowed me to trigger the tests surrounding the items, but more importantly, to understand and refactor to where now I’m back to green without previously messy state of affairs.
00:19:33.440
Meanwhile, I've stripped the ugly old case statement’s functionality away from the Gilded Rose class. The remaining responsibility of Gilded Rose is to determine which item class applies to which strings.
00:20:16.760
Now onto inheritance. Each subclass shares this commonality. The goal here is to maintain an inheritance hierarchy that’s shallow, where the subclasses operate as nodes at the edges of your object graph and utilize the shared behaviors in the superclass.
00:21:41.920
The public API for item comprises its quality and days remaining attributes, while those of the subclasses have the tick method. It’s crucial for the superclass to implement tick. The overriding method could just call nothing, which at least cleans up the hierarchy, yet it bothers me from an architectural perspective.
00:22:20.880
Sulfurs implementation currently overrides the superclass method, effectively rendering it unnecessary. I’ve subsequently decided to eliminate the sulfur class due to its ineffectiveness.
00:23:03.360
Furthermore, I need to metamorphose case statements, often used in business logic, into structures classified as configuration values distinct from their connecting algorithm. I’ll construct a hash and facilitate the algorithm’s use of that hash.
00:24:05.760
We have successfully refactored to a point where we manage small objects in place of a gigantic Gilded Rose class. The ultimate structure has several item subclasses and configuration data, leading to simplified and clearer code as illustrated through squint testing.
00:25:06.280
Final examination of fog scores reveals substantial improvement as computed complexity dropped from a previous chaotic state.
00:26:19.760
I have arrived back at my original objective, which was to implement conjured. The focus today is about methodological approaches leading towards solutions without losing sight of fundamental principles.
00:27:00.160
In conclusion, I emphasize that while repetition is typically discouraged, between duplication and improper abstraction choices, duplication may be the lesser evil. It's crucial to structure code to accommodate future changes instead of rigidly trying to force an open/closed principle prematurely.
00:28:21.600
As Kent Beck succinctly articulates, our goal is to make necessary changes simpler before churning efforts into completing those changes. This session has leaned into small object creation, reinforcing their single responsibility, letting object-oriented design principles guide significant outcomes.
00:29:56.160
Metrics play a role but should not limit your view; understanding principles will guide you toward improved code. The journey through complexity often reveals simpler structures that facilitate adaptability and clarity across the object-oriented design spectrum.
00:30:56.160
I encourage you to put these principles into practice. Thank you all for attending, and a special thanks to Jim War for facilitating this talk.