 I want to take a little bit of time just to talk to you a little bit about me. So I am a software engineer at MailChimp in Atlanta, Georgia. Throughout my career, I have developed a love of refactoring and testing, specifically testing and development. And I have a wonderful dog, Luna, that I'm just, I can't help but share pictures of. If you want to reach me out on the internet, you can reach me at Twitter, at Elcrawfish. So what are we going to talk about today? You probably saw that I was gonna talk about legacy code and I'm not tricking you. We are gonna talk a little bit about what's up with legacy code. Go into what I define as legacy code and the premise of this whole talk. Then I'm gonna go over some pain points illustrated by adding a feature to some legacy code I built and fixing a bug. After that, I am gonna review some lessons I hope that you learned today and give you some other recommendations. So let's begin. What's up with legacy code? Can I get some hands in the air if you currently work in legacy code? Oh, yeah. Have you ever felt like Ron Swanson? Anyone? I certainly have. And so it seems like a lot of us work in legacy code and we've all encountered these really frustrating moments where we don't know why it's happening. We don't know what's even broke. But to begin with, I wanted to also talk a little bit about why legacy code isn't that bad. In the beginning of my career, I would look at some code I was working on and I would immediately be like, who did this? G-blame, right now. And then I'm whispering like, who's Tim? Like, who is this guy? And I kind of had this bad connotation of people that made this horrible code. But as I've grown, I've come to realize that the people that came before me operated under a lot of different constraints than I did. They had different information. They were certainly under different time constraints than I am now. And generally they were trying to do the best with what they had. So I wanted to just get it out there that legacy code isn't awful. And in fact, if you're working in legacy code, it means that you're probably at a pretty successful company. At least they make enough money to pay you, hopefully. So it's somewhat successful. And I'd like to quote one of my colleagues from MailChimp, Dustin Shields-Claus. He says on Twitter, when someone makes fun of legacy code, remember, legacy is a funny way to spell successful. Your startup with perfect code will fail. And this again demonstrates a good point about legacy code. It's cumbersome, curmudgeony, and doesn't always work the way we think it should, but it's still successful, and it was still written by people that contributed to that success. So now that I've kind of laid it out that legacy code isn't all that bad, I wanna tell you what my definition of legacy code is. Legacy code is a system of working software written by many developers over time that is often hard to change and contains business logic complexities that are not easily distilled from the code. So at the bottom of this slide, you'll notice I have working definition. And the reason I put that there is because in the beginning of my career, I would have had a very different definition of legacy code. It might have had some not so nice words in it, but as I've grown in my career, I've widened my definition, and I'm sure in the next five to 10 years, I will have a different definition. So this talk is about changing legacy code. How do you handle it? How do you wrangle it? But I also wanna mention something about when not to change. So we're talking about lots of change of legacy code, but when to think about not changing it. And I'm gonna stand on the shoulders of giants today in paraphrase Sandy Metz. She told me one time that if you have this really ugly piece of code and nested ifs, and there's a switch statement, if no one ever opens that file, no one ever reads that, don't change it. The cost of changing it, the potential for introducing bugs and risk is very risky, and if no one's reading it, then you're not reaping any of the rewards of changing software, unless of course it's broken. So Michael Feathers in his book, Working Effectively with Legacy Code, breaks up four reasons why you would change legacy code. You've got adding a feature, which is probably what a lot of us do all the time, fixing bugs, hopefully we don't do too much of that. Optimizing performance and also refactoring. I've taken these four and I've broken them up into changing behavior and improving design. And the reason why I break them up to these two components or these two buckets is because I believe that you can actually improve design as you change behavior. And it's a more pragmatic way of changing and improving the design of your application. I'm sure many of us work in businesses where we need to add features, we need to fix bugs and asking the business to give you a couple of months to refactor this really gnarly part of the code is probably not gonna happen. And so I would encourage you to take the opportunity as you change behavior in little bits of your code that you're improving design. And when I say improving design I mean adding tests and I also mean refactoring, which we'll get into a little bit later. So let's go down the rabbit hole. First we're going to add a feature. And today I'm gonna operate under this example of that we work at this place called Yarn Hearts. It's an online yarn marketplace where people from all over the world can buy yarn. They can buy different types of yarn for all different types of things. And you don't need to know a lot about yarn to understand this of course. We've got plenty of examples out there that you can draw from. So this part of the talk we're talking about adding a feature. So let's talk about the requirements of our feature. We have users who want to be able to search our application for online sellers that sell alpaca yarn. This alpaca yarn also needs to be fair trade and it needs to be organic, whatever is returned. So we have these requirements in my contrived example. So what do you do? I'm going to tell you first to look at the tests. Tests are a great place to start and especially in Rails you've already got this built-in test directory along with these subdirectories, controllers, mailers, systems, models, all of that. You could also be using RSpec in which case you're under the directory spec but either way hopefully you have somewhat of a good directory full of tests. And if you don't really know where you should be looking think about what kind of object would I be calling? What is it going to do? Is it going to change data? Is it going to change something in view? That sort of thing. You may also want to reach out to your teammates if you have no idea what you're doing in this new code base. Say you're like in the second week. Teammates can be a great resource with legacy code as I'm sure a lot of you know. There's some people that have probably been around your company for a while and they're the ones that kind of contain a lot of the information and the knowledge. Unfortunately it's just stored in their brain for right now. Maybe we'll come up with a way we can extract that out easier than just getting them to write stuff down. But the knowledge that it's better in people's heads is incredibly valuable so make sure you're doing that. But for the sake of this example, so we're adding a feature, let's assume that our legacy code has no tests around it. I'm going to quote Michael Feathers again. It says, he says rather, it's better to confront the beast than to hide from it and I'm taking this completely out of context so you can have to believe me when I say that what this is referring to is when you have no tests surrounding current functionality you should stop and add tests to preserve current functionality. So let's go ahead and add tests to preserve current behavior. You may be thinking why should I be testing? Hopefully all of you understand the value of testing but you may not be fully convinced. So why I test is I want something to be able to prove that my code works and that the assumptions I'm making about my code work are correct rather. Testing also helps you to uncover trivial bugs. For example, I don't know how many times that tests have helped me to uncover a bug where I expect something to be accepting a string as a parameter and sometimes it's getting an integer or something like that. Another reason why I test is it's a great way to document how your code is actually working. I've been in places where there is sort of a divergence between how code should be working and how it actually works and that can be kind of confusing for someone who's new to the code base or even new to the area of code that they're working in. And finally, testing helps you design better code. If something's hard to test, chances are it's not as well-designed as it could be. We've all been in that situation. We just don't have time. I do not have time to go into that test file and add the tests. No, no, no, no, I need to do this other ticket. First, I'm gonna say that I hope I think that you might have the time. Just gonna go out on that limb. And I'm also gonna say this. So, since we are going down the rabbit hole, I'm gonna quote Lewis Carroll. He wrote that the white rabbit always, the white rabbit said in Alice in Wonderland, the hurryer I go, the behinder I get. And this is sort of me cautioning you again, saying like, if you don't have the time, that's fine. But that's a trade-off. You're making a decision that the tech debt that you are putting down is going to need to be repaid. You're kind of making a contract. Hopefully your legacy code is successful and someone gets to touch that part of the code again. And I think you and your team need to fully realize that this will decay and you're not improving necessarily the design of your code. You're not improving it for other people. So it's a trade-off. Also I have to suggest if you don't have a lot of time but you'd like to kind of test whatever you're adding, working effectively with legacy code. Again, Michael Feathers, he has a chapter all on this that you don't have the time but you have to change an area of the code. So I recommend reading that as well. Okay, so example time. I've done a lot of talking. Let's look at some code. So again, our requirements, our users want to be able to search online sellers for alpaca yarn and all the yarn that is returned or the sellers that are returned are fair trade and organic. So we look into our magical legacy code that I've created and we have this yarn model and we look in the database or we look in the model and we see that it has a yarn name, gauge, weight, color is organic, is fair trade and seller ID. Now I'm thinking seller ID is probably a foreign ID. So I look at seller. Look at the seller table and it has name, address, in good standing, is organic, is fair trade. And I find that the yarn model has this class method called get sellers. So I'm adding this feature. I think I know about where it needs to be. And I look at the code. Please don't read this. Don't try to read this. It's too much. But I like, if you squint, you can see like a lot of nested code. You've got a switch statement. Those are a lot of queries. You've got some nested ifs. Looks pretty gnarly. So again, you've got this, you've got some cotton, silk, mohair, cashmere, synthetic, Gloria, these look like yarn types. If you don't know that they are, they, I Googled it, they are. But so you've got all this switch stuff happening. Oh dear, and I'm telling you, you need to add tests to preserve all this current behavior. It's a bit of a doozy, right? It's a lot to take in. But we're gonna do it together. The first thing that I'm going to suggest that we do is look at the like, the most innermost logic around what we saw in the first bit of the switch statement, which is around wool. So we're in the yarn model. We are in a method called get sellers. It takes a name and some optional params. We are defining an empty array found sellers before we go into the case, the switch statement. Within this, we have some found sellers. So presumably we took that empty array and defined it into something that is not an empty array. And we iterate over all of those sellers. We check if they are in good standing. We check if they are organic in our fair trade. And if that's true, then we are going to place that seller in a preferred seller's array. If they are organic, but they are not fair trade, we are going to put them in a new array called midsellers. Okay, that's a lot. You may be thinking like, Lauren, we're adding a feature. Don't tell me about what the code is already doing. But this is an important part to kind of see like what the code is doing and what people were thinking of while they were writing it. We also in this code do a little bit more after that loop around found sellers. We, if preferred sellers, if the count for preferred sellers is greater than the midsellers count, then we want to return both preferred and mid together, making sure that the midsellers is after the preferred seller. And if that's not the case, we just want to return all of the found sellers. So we're adding tests to preserve current behavior before we add the feature. First, we're going to write the simplest test I can think of. First, we're going to look back. So we know that in order to kind of get set up with this test, we're going to need to set up the right kind of seller and to point this out. So we need a seller that is in good standing, a seller that's organic and a seller that's fair trade in order to get some preferred sellers. So let's start out. I've got a context. When you pass in wool, or I'm sorry, and we're describing get seller, the method, and we're passing wool into that. So when you pass in wool as yard name to get seller, it returns one found seller. So that is my test. So I need to create that one seller. Again, the seller needs to be organic, fair trade, and in good standing. And then I need to create an instance of yarn that has the name wool and that has the seller that I just created attached to it. The yarn also needs to be organic and fair trade. So I have the setup. I've got my seller, I've got my yarn, it should pass. So I go ahead and run the method under tests. So found sellers equals yarn.getsellers passing in wool. And then I have my expectations. I expect that we get one seller back and that it should equal the seller that I created and set up. And it passes. So we're adding tests to preserve the current behavior. I wanna go back here. So the reason why I would suggest adding the tests first because originally, and let me back up, in my earlier career, I would think, oh, we need to fix this. But I really encourage you, if you do want to, if you do see areas like this and you want to improve it to make sure you're adding the tests first so that you don't inadvertently break something. So let's add another test. So we need to also think about mid-sellers. So in my previous example, I just created preferred sellers but this time we need both preferred sellers and mid-sellers and mid-sellers needs to be at the end. So preferred sellers needs to be at the beginning. So I write another test. The context, when you have more preferred sellers than mid-sellers, it returns an array with preferred sellers before mid-sellers. So again, I gotta do my setup. I need a preferred seller that is organic in a spare trade, a second one of the same type. Then I need a mid-seller. And a mid-seller is organic but is not fair trade. Then I need to create yarn instances that match those sellers or that I'm sorry are attached to those sellers. So I've got those there. Next, I'm going to call get sellers on yarn, passing in wool and then finally my expectations. So I expect that this method will return three objects in its array and that the found sellers, the last bit of found sellers is the mid-seller and it passes. Now you may be thinking, like I've talked a lot about refactoring and testing and you may not still be convinced because this curve is gonna get difficult to test kind of quickly. But the reason why I suggest you go through all of this is because of legacy code. That's why we're here today, improving legacy code. If you're able to add tests to a certain area of the code base, even if you can't refactor it, you've left it better than you found it. And if we incrementally improve legacy code then eventually it won't be this gnarly kind of weird, smudgery thing that doesn't do the things that we want it to do. I'm going to quote Martin Fowler from his book, Refactoring. And although we're not doing refactoring right now in this example, he says, whenever I do refactoring, the first step is always the same. I need to build a solid set of tests for that section of code. The tests are essential because even though I am following refactoring's structure to avoid most of the opportunities for introducing bugs, I'm still human and still make mistakes. So we're all human, hopefully. Well, if not, maybe it's an upgrade. I don't know. But we're human and we make mistakes. And that's why we should be adding tests if we have any hope of refactoring. So I've added tests to preserve current behavior but we still have to add this feature thing that they pay us for. So again, users want us to be able to search online sellers for all packing yarn and that all packing yarn has to be fair trade and organic. So let's add a test. I did say that I loved test-driven development. So we're gonna add a test first. So this is my test. Context, when you pass an alpaca as the yarn name, it returns sellers with only yarn that is fair trade and organic. And to be more specific, this should say it returns sellers with only alpaca yarn that is fair trade and organic. So like the other tests, we have to build some setup. We have one seller that should be included that is organic and fair trade and a seller that's not included that is not organic and is not fair trade. Again, I go ahead and create the yarn attached to the sellers. I call the method get sellers passing an alpaca and I make this assertion that we should find one seller and it should be equal to the right seller that we put in our setup. And the test fails. And this is good. That's why I have thumbs up here because you've captured your requirements in a test so you know that when it passes that you've done your job. Specifically, this is the error that you get. Undefined method count for nil class. And just to go back here, that means that found sellers is nil. So that means that nothing's being returned when we make the call. And that makes sense since we haven't implemented it yet. So I go into the big gnarly method that I added tests for and I go ahead and add another switch statement with alpaca. And I get the sellers where the yarn's name is alpaca and I'm joining that to the yarn table. So good. And then I do a similar thing that I saw in the other examples with the switch statement. I define found sellers and iterate over each one. There's a typo there. It should say found sellers.each instead of sellers. And then for each seller, I check if they're organic and fair trade. And I return and I add them to found sellers and then return found sellers. And the test pass. Which is good. But the cool thing too is that looking at this, there are probably better ways to do this. I could have probably passed in the is organic and is fair trade into my query at the beginning and not even had to iterate over all of these. But I have a test and when I go back and improve this code, I know it's still gonna work because my test is gonna pass. But that's only the happy path. So this is a contrived example, of course. But there may be other constraints that you're working with. Edge cases where maybe something can be organic but then something new comes out and they have to be that too. There are edge cases all over legacy code and making sure that when you add tests that you're not just thinking about, well, this is the happy path. Let's go ahead and like we did our tests. We did our due diligence. But also think about the not so happy path. What happens if something wasn't passed in? What happens if an error is thrown? Should an error be thrown? So I'm gonna flash this slide back up here and don't try and read it. So we are still in our add a feature phase of this talk and we did a lot. We added, we saw that there was a lot going on in this method called the get sellers, a lot of nested logic but we added tests for all of that. Like let's assume that those two tests were enough to just cover everything. So we did our due diligence. We tested the current behavior. We added our tests for the feature and then we added code for our feature as well. But then we go back here and like we have this reaction. I don't know about y'all but I've been in some code bases where I see these really long methods and I'm just like, I don't know. I don't know how to do this. I don't, you know, it's just a lot to take in but the beauty of it is that if you kind of follow these steps, you will actually be set up to refactor quite well because you have these suite of tests that are automated that you can run and wow, you can go ahead and refactor it. You may want to pull something out into a service object. You may want to maybe create different subclasses. There are other opportunities out there for you to change the code. It will of course change some of the tests but ultimately you'll be more confident in your changes. So I have an asterisk by this word refactor and it has if you can at the bottom. And the reason why I would totally be gung ho about like write all the tests, add the feature, refactor but we don't all have that time. Because probably your ticket should have been done yesterday. And so we don't always have time to refactor but and actually you shouldn't always refactor. I think that I've been in a couple of code bases where there are some really big parts of the code that make us a lot of money and to go into that and refactor it is a huge risk because even if it's a small bug, it can impact how much money your company is bringing in and no one really likes shipping bad code either. So I strongly recommend if you are going to try and refactor something that makes your company a lot of money or it's particularly difficult to understand refactoring would be best be done more in a collaborative effort with people who know that area of the code base a lot more. But refactoring is good and if you have the time, do it. The reason why I've already covered. Now we go into kind of like how we refactor. Some of you might already know and I've mentioned this before but in our example, look for opportunities to split up the code. For example, this switch statement might be a really good example or a really good opportunity to replace the conditional with polymorphism. You may even want to do subclasses. You may even want, plus the extracting out into subclasses, you may even have like additional objects that you could pull out and separately test while preserving the current test and your current behavior of the method you're in there now or you're working in now. All right, so we've talked about legacy code. We've talked about some of the pain points when adding a feature. Now let's go ahead and fix a bug. I'm not gonna talk about how to debug that in and of itself kind of deserves its own talk. So I'm going to recommend that you watch a common taxonomy of bugs and how to catch them by my colleague, Kylie Stradley. It's a really great talk and covers a lot of the stuff that I'm not gonna cover here. So in our example, so imagine we have this bug that there are sellers with silk blend yarns that are not being returned when a seller searches for silk and that should be happening. So first, we're gonna write a test to duplicate the bug. Very similar to before context. So when you pass in silk as the yarn name, it returns sellers with silk and silk blend yarns. So we're gonna start up with setup. So we're gonna create two sellers. One seller is just silk, just sell silk. And then the second seller has a silk blend. Both of them are organic. We're gonna again create yarn that's attached to these sellers and make sure that one is silk and one is silk blend. So we've got the setup. Now we're going to call our method again. Yarn get sellers. And this time we're passing in silk and we expect to find two sellers. And we expect that the silk blend seller is included in the returned sellers. The test fails. So good, we've effectively replicated the bug that we were seeing. This is the specific error and this is exactly what we expect. We expect to only be getting the seller that has the silk yarn and not the silk blend. So this is when we change the code to make our test pass. So this is the original code where we are getting sellers and joining on to the yarn table with the name just silk. And so what I add is not only silk for the name but also silk blend and the test pass. But you may remember that this part of the code is quite long and the circled part there shows that I'm only addressing part of the problem because we are grabbing the name of silk here, here, here, all the way down. All of that, that's a lot. So I'm only addressing one part of the problem. So I think about what other ways, how this could go. So first I look that in the first if statement we are also passing in gauge. So I'm gonna add a test, very similar to the previous one. So I've got a seller and a silk blend seller but this time I make sure that the yarn that I'm creating has a gauge of six. I pass not only silk in but six into the get sellers method and I expect there to be, I expect there to be two sellers and the silk blend sellers to be included and it fails. Again, this is the same error we got before so I go ahead and add my solution and the test pass. Now let's imagine that we did that for all of the ELSA statements. It's kind of annoying, right? So this might be your opportunity to refactor and again, like my argument here is that we can improve legacy code in increments pragmatically. When you're changing part of the code you are also improving design and this would be a great opportunity for you to do that in which there are so many other more optimal ways than wedging all of that into an if ELSA statement. So after we've written the test to replicate the bug, fix the code so that the tests pass we want to run all of the tests. We want to make sure that we didn't break anything. I've had the experience of kind of like being in spaghetti code where you start pulling here and you kind of get everything ready here and then like over here there's even a more of a bigger kind of not occurring. So running all of the tests especially if you have good test coverage can give you more confidence in pushing your changes. So what if something broke when you ran all the tests? First this is good, I think that it means that you might have a really good test suite and you didn't go ahead and ship bad code. Secondly go ahead and look at what failed and why did it fail? Did it fail because it's a flaky test and it always fails? Did it fail because you accidentally changed some logic that you expected to be fine? So look at what failed. And if you think that it might be the code that you added go ahead and check your assumptions on your new code and see if it aligns with what really needs to be implemented. Okay, so we've talked about legacy code, the pain points, adding a feature, fixing a bug. Here are some lessons that I hope that you learned. Firstly add tests when you touch code that is currently untested. Your fellow developers will thank you. Bonus is that if you are adding tests to parts of the code that are not tested currently, you're gonna learn that area of the code a lot faster than you would otherwise because learning how to test something means that you understand the problem. I'm also gonna suggest that we refactor to improve design incrementally and I've kind of been harping on this the whole time but we're rarely gonna get a lot of time to refactor big parts of our code. So if we can do it in small increments and kind of wedge them in there, it's the best way to improve our legacy code. Ask for help. Sometimes our teammates are the best sources for help. Add tests for new functionality. And that's all of my lessons learned and here are my recommendations. So I talked a lot about Michael Feathers working effectively in legacy code. I also mentioned refactoring by Martin Fowler. Both of those are great. I also recommend object-oriented design in Ruby and apprenticeship patterns. In addition to those books, I recommend the talk by Kylie Stradley, a common taxonomy of bugs and how to catch them. Additionally, Get a Whiff by Sandy Metz which heavily relies on the code smells described in the refactoring book. And finally, if you wanna learn more about code smells right now, you can go to sourcemaking.com slash refactoring slash smells. I'd like to take a quick moment just to thank all the people that helped me to be here today including my conference buddy, Megan Tew, my co-workers, Tim Hayes, Kylie Stradley, Michael Hurd and Chelsea Yeomans. And finally, my wonderful and supportive husband, Gabriel Hawley. Thank you very much.