The video above was recorded as the Day 2 keynote at Ruby Kaigi on September 9th, 2016.
Background
I’ve been occasionally criticized since my Make Ruby Great Again talk that it’s been a while since I’d contributed much to the Ruby community. I was thinking about this when I was asked to design a talk for Ruby Kaigi (Japan’s national Ruby conference) that was about something other than testing or feelings. (This forced me to consider whether I knew anything other than testing and feelings.)
I sat on the idea for a while before surmising that the greatest risk facing Ruby teams’ continued use of Ruby was not only whether Ruby was fast enough or concurrent enough, but whether their existing applications were maintainable enough. The most common reason I’ve seen teams leave Ruby for has been the lack of tools and education needed to work effectively with large, complex applications. (New languages are appealing, but new languages with which you don’t yet associate with tremendous pain are even more appealing!)
Regardless of language, when saddled with a hard-to-change codebase, the grass of a total rewrite will seem greener. But Ruby’s dynamic nature gives us fewer tools than some other language ecosystems for successfully managing the invetible accretion of incidental complexity that every long-lived system faces. Is there anything we can do to make legacy Ruby more maintainable?
That question led me to this talk. In it, I introduces a new gem that we designed to help wrangle legacy refactors. It’s called Suture, and along with providing some interesting functionality to make refactoring less mysterious and scary, it also prescribes a clear, careful, and repeatable workflow for increasing our confidence when changing legacy code.
We hope you’ll watch the talk and try out Suture in your projects! Please give us a shout or open an issue if you have any questions.
Slides
This talk’s slides are available on Speakerdeck.
Transcript of the talk
[00:00:00] This talk is called surgical refactors. Could we bring the lights down so we could see the screen better? All right, cool. Good morning. Let's start. My name is Searls. That's me on Twitter at Searls. That's what I look like on Twitter, if you maybe have seen my face before. My email address, if you want to send me a message, is justin at testdouble.com.
[00:00:23] My last name at Katakana Eyes usually is Saruzu, if you want to call me that. But the Hatsun ga muzukashii no de Nihongo de Juusu to moshimasu. Hai, Ringo Juusu no Juusu. So I come from a company called Test Double we're software consultants. If your team is looking for additional developers that are really good at Ruby and JavaScript and testing and refactoring, we would love to work with you and if you're okay with people who mostly speak English, you can send us an email at hello at testdouble.
[00:00:57] com. So first a little bit about me this is me eating some Yabaton miso last week. I talk very fast when I'm nervous. And I am always nervous. Please forgive me we have plenty of time today if I'm speaking too fast, it's okay to shout. Us. I, speaking of me being nervous, I was very nervous about screen size up with the projector here this morning. I didn't know if it was 16x9 or 4x3 but I think we landed okay.
[00:01:28] But it actually gave me an idea cause Matz yesterday was talking about Ruby 3x3 and how difficult it will be to solve. So I think we just We can solve it here today really easily. There it is. That was a lot easier than he made it sound. Speaking of Ruby, it's a massively successful language, right?
[00:01:44] And in the early days, success looks like a lot of happy people. They're building stuff for fun. People give us a lot of positive attentions. I remember when I started learning Ruby, Everyone I knew who wrote Ruby was really cool. And in the early goings of any language, the marker for success is if you're able to make it easy to make new things.
[00:02:08] And one of the best things about Ruby is building something new is very easy. It does that very well. But later, success is very different because people are more critical now that it's popular, it's an incumbent. People are more critical in how they analyze the language. They're more serious because people use it for work.
[00:02:29] And as time goes on, more and more money is involved in the existence of the programming language. And so the mood is very different with an older language. And I think the key to later success for a programming language is making it easy to maintain old things. And nobody likes maintaining old things.
[00:02:47] But my question today is, can we make it easier to maintain old things? Old Ruby code. And so I sat and I thought about this for a while. So today as an exercise, we're going to refactor some legacy code. The word refactor stands out. You might define the word refactor if you're not familiar. It's to change the design of code without changing its observable behavior.
[00:03:12] You change the implementation, but it still behaves the same way. How I think of refactoring is, it's to change in advance of a new feature or a bug fix. In order to make that feature or that bug fix easier later. Legacy code is the other word in this, the phrase in this sentence. And legacy code has many definitions.
[00:03:33] Here are a few legacy code definitions. First, old code. Or, some people say legacy code is code without tests. Usually we say legacy code just to mean a pejorative. It's a code we don't like. But today I'm going to use a different definition of legacy code. I'm going to say legacy code is code that we don't understand well enough to change confidently.
[00:04:00] So today let's refactor some legacy code. Now whenever anyone says refactor and legacy code in the same sentence, I feel like refactoring is really hard, because you have to take something that exists and then make the design better. And that just takes a certain amount of creativity that I don't always have.
[00:04:18] But refactoring legacy code is very hard, because it tends to be very complicated. And as a result, it's easy to accidentally break existing functionality for users. And so it feels dangerous. As a result, legacy refactors on teams often feel unsafe, and they make us nervous. Additionally, legacy refactors are hard to sell to our managers and to business people.
[00:04:44] If we were to chart business priority of our activities as developers against the cost and risk of what we're doing, in the top right corner in that quadrant, new feature development, obviously very high priority, but also expensive. In the top left, you'd put bug fixes, high priority, but relatively less expensive.
[00:05:04] In the bottom left, low priority from the perspective of the business. But still, relatively low cost is testing. And what's down here in the bottom right, I think refactoring goes here. So new features, we don't have to sell the business on new features. If they're paying you a salary, they already decided they want to invest in new features.
[00:05:24] Bug fixes are normally easy to sell because the cost of benefit's good. Sometimes we succeed at selling testing, we usually do nowadays. But we don't always because it's not always seen as very important. But selling refactoring is very hard. Typically in my experience, in general, refactoring is just difficult.
[00:05:43] It's difficult to estimate how long it's going to take because you don't know how much work there's going to be or how risky it is. From the business's perspective, it's invisible, right? If to refactor code is to change its implementation and not its observable behavior, they don't know what value they're getting out of the refactor.
[00:06:00] And typically, because we're changing something that's very complex, we have to put a stop on all other work in that area of the code because it would be difficult to merge in multiple changes. So we're stopping everything now. As complexity of that legacy code increases, it probably means that it was more important, right?
[00:06:20] The business needs something about that code to have 500 if statements and all sorts of complexity. So it's probably a very important piece of code. And so therefore changing it is less certain and in general more costly. So today, as part of my Make Ruby Great Again series of talks, I want to make refactors great again.
[00:06:42] Of course, I thought about that line for two seconds before I realized refactors have never been great. So I want to just make refactors great for the first time is my goal today. So in looking at this quadrant and looking at refactoring, there's really two ways we could make refactoring easier.
[00:06:57] On this axis, on business priority, we could try to sell refactoring to businesses so that they view it in a higher priority. Now when we sell refactoring to business people, the image in their mind is typically like road construction. We're going to stop everything, nothing, no traffic is going to get through, but money is going to continue to fly out the window at the same velocity that it normally does.
[00:07:21] And that's not a very attractive image to our managers. So we have a few tricks that we use to sell refactoring. The first one is we try to scare them into it. We say, Hey, if you don't let me refactor this right now, someday we'll need to rewrite everything. But that's far in the future, that's difficult to prove.
[00:07:40] We might say your maintenance costs in the future will be much higher, but that's difficult to quantify. It doesn't feel real. Secondarily, we might try to absorb the cost through discipline and professionalism. Maybe these are our new future activities normally. We plan, we develop, we write tests. Maybe we just grow the pie, and spend a little bit more time on each new feature by baking in some time for refactoring.
[00:08:06] And this is fantastic, but it requires immense amounts of discipline that most people don't have. And, as soon as there's any time pressure, refactoring is going to be the first practice that we drop. And most teams experience a lot of time pressure, so that's not very effective either. Another strategy teams use is to take hostages.
[00:08:25] The business sets the backlog priority saying I want feature one, two, three, four, but the team says no. We're going to do some refactoring after feature one. And then before you get feature three, we're going to do some more refactoring. And this is problematic because it's adversarial. Basically we're blaming the business for rushing us and telling them that, no, we need to go slower.
[00:08:47] We need to do our stuff now. And it erodes the business's trust in the team, because they're paying us a lot of money to write code. And if the message that we're sending them is that we're so bad at it that we have to stop and fix it every now and then, they're going to think that we're less competent at our jobs.
[00:09:05] So yeah, refactoring is hard to sell. We're all programmers. We all believe in it. We're probably not going to successfully change the culture today. That's probably not where the solution is in the short term. If we look at the other axis, why is refactoring so expensive? Whenever I refactor code, I feel a lot of pressure.
[00:09:25] There's a lot that I have to keep in my head at once. I have to get a lot done but in a short amount of time because other people are waiting on me to maybe merge in my changes because it's low priority so it doesn't get afforded very much time to work on it. And my tools, in general, we don't have a lot of tools that help us refactor code.
[00:09:45] Most open source tooling is about creating new stuff, because that's more exciting, and that's what we want to think programming is. But most of us are getting paid to maintain old code. So you'd think that we'd have better refactoring tools, and we just don't. So for me, refactoring is really scary, and I'm on a mission to find everything that's scary about software, and try to find a way to make it less scary, because I'm so anxious and scared all the time.
[00:10:12] In fact if you're like me and you're scared all the time, you should buy my book that I'm working on. It's called the frightened programmer. It's not a real book because I'm too afraid to write a book. So what if, what can we do to make refactoring less expensive? We do a few things already. We have refactoring patterns like this book by a Martin Fowler, Jay Fields.
[00:10:36] And they're explicit operations, like extract method, or pull up, push down, or split loop. They have names because if you follow the procedure carefully enough, it's safe to undergo certain refactoring operations. And it's safer still if you have good tools. Easily my favorite thing about Java programming language is that with Java, it's so not expressive that you're able to get all these automated refactoring tools in your IDE with a relative guarantee that you're not going to break anything, but they're also not very expressive of operations either.
[00:11:12] You couldn't possibly take a complex design and then make it into a good design if you only follow that advice. If you only follow those operations. Second, a technique called characterization testing was made popular in the book, Working Effectively with Legacy Code by Michael Feathers, and that's still, I think, the best advice a lot of people have about legacy rescue.
[00:11:34] Basically, you treat the code as a black box, and then you put a test harness around it, and you send it some input, and you get back an output. You send an input, you get back an output. You send all the inputs that you can imagine into the black box and you just record the output, no matter what it is, without understanding, without judgment.
[00:11:52] There's no wrong answers. The goal is simply to create a harness by which you're pretty sure that if you change stuff, as long as the test passes, the change was safe. Once you zoom in and you have that test harness, then you can begin to aggressively refactor the code into new units to new objects.
[00:12:10] And then when you're done with that, you can backfill new unit tests that understand what the code is doing and have clear intention behind them. And that's a lot of testing, right? We have to write all this characterization tests. That takes a lot of time. We have to write new units and then we have to write tests for those.
[00:12:25] That's a lot of work. And the next step is actually to delete a test. After we're done with the refactor, we're supposed to delete our characterization tests. But if you're having a lot of legacy code in your code base, you probably want all the code coverage you can get. And if you just spent a lot of time on a test, the last thing that you're going to want to do is delete it.
[00:12:46] And it's tempting to quit halfway through because it's a time consuming, exhausting process, and that's not good. More recently, a technique has been popular with Legacy Rescue that resembles A B testing. Basically, if you have the old code over here and you write a new implementation over here, then you just put a router in front of it.
[00:13:07] So maybe you send 20 percent of the traffic to the new code and 80 percent of the old code. I think her name's Jesse Toth. I think she's here today. Jesse, are you here? Yeah, there's Jesse. Jesse is working on this awesome gem from GitHub called scientist. And what it requires, it does, it facilitates this for you, but what it requires is You're on your own for how you rewrite that new code path, and a lot of like sophisticated monitoring and logging and data collection that scientists produces is necessary to figure out whether or not the changes are safe.
[00:13:42] And finally, it's only really appropriate for business domains where it's safe for transactions to fail. It might work for GitHub, but it may not work for a bank that's handling transactions. So if you think of it as a spectrum on one end with characterization testing on one end and scientists on the other end, You can look at Michael Feather's book and say that's good for development, it's painful for testing, and it has no solution for staging or production environment.
[00:14:10] If you look at Scientists on the other side, it doesn't have a lot to say about development or local testing. It's really obviously beneficial for a staging environment, and if anything in production, the amount of data that it produces is overwhelming. It's really cool. But what if we had a tool that was actually good at all four stages of the life of a refactor from planning to completion?
[00:14:36] And what would that look like? And so that was the question that I posed to myself when I submitted to speak at this conference. And then months passed by and I still had no good answer. And eventually I said, oh no, I have to give a talk on this. And, being frightened like usual I thought about it and thought about it and I had an idea and I decided that Instead of writing a lot of slides today, explaining how to refactor well, I'll just write a new gem because even though, I speak English and my English may be hard to understand, our common language is Ruby.
[00:15:07] So let's talk about Ruby for the rest of the talk. I used TDD. So this is a TDD talk based on that explanation. TDD stands for talk driven development. I like this practice. I'm going to try to rely on it more. The tool that I wrote is a new gem called suture. Sutures are the stitches that you make when you're closing closing a wound after a surgery.
[00:15:32] So I think it's a good image for surgical refactoring. It's up on our GitHub at testdouble up here. The page looks like this. You can install the gem just like any other gem. You know how to install gems, I'm sure. And the metaphor here is to treat refactors like surgeries. Now, surgeries, they all serve a common purpose.
[00:15:54] To help us get well. We want to take a scary thing and make it feel safe. They require careful upfront planning. They require flexible tools because you can imagine this refactor is going to be loaded with like roughly the same information and that same information can be used to make development tests, staging and production all easier.
[00:16:16] And we want to follow a consistent process because there are already more than enough variables in all of our legacy code, because it can look all sorts of different ways. So if we make the process consistent, then we can feel a little better and we take multiple observations. Initially, we need to be very up close to our refactor, but after we've initially developed it, we can step further back and eventually just assess based on logging the health of the refactor before we've decided that it's complete.
[00:16:44] So we're going to talk about nine features. Or workflow steps that are built into Suture. First, how to plan. Second, how to identify the seam that we're going to cut. Third, how to record the interactions of the old code path. And then how to automatically in a test environment, validate that we're able to reproduce all of those recordings.
[00:17:06] Finally, we get to refactor the code or re implement it. Then we verify that the new refactor behaves the same way against the original recordings. In a staging environment, we can compare to make sure that both the new and old code paths operate the same way, even if they're getting input that's different from what we've gotten before.
[00:17:26] And in production, we can use it as the same information as a fallback mechanism to rescue any errors in the new code that we didn't otherwise anticipate. Finally, the last step of suture is to delete suture. Just like you have stitches removed, suture shouldn't be in your gem file forever, only when you're doing a legacy rescue.
[00:17:46] Alright, so a little about planning. Today, in this exercise, we're going to do two bug fixes. The first bug fix is we have a calculator service and this calculator service has a, an add route, but it doesn't add negative numbers correctly. It's a pure function, so if you look at this controller method, we create a calculator, we call add with two operands, and we set it to result, just like a Rails controller method.
[00:18:12] If you look at the implementation, You can see we've got the declaration of the function, and then for whatever number of times the right operand is 8 times, it'll loop over 8 times, and 8 times add 1 to the left, and then return the left. Now, that's where the bug is, obviously, because it's only positive.
[00:18:32] And yeah, this legacy code is really ugly. But I'm sure that your legacy code is really ugly too. So that's the idea. So we zoom back out. We're going to create our seam here. This is going to be the point at which we want to branch between the new code and the old code, because it's the call site. It's the most common sense place for us to cut it.
[00:18:53] The next one from a planning perspective, the next bug is. We have a tally service, where we can invoke the calculator multiple times and then ask us what the total sum of all the numbers that we called it was. And this is a different type of function because it's going to require mutation of an object over time.
[00:19:11] So here's what that method looks like. We again create a calculator, and then over an array of numbers, for each of them we call a tally function. Finally, we take the total from the calculator and set it to the result. If we zoom in here, this is an even more ridiculous looking function. We instantiate a total instance variable, and then we count down from whatever was passed to zero, and if it happens to equal exactly half of what's set, then we add double it.
[00:19:40] And this is really ugly, but it was a way to introduce the bug, right? So this will only work for even numbers, it'll have no effect if you pass it an odd number. And we want to fix that's our story today, is to fix that bug. We zoom back out. Obviously, that's our call site, just like before, but it's bigger than that, because we also depend on the value of that total instance variable.
[00:20:02] And so this seam is going to be more complex. It's going to require a little bit more work. Next, let's talk about how to cut those seams. So in the pure function case, What we do is we use Suture's API. So zooming out a little bit, instead of calling calc. add directly, we're going to create a suture. We're gonna say suture create a scene called add, and then we're gonna pass it the same arguments as an args option, as an array, and we're gonna pass it a reference to the callable add method.
[00:20:34] Now this can be any callable. It could be a Lambda, a p proc, it could be a reference to a method like this. It doesn't matter as long as it can be called with call. And at first what I've just done here is a no out. I'm going to write exactly this much and then go to the page and make sure that I did this correctly, but it's not going to take any further action.
[00:20:52] It's just going to call through like it normally. In the mutation case, this is going to be more complicated. So let's zoom out again, give ourselves some space to work. And I'm going to change that tally call to another suture, creating tally. And the args here, I'm going to say, are the calculator and the number.
[00:21:12] And you might say to yourself, wait, tally just takes this one argument. How's that? What's going on? To design a seam, we have to think in terms of the impact of the function. So pure functions are really easy, right? We treat them like a black box. We pass in a couple of arguments, we get a return value.
[00:21:29] We pass in the same couple of arguments, and we know we're gonna get the same value. They're repeatable input and output, so recording a bunch of those is going to be safe and make sense. But mutation is a lot more difficult. In this case of this tally function, if I pass it 4, I get 4. But if I pass it 4 again, I'll get 8.
[00:21:48] And so if you think more broadly, it's because this instance variable is changing. And so the real argument of this is twofold. First, there's the calculator, and what's the state of the calculator, and then there's the number we're passing to it. And so if we fix both of those invocations to pass in the calculator at zero, then we'll actually get back to a repeatable input and output.
[00:22:11] And so if you think of it that way, we're just broadening the seam of the cut that we're gonna make. And here we're going to just pass an anonymous Lambda. And in that Lambda, we're going to call tally on the calculator that gets passed in for the number that gets passed in. And then also we're explicitly returning the total so that we get a clear input and a clear output.
[00:22:32] That'll make our recordings more valuable. Once we've recorded, we want to make sure, or excuse me, got ahead of myself. Now we've got to record the interactions at the seams that we just made. In the pure functions case, all we have to do is add this option that says record calls true. And, because in legacy environments, often times we might want to deploy code, but not actually make code changes to our Ruby files, almost all of Suture's options can also be set with environment variables like this.
[00:23:03] And to record some calls, all we have to do is call the thing. You could use the command line. You could just set some params, call show, that's gonna record an invocation. Set different programs, call show, that'll record and so on and so forth. You can also record via the browser. If you've got a route, you can just go to the browser and keep refreshing the page.
[00:23:21] Those will record as well. You can, even if you need to, you can record in a deployed environment like production, and then pull down both suture snapshot and a production snapshot to to replay off of that for cases where you don't have a good, solid, reliable development database. Now in the case of the mutation, we're also going to set record calls true, and because we already did the hard work of identifying a seam, it'll work the same way.
[00:23:48] We can set some parameters and call index, and that'll record. We can set different parameters, call index, and repetitively do this to generate some recordings. Now, where do those recordings go? By default, Suture is gonna assume you have a database, a SQLite database, at dbsuture. sqlite3. And you don't have to set this up.
[00:24:08] This is all invisible to you. You'll just notice that a database shows up. You don't have to worry about the schema or calling it. It's just a place to save stuff. I did this because I heard Ruby was getting a database yesterday. Matz was talking about Ruby 3 and databases, so I figured that was a good approach.
[00:24:23] And all it really does is it dumps, using marshal. dump, the inputs and the outputs, so that we can record the calls. So you might ask yourself does this scale? Does this work with Rails? I was also curious about this, because I spent a hundred hours on this gem before checking to see if I could use it with Rails.
[00:24:41] I'm gonna look at an example, the Gilded Rose Kata. This is an exercise. for practicing refactorings. It's up on Jim Weirich's GitHub here. And here I intentionally put it into a Rails controller and what it, you don't have to read it too closely other than to see this is a suture that takes in items and returns items after they're updated.
[00:25:01] And so I'm going to go to the page and this is the little page that I made. This repo, by the way, is inside of sutures repository, and you're welcome to pull it down and play with it. And obviously this is a really beautiful website, but it's just there as an example. I'm going to create a few items. So here's a normal item and another item and a third item, and I'm going to put them in a table and then hit this update quality button, because that's the, that's going to invoke the function that I want to record, and you can see that the table changes.
[00:25:32] And if you look at the SQLite database, there's now a few invocations that have been recorded. And I'll click the button again, and a couple more invocations. And if I click the button again, a couple more. And I keep doing this to generate all of the different cases I want to get better test coverage. So yeah, Rails apparently works, so that's cool.
[00:25:50] That's reassuring, because a lot of us who have legacy code have legacy Ruby on Rails. Next up, we have to take those recordings and validate that we can reproduce them. Otherwise they won't have any value to us later. And so in the case of the pure function, we're just going to write a little test. So we're going to test that if we create a calculator, and then we call suture.
[00:26:10] verify with the same name that we just recorded everything under, and then we pass it the subject method, which is the method under test, the old method in this case. Once we do that, it's going to load the database for that name, and then go over each recorded call, and compare the recorded arguments against the against the return value.
[00:26:30] So you can think of it in terms of Michael Feather's book. We just created a black box, and we put some characterization tests around it. And you get these tests basically for free. Instead of writing a lot of tests, we can just write a few. In the case of the mutation, we read a very similar looking test.
[00:26:46] So here we pass it a lambda call tally, and we return the calculator. Note that we want to duplicate the lambda exactly, because it needs to behave just like the other one in order for it to work with the recordings. What's interesting about this is it's actually a really good use for using a code coverage tool.
[00:27:04] So if you look at the Gilded Rose Kata and you look at how Jim wrote the initial tests for it, this is the initial test file and he had to read all the code and then write hundreds of lines of tests understanding all of it. And that, he clearly, he put a lot of investment into writing those test cases, but that's a lot for a test that you're supposed to delete eventually.
[00:27:25] So with Suture, you can take the exact same code base and write a little test like this one. Pass it a lambda, up, items come in, items come out and Suture's smart enough to marshal dump out the arguments. At argument time, and then dump them out again after invoking the function.
[00:27:43] So even though the items are just mutated here, these calls work fine. Additionally, we have a fail fast option. Normally it aggregates all your errors, so it can give you good messages. But here if you expect all the recordings to pass, it'll fail faster, which is a little bit more performant.
[00:27:59] Especially if you've got a lot of database interaction. And before we call it finished, we can check code coverage. So if you look at the code coverage here, you see it's all green. But if there was like, A particular case that was yellow or red, I'd be able to read that, and then all I'd have to do to cover that with another test is to create an item that matched those conditions and record it.
[00:28:20] So you can get 100 percent code coverage writing no tests at all, which is really pretty cool. So now comes the most important step, the center of this square, how to refactor the code. Unfortunately, I'm really bad at refactoring. I don't know anything about refactoring, really, and that's not my forte.
[00:28:37] Okay. That's why I needed this tool to make it more safe for me. If you want to learn about refactoring, my friends Sandy and Katrina wrote a book this year called 99 Bottles of Object Oriented Programming, and it focuses a lot on refactoring techniques for Ruby. Also unlike my book, this book actually exists.
[00:28:57] So you can go buy it and enjoy it. In the case of the pure function, how we can refactor this, it's a simple method, right? It's adding. So if we remind ourselves of what the bug was, which is, it doesn't work with negative values. Then we can create a new function and do the simple thing, right? Left plus right.
[00:29:15] That's all it really needs to do. But we're going to do something else as well. We're going to actually reproduce the bug. So we're going to return left if it's negative. And we'll just keep a note to fix it later. Because we want to retain all of the behavior that it currently has so it passes against all the recordings.
[00:29:32] And the real reason we do that is we want to just change one thing at a time. We're here to refactor the code to prepare ourselves to fix the bug. We're not here to fix the bug yet. And it's, it would be arrogant to try to fix it now because we honestly don't know where else in the system it's actually depending on the unintended consequence the transcript so we're going to, we're going to make changes to the new tally function.
[00:30:02] Here we, we instantiate the IVAR, we add the number, and then we return nil. Just because the other one returned nil, and we want it to behave as identically as possible. But we want to reproduce the bug, so if it's an odd number, we'll just return as a short circuit at the top, with a fix me to remind us this is what we're here to change later.
[00:30:22] Kent Beck said, make the change easy, and then make the easy change. So that's the mindset of this kind of refactor step. And that's all we really have to do to refactor the code in this case. Verifying is interesting, because now we're going to use the same recordings against the new code. And in the case of the pure function, we write another test, and that test is going to look exactly like the first test, except instead of calling the add method, we're calling our new add method.
[00:30:51] And it just passed right away, because pure functions are easy. Inputs and outputs. In the case of the mutation step, we have to again, replicate the same kind of test, So create, call suture. verify, we pass it the same looking lambda, and then it fails. And it fails because our refactor was actually, there's a bug in it.
[00:31:12] So what's going on here? Speaking, I mentioned Jim Wyrick earlier. Before Jim passed, one of the things that he taught me is that any library is only as good as it's error messages are helpful. And so I wanted to write very helpful error messages. So this is an example of one of Suture's error messages.
[00:31:29] It's going to, whenever a verification fails, print out a customized readme to help you fix the bug that, that, that's in your code. Including your progress to date. So if we look at it and we zoom in, the first thing it does is it lists out all the failures, just like a test runner would and you can see here that it's got ideas to fix that thing underneath each one.
[00:31:50] In the first case, there's like a focus mode, so you can just focus on one thing by setting this environment variable to the ID of that recording. Additionally, if you think that a particular recording was made in error, you can delete it from from a console. And I wanted to give good advice about solving these failures.
[00:32:07] So there's some additional advice at the bottom of the failures. The first one suggests that maybe you want to build a custom comparator to compare whether the old thing and the new thing's return values are identical. To compare results by default, it's very simple. Suture just assumes that the thing on the left will pass a double equals test against the thing on the right.
[00:32:29] Alternatively, if that fails, it'll also consider two things equivalent. If they Marshall dump to the same string value, you might ask like about active record. It has some custom stuff in place in case it detects that the two objects are active record. And essentially what it does is it compares their attribute hashes.
[00:32:47] There's some more to this and there's an option that you can exclude certain attributes if you want to. I think by default it just skips created at and updated at. But what if even still, your two things don't equal each other? That's not good and so I needed to create a way for you to have custom comparators.
[00:33:04] Here you can define a comparison however you like. So if you look at our calculator example, Just pretend it had a lot of other fields that weren't very important for the purpose of our refactor. What we could do in the case of, say, maybe we returned calculator instead of just the total number, we could write a custom comparator that would take the recorded and the actual values, and we would just simply just like you were writing a custom comparator in any other Ruby or a custom equals method, just compare the total value between the two in order to get to a passing working state.
[00:33:34] Classes are also a thing, and so you don't have to use so many anonymous lambdas, You can also create your own class. You can even extend our default comparator. So like you could call super and if it passes that, that's fine. Otherwise call some custom logic. And then all you do is pass an instance of your comparator into the suture.
[00:33:53] So going back to the error message, there's a little bit more here. All of the tests are run in random order by default, and it's at a generated seed, and so the error will tell you what seed it ran at, just in case you come across something that was like, a bug that only happens in a particular ordering, you can lock it down.
[00:34:10] If you know that you're in a situation where insertion order has to be the order you call everything in, You can also turn off the randomness by setting this to nil. And there's other configuration options too, and I wanted to make those discoverable. And so the error will list off how this particular verification was configured.
[00:34:26] It'll tell you the comparator that you're using, where the database is, whether you're failing fast. You can limit things like how many calls you test, because maybe it's really slow. Or set an explicit time limit to only budget maybe five minutes in your build. You can also limit how many error messages get printed, because you don't need to see a thousand error messages, typically.
[00:34:46] And it'll tell you what the random seed was. Finally, I wanted to give a sense of progress, because if you're refactoring something, you expect it to always pass, but if you're gonna re implement something, you start from zero and you slowly build up. And if you look at the bottom, the result summary tells you how many passed, how many failed the number of total calls, and a little progress bar at the bottom.
[00:35:06] Selfishly, the reason I did this is I wrote a progress bar gem five years ago and I never had a chance to use it. This is the first thing that uses my progress bar gem and that made me pretty happy. And so the idea is to give yourself a sense of progress over time. Again, I think it's really important and like a takeaway from this talk is to think about the messages that your gem produces.
[00:35:28] So all that to say, remember, our test failed, right? So like, why did it fail? Let's look at the message now. And in this case, you can see that the expected return value was 0, but the actual return value was nil. And if you look at the calculator, its state was nil as well. And this is the only case that failed.
[00:35:45] So let's look at our code. And what we realize looking at the code is that we're too aggressively returning if something is odd. So in the case of we only call with odd stuff, Then total never gets set, so it's nil. So the solution is just simply to move this up to the top of the method. And if we do that, the test passes.
[00:36:02] So in that case, the test was, already a little bit useful to helping guard against this refactor. Alright, so once we've verified that the new code path behaves the same as the old code path, with the data available to us, an interesting thought is, what if we could just use like In English, we might call this double entry accounting.
[00:36:22] You do it over in the new code path, and then you do it over in the old code path to make sure that it's consistent. Remember our mission here is to make development, happy, testing, happy, staging, happy, and also production. Happy. And our progress so far is that we've only really concerned ourselves with development testing, and we haven't yet addressed staging or production.
[00:36:44] If we look in the case of the pure function, all we have to do is add a parameter called call both. And it'll call the new code, and then it'll call the old code, and make sure that they give us the same return value. Otherwise, it'll raise an error, which is usually safe to do in a staging environment.
[00:37:00] In this case, it just works. But you'll find that in practice, in staging and production environments, you tend to get a lot more interesting data than you can generate locally. And so my hope is that this feature will be valuable to people who are trying to get something through a rigorous QA exercise.
[00:37:17] If we look at the mutation function here, we just set the same setting called both, but unfortunately it doesn't work right away. We get another huge error message. This is a a mismatch error. And if we zoom in, it's telling us that the calculator with total two and argument two. The new codepath returned 2 here, but the old codepath returned 4.
[00:37:40] Why is that? If you look at it, it's because we're passing the calculator in and then changing the calculator. And so there's an additional option when you know that you're mutating the arguments, where you can dupe your arguments, clone them before each invocation. So this protects against arg mutation.
[00:37:56] So I set that, and I think, okay, cool, this will work. But unfortunately, it still doesn't work. And the reason is, now calc is actually never getting mutated, because we're just duping everything before every call. So we actually have to update our lambda, or else total will always be nil. So we zoom out, and now we actually have to reassign calc inside of the lambda, In each of these cases, and we get back to a working state.
[00:38:19] Now, this is a little bit ugly. This is a lot of boilerplate code configuring this thing. And just remember, each of these modes is optional. Maybe you only use Suture for one of its four uses. But, keep in mind too, that if you're genuinely like dealing with some very nasty legacy code, You have to think of the trade off of, do you want to go slow and safe, or is it okay to go a little bit faster and make potentially make more errors?
[00:38:44] And that's a judgment call that you have to make. So that's what it's like in staging. In production, I want to be able to fall back, because one of the reasons why refactoring is so safe is that Or so, so unsafe feeling is that I empathize with users. I don't want to make a change that's going to produce a bad result for people using my software.
[00:39:05] So what suture does is if the new path errors out, then you can just try the old one, it'll rescue to the old one. In the case of the pure function, I changed call both here. Cause now I'm going into production. I'll set fall back on error to true. And so if new ever raises an unexpected exception, then it'll just call old and return that instead.
[00:39:25] And it should be invisible to the user. And in this case, because this function works, it just works. In the mutation case we already did the hard work in the last step to make sure that both these things can be called safely. So we just change call both to fall back on error, and that works as well.
[00:39:41] Obviously, in production environments, it's going to be much faster to like just call the new one most of the time than trying to call both every single time. And there's also going to be fewer side effects. There won't be any additional side effects except for in exceptional cases. So overall, it should be safer.
[00:39:57] And if there's ever a rescued error, Suture has its own logging system built in. And you can go and check in production and see how logging see that there's not any logs about unexpected errors before you ultimately call your refactor complete. Sometimes our code raises errors expectedly, and so you can register certain error types as being expected.
[00:40:17] That way, we'll know not to rescue them, and we'll allow them to be propagated normally. So that's about the production story. Now the last step is the most fun, because we get to delete all of my code. And so just like stitches, we're gonna remove suture completely once the wound is healed, once we've decided that the refactor is complete.
[00:40:36] In the case of the pure function here we can look at this little tiny test, and unlike the really long test that I showed before, we feel completely fine blowing this away. The next thing we do is we can open up a console and delete all of our recordings for that particular seam. And those just go away.
[00:40:53] And we look at our controller method and there's a lot of cruft here, but if you just start, looking at it more closely, you can say I can kill those two things and I can remove this option. I can remove all the parameters and then just change this to the method and then shrink things back down to look normal again.
[00:41:08] And now I'm done removing it from the first bug. In the case of the second function, it's the same thing. We can delete the test safely. We can delete all of our recordings safely. And then, this is really ugly here, but we can just start deleting all the stuff that we wrote here, eliminate that, get rid of the whole broadening the seam wrapping that we did and then just return that and shrink it back down to a normal looking function.
[00:41:32] And so now we're pretty much done. We did it. We just went through a very safe and rigorous approach to refactoring the software and it was, Thanks to Ruby's dynamic nature that let us do it which I thought was pretty fun. Suture is ready to use this is the first time I've ever written a gem and then not really shared it with anyone or worked with it with anyone prior to release but as part of a talk I'd love for you all to play with it and test it.
[00:41:57] The GitHub is again, that test double slash suture. And I've also declared today to be 1. 0, so even though it has zero users I'm going to keep this API stable going forward. And together, if we work together on this, tools like Scientist, tools like Suture, different ways to think about it, we can make refactors less scary.
[00:42:18] And I think that as part of the overall theme we may be able to make Ruby more palatable for businesses to continue using longer term because we're making it easier to maintain legacy code. And that's really important over the long span of a life the long lifespan of a language. There is one last thing I want to share.
[00:42:38] Just a little story about how I met Ruby. A long time ago when I was in college, I lived in Shiga Ken Prefecture. Hi. If you don't know hi Joe then you might know Ian. You guys know Ian? He is maybe if you're not Japanese, you haven't seen this like samurai cat mascot who's become synonymous with the city of Koe.
[00:42:56] This is what Ian looks like in person. And he, if you go to hie, Heian is everywhere. He's all over the place. I was in a homestay, and my my, my homestay brother, he was a programmer too, and he had a big bookshelf of Japanese tech books, and I thought that was pretty interesting. And one of them was this the year 2000 book Programming Ruby.
[00:43:15] And I'd only just heard of Ruby cause it was still very new in America, and it was fun to see, to flip through the pages of this really old book about something that I thought of as very new. And I would talk to my friends back home and say, Hey, look, there's this Ruby language. I'm having a lot of fun trying to learn it in Japanese.
[00:43:30] And they called back and said, This is right when Rails was 0. 7 or 0. 8. They're like, Yeah this is gonna be the next big thing. And it was a really cool experience, especially to have it as an American living in Japan. But eventually I went home. And a lot of time passed and I've been doing a lot of ruby and a lot of rails for a long time now.
[00:43:47] But I have to say, like getting to come back today to talk with all of you about my experience with Ruby is very precious to me. And so I thank you very much for this opportunity. Mina.
[00:43:57] Again, my name is Searls on Twitter. I'd love if you'd follow me on Twitter. We could become friends. Tell me what you thought of the talk. I'm also my wife and I are going to be in Kansai all month. I think we go we leave on October 3rd. So if you live in Kansai and you want to go for coffee or curry in Kyoto or Osaka, we would love to meet you.
[00:44:16] Once again, very much.