Talks

Refactoring Live: Primitive Obsession

Refactoring Live: Primitive Obsession

by James Dabbs

In the video "Refactoring Live: Primitive Obsession" presented at RailsConf 2019, James Dabbs discusses the importance of refactoring code to avoid primitive obsession, which refers to the practice of using primitive data types to represent business objects. The session focuses on real coding practices aimed at simplifying and enhancing code structures in Ruby on Rails applications. Dabbs emphasizes that effective refactoring should not alter the observable behavior of the system but should aim to make the code more understandable and maintainable. Key points discussed include:

  • Definition of Refactoring: Refactoring involves changing the internal structure of software to make it easier to understand and modify without changing its external behavior.
  • Primitive Obsession: This is identified as the tendency to use primitive types like strings to represent complex domain concepts, thereby limiting the expressiveness and usability of the code.
  • Approach to Refactoring:
    • Begin by identifying code smells related to primitive obsession.
    • Use specific refactoring recipes, such as replacing primitives with objects and employing polymorphism to enhance code flexibility.
  • Example Implementation:
    • Dabbs demonstrates this by live coding a Rails application managing student records, where he refactors how grades are represented. Initially, grades were treated as strings, leading to incorrect logic when handling letter grades like C+ or B+.
    • He illustrates the development process, transitioning to a proper Grade class to encapsulate the logic relevant to grading, making comparisons easier and more accurate.
  • Iterative Refactoring: The live coding segment highlights a step-by-step approach to refactoring, ensuring that tests are run frequently to maintain code integrity and prevent regressions throughout the changes.
  • Final Takeaways:
    • The session concludes by encouraging developers to integrate refactoring into their daily practices to prevent technical debt accumulation.
    • Development should harmonize with business requirements—refactoring is essential anytime a new feature requires changes to existing code, and it should be a continuous process rather than an afterthought.
00:00:21.570 Real quick, getting started, I'd like to thank the sponsors for making this possible, as well as the organizers and staff for actually making this whole thing happen. A real quick round of applause.
00:00:40.440 Thanks for that! Hey everybody, I'm James, and I'm here today to do some coding with you. Specifically, we're going to be talking about refactoring. I'll say that most of what I know about refactoring came to me through these people who wrote this book. It is my sincere hope that most of the concepts we'll discuss today are not entirely new to you. However, if you have tried to use these concepts but haven't really integrated them into your day-to-day work, or if you've had trouble putting them into practice in a Rails application of scale, then this talk is for you.
00:01:10.800 How many people here have seen this talk or caught a whiff of it from RailsConf 16? A good number. Okay, great! If you're watching this on video at home or otherwise have control of time, feel free to pause me, go watch that, and come back in like a minute and a half for a quick recap for anyone who is stuck in the here-and-now.
00:01:43.380 Refactoring—what we're talking about today—can be defined as a change made to the internal structure of software to make it easier to understand and modify, without changing its observable behavior. A couple of important things here: it is purposeful; we are making a change to make something easier, and it does not alter any observable behavior. The big question is, how do you actually do it?
00:02:22.950 Here's the hardest part: if you have code that is in production and your customers are happy, if your servers are happy, and if your monitoring services are happy, do not touch that code! There is really nothing you can do to improve it; indeed, there is a lot you can do to make it worse. What you need to do is wait, and that's super hard. But wait until you have a new requirement.
00:02:49.590 Once you have a new requirement, ask yourself: is your code open for that new requirement? When I say 'open', I mean it very specifically. It doesn't just mean that you see how you can add this; it means can you add this new feature by only adding lines and not changing any existing definitions or methods in your code at all? If it is, that's fantastic! You're done—make the easy change and move on to the next thing.
00:03:15.690 If it isn't, but you see how to make it open, that's also fantastic—you’re done. The interesting part is what happens when you need to make a change, but your code resists that change in a way that makes it not obvious how to proceed. In that case, the refactoring books say that you should pick the code smell closest to your new requirement. This again doesn't mean something vague; a code smell is a very specific thing. We'll talk about one in a second. But once you locate the code smell, you apply the curative recipe for that code smell. Each code smell has a handful of curative recipes—some process that you can apply to fix it.
00:04:15.530 Today, we're going to be talking about a particular code smell called primitive obsession. Primitive obsession is the tendency to use primitive types to represent your business objects. It's a very easy pitfall to fall into in Rails. For instance, you might write a user model that has a phone number represented as a string. This is reasonable in your database; a phone number is a string. However, the temptation is to then think of a phone number as being the same as a string, and when you do that, you end up writing code that doesn't properly express the concept of what a phone number represents in your problem domain.
00:05:07.210 The question then is: when you discover that you have this problem, what do you do? Fortunately, there are a handful of recipes to follow. We're going to look at two in particular: 'replace primitive with object', which has some steps outlined in Fowler's refactoring book, and 'replace conditional with polymorphism', a more complex approach. The key takeaway isn’t to memorize these; instead familiarize yourself with them, and go look them up when you need them.
00:05:40.070 We're going to dig into these concepts within a real, actual Rails application. So, we are going to be doing some live coding. Everybody ready? Here we go! I've created a little Rails app for managing college records, grades, enrollments, and other related concepts.
00:06:02.919 A couple of key domain concepts in this app: a section is essentially the core object where we define what a section represents, such as Math 101 running in a specific term, like Fall 2019, in a particular room, taught by an instructor. Students are enrolled in the class, and the enrollment record holds the students' grades in that class. One of the core responsibilities of a section is to decide whether or not it should admit a particular student.
00:06:30.110 The section checks various conditions like prerequisites and grades, ensuring that the student meets the minimum requirements to be admitted. If you can't see everything here, on the right I've got Guard set up with RSpec. As I change files, saving the section file on the right will run the unit tests for the section model, and then, if those pass, it will run the full spec suite.
00:07:05.640 I'm going to make a lot of changes, but I’ll mostly ignore the test feedback unless it turns red. So if it turns red, please yell at me loudly! Otherwise, just look away.
00:07:38.580 One of the other things this app does is manage transcripts. A transcript is largely responsible for computing a student's GPA by pulling their enrollment grades and looking up the appropriate scores. However, our system currently only understands whole letter grades: A, B, C, and D. This works well until we decide that we would like our app to support plus and minus grades. When we do this work, we'll define some acceptance criteria and set up corresponding specs.
00:08:12.270 One spec asserts that if I have a student with all B+ grades, I can compute their GPA appropriately. This spec is currently failing; I won't dive into the reasons just yet, as there's another spec that's more interesting. This one indicates that if a section requires a prerequisite to be passed with a C, and a student has passed that prerequisite with a C+, they should be admitted into the course.
00:09:11.790 This test also fails for somewhat uninteresting reasons that I'll address shortly. The important thing to note is that I've set up some validations leading to this failure. So let's dig into the section implementation.
00:09:55.800 The issue arises when checking the requirements. My minimum grade is a C, which you'd expect, and the student has a C+. While that means they should be admitted, the underlying logic compares grades as strings, leading to a situation where C+ does not come before C. This misunderstanding of how to treat grades is the core of my primitive obsession.
00:10:45.780 Before fixing this immediately, I'm going to comment out the failing spec so I can focus on refactoring towards making my code open for this requirement.
00:11:05.890 Now that I understand I have a primitive obsession, I turn towards the recipes. I'm going to apply the 'extract object' recipe for this value.
00:11:18.500 The goal is to create a new class that represents a grade, which I want to populate with a name and ensure that I didn't make any syntax errors. With this implemented, I can update the previous lines to use this new grade class without altering any existing behavior. This is a small, incremental change that should work as it did before.
00:11:54.280 Now, I want to take it a step further and extract comparison behavior into the grade class itself. The code I want to achieve should make it clear when comparing grades. The good news is that I own the grade class, so I can control its behavior.
00:12:31.800 As I focus on the code change, I can carry out these transitions one step at a time. This incremental change reinforces that I am now comparing grades as grade objects, rather than just strings.
00:12:53.020 However, I discover that I am still treating grades as strings throughout the application, even though I've started implementing objects for them. It's essential that my entire system consistently uses grade objects instead of primitive strings. Therefore, I begin examining how I can adjust calls to ensure that grades are points of robust interaction.
00:13:32.770 I would like the call to 'user.grades' to return grade objects, but I need to insulate myself from that change to avoid breaking the existing functionality. To do this, I'll modify the structure of 'grades' in the user model.
00:14:02.610 We'll convert the values to grade objects and create a new method to handle returning them. Once this is implemented, I will be able to simplify other parts of the codebase.
00:14:14.700 This adjustment allows for more consistent object usage across the board. However, I still have references to strings, and I do not want that variable to interact with other systems as compared to grade objects.
00:14:32.370 With that in place, every time I need to deal with grades, I can just write the logic to transform them. If anything upstream changes to send down grades instead of a primitive string, everything will continue to work as it did before.
00:14:53.460 This transformation gives me confidence that whenever I reference grades, they're treated as objects. At this point, I can also convert the score logic for grades into the grade object itself, further reinforcing this object-oriented approach.
00:15:25.410 However, I notice that grades do have a score method now. While I don't love the idea of allowing invalid scores to be created, it's not strictly required yet.
00:15:45.330 This line is now safe, and I will continue to refactor to remove any old logic referencing strings where grades are concerned, ensuring they utilize the grade objects instead.
00:16:20.630 After this, we realize that grades aren't represented the same way throughout the application. Enrollments are treating grades differently; I want to unify grades within the application and fix any bugs that arise from disparate representations.
00:17:02.450 The challenge is that Enrollment provides grades as strings, instead of objects, leading to inconsistencies. My goal now is to ensure that the way grades are represented in records harmonizes with what I have set up in my business rules.
00:17:24.690 To implement this, I'll create a factory method for grades within the Enrollment model. I'll focus on ensuring that the serializations for grades understand how to deal with grade objects appropriately.
00:17:44.770 With that infrastructure in place, I can implement a conversion method that transforms the grades appropriately, allowing for seamless integration while maintaining the ability to handle serialized data.
00:18:00.940 At this point, we've worked through the addition of those necessary structures to facilitate grading, and we are on the verge of being able to set up many of the business rules that interact with grades.
00:18:27.110 Next, we should ensure our system reflects this change well by checking the specifications tied to grades. This means reflecting on what a grade means and recognizing that many areas of our application need to be aware of how to effectively update grades.
00:19:00.580 Now we see another opportunity to introduce polymorphism. By refactoring the role definitions in the system to better encapsulate the behaviors required for certain roles, we can achieve cleaner, more maintainable code.
00:19:24.450 Adopting new roles within the system—such as an admin or a teacher—allows understanding different paths to exist for updating and creating grades across users involved.
00:19:51.830 In conclusion, we've created a framework that allows for upgrading user roles and their corresponding abilities while maintaining a well-structured and clean approach.
00:20:14.710 Let's reflect on these takeaways: it's essential to continue your journey of incorporating refactoring into daily practice rather than viewing it as a separate stage of development. It's all about consistency and building flexible systems with robust object orientation.
00:20:38.680 Through this method, we can create applications that are open and adaptable to change, allowing us to confidently evolve our work without fear of breaking existing functionality.
00:21:04.920 If you have expressed concerns about technical debt hindering your progress, remember that refactoring is an opportunity for improvement that can enhance your workflow and product efficiency.
00:21:50.220 After all, refactoring should never be seen as a burden but as an integral part of a fruitful development cycle. Take the time to invest in your code quality, and it will pay dividends later on.
00:22:29.310 Thank you for joining me on this journey into refactoring and the concept of primitive obsession. Let's keep pushing forward and strive for excellence in the code we create.