Your Refactoring Toolbox
Rachel Bobbins at Playgrounds Conference, 2017
Awesome. Like I said everyone, my name is Rachel Bobbins, I'm an engineer at a company in San Francisco called Stitch Fix and prior to working at Stitch Fix I worked at a company called Pivotal Labs, where I got to work on lots of different iOS apps. Small, large, enterprise, startup. During that time I learned a few things about refactoring and I hope to share some of my learnings with you, in order to make your lives easier.
Yes. Last summer I went to Iceland for the first time and I saw these houses from the side of the road. From far away they looked really awesome. They looked like they were built into the landscape. Like that volcanic rock had gradually covered them up overtime and I am not an architect, but it seemed like an architects dream to have houses that blended so naturally into the landscape like these. Naturally, I had to get closer to take pictures, but once I got closer I realized that actually it was all a façade. I don't know if you can tell from the top photo, but the house wasn't gradually covered by volcanic rock. It was built around the volcanic rock.
There was no back wall and to be honest, the connection between the roof and the sidewalls and that rock was a little bit loosey goosey. When you got really close and you peered into the house, like we see on the bottom photo, you could see that it was never actually finished. There was no back wall, there were no windows, there was construction material all over the floor, and of the two houses pictured, one of them had a cavernous pit instead of a floor.
These houses reminded me of some of the refactors I have worked on before. You start off with the best of intentions. "I'm going to make a beautiful, beautiful codebase, it's gonna fit so naturally into what's already there and it will be so harmonious". Then what you actually end up with is a pile of rubble that looks good from far away. It has the best of intentions, but it doesn't actually meet the need that you're trying to address. I think that with these houses, it was a shame that they ended up like that, because they had so much potential. There's so many tools out there to help renovations go well. Right?
Architects, people whose whole lives are about building houses that serve the needs of the people who live there and they know what works, what doesn't and what some of the best practices are.
Scaffolding to help support the house while renovation is in progress, and to keep things from falling down.
Tarps which catch things when they do fall down.
Local regulations, which tell you what is and isn't allowed and what might or might not be a good idea.
Hammers, screwdrivers and power tools. All of these are basic construction materials that architects and builders use for their jobs on a daily basis. It's really important to not forget, that those are there too, to support the renovation.
Everything that's already in place. The walls, the windows, the roof, all of the spare materials lying around. All of these constrict what is and is not possible, and give suggestions about how the renovation might proceed.
Refactoring is a lot like renovating
Refactoring is a lot like home renovation and like home renovation, there are so many tools out there to support it. What we're gonna go into today is some of the tools that are out there to help make your refactor a success and to make sure that it doesn't end up like those beautiful decrepit houses.
But first off, why refactor? I think this is a really important question to answer because a lot of the time, it seems like starting from scratch can be easier. Why refactor when you can build a brand new app, build a brand new module? I think there's a few really good reasons to refactor.
Rebuilding costs time and money. The time that you spend rebuilding your app or rebuilding your library, is time not spent on production code. It's expensive in terms of time, it's expensive in terms of money.
You can make incremental progress. If you're starting from scratch, it's a lot harder to make incremental progress, whereas if you're refactoring, you can make incremental progress and if your priorities shift during that refactor, it's okay to pause, address the higher priority things and then come back to the refactor.
You are not starting from scratch. I think another really powerful argument for refactoring is that if you start off with a vision of where you're going, you can build on top of everything that you have already built in your app or in your library and you're not starting from scratch. If you start from scratch, you will probably run into some of the same pitfalls that led you to where you currently are, thinking that you need to start again.
What does a successful refactor look like?
Talking about refactoring. What does a successful refactor look like? I think it's really important to define success. Fun fact, this is one of my favorite interview questions, if you're ever looking for one; What does success look like to you?
To me, a successful refactor looks like it has three things.
- Team effort. One of them is that it can be a team effort. I don't want my team's buzz count to be one, and I especially don't want to be that one person, who is holding up that buzz count. I want everyone on my team to be motivated and excited about the refactor and to understand what we're planning to do and why we're planning to do it.
- Avoid a pile of rubble. I think that this is worth calling out, as a metric for success. Because a lot of people accept that an intermediate pile of rubble is the cost of doing a refactor. And yeah, you'll have to start over and throw it out a few times so eventually you'll end up where you're going. No, that is not my idea of a successful refactor, so I'd like to avoid it as much as possible.
- Incremental progress. Finally, one of the things that I think helps make a refactor successful, is that you can make incremental progress. You don't end up with a build that is red for three days, while you're trying to refactor all the things, all at once.
I will also pause here to say, that the refactoring strategies that I'm talking about, are great when you're working on your app and you're refactoring the code that already exists. They don't work for swift language migrations. Those you're on your own for.
So talking about tools, we talked earlier about what's available for home renovations, and there's also a lot that's available for people who are refactoring their Swift applications.
Some of these tools are things that the Swift compiler can help with, things like:
- Deprecation warnings,
- Compiler warnings
- Compilation failures.
All of these are messages from the Swift compiler, from Xcode, to you, telling you that something is not quite right and it might be worth taking a look at.
Protocols and tests I think are both pretty trendy.
Whether you agree or not, with protocol oriented programming and whether you agree or not, that tests are valuable, they can be really useful when used in a targeted specific way, for your refactor.
Finally, at the bottom are some of the fuzzier things that I think are really helpful for your refactoring.
Things like: your team's existing code. This is really important, because it's informing your decision to refactor. You know what the problem areas are, you know what you would improve if given unlimited time to work on it again. Between you and your team, you have a pretty good vision for why you're refactoring and what you hope to get out of it.
Finally, I think open-source inspiration is a really useful tool. The same way that architects might visit buildings that other architects have built, you can gain inspiration from them and gain an idea for a way to approach a problem that you might have not thought about before.
Real life example: VeryOldBathroom
I'd like to tell you a little story. I moved apartments recently. My old apartment was tiny and I'm really happy to be in a new one. One of the many problems in my old apartment was that the bathroom was about the size of an airplane bathroom, with all of the similar problems to boot. One of those problems is that the shower was really confusing and temperamental, and every time we had house guests who came over to visit, we would have to inform them how to use the shower.
We'd have to remind them how to use the shower and then we would have to plead with them, not to screw up the shower. If they screwed up, what ended up happening was they would turn on the water, scolding hot, couldn't figure out how to turn it off. Run out of the bathroom wrapped in a towel, "Rachel, how do I turn off the shower"? While all that yelling is going on, the steam is setting off the smoke detector.
It was not fun. It was just a little bit tense when people came over and they used the shower for the first time. So one of the big things I wanted to solve in moving, was; I wanted to have a dream bathroom. Why did I want to have that? Because here are the problems that existed with the old shower:
If you look at the method called turnOnShower, we can see two really big issues. One is that the shower knob angle is non-deterministic and if it ever gets to -720, the shower will collapse. That's one of the problems; that the shower knob angle is hard to turn on.
The other problem, if you look at the implementation of turnOnShower is that it's non-deterministic and it is possible to get stuck in an infinite loop of trying to turn on the shower. You can also see that whether it worked or not was totally random.
A dream bathroom
My dream shower is a lot more deterministic and a lot less likely to have the shower knob handle get twisted off. It looks something like this:
It's not Barbie's dream bathroom, it is just a shower that works. Either it's on or it's off. Either the knob that controls the temperature is at 0 degrees or it's at 10 degrees. That's it. It's really simple.
How do we get from the old shower to this new shower?
1. Know the problem. Have a vision.
First of all is knowing what the problem is and having a vision for what you're trying to improve. With this example here, the problems with the shower were fairly obvious and I knew what the code looked like, to get it to where I wanted it to be. I think that knowing the problem is actually really important, because a lot of the times we'll get caught up in wanting to refactor our code, to use the latest, new Swift feature. "Oh man, I'm gonna use generics, it's gonna be so great". Yeah, but is that actually solving a problem? I think that's a really important question to ask yourself, because like I said earlier, time is money, and you don't want to necessarily be spending your time refactoring just for the sake of using the new feature. That vision is pretty important.
2. Temporary deprecation warnings.
Talking about actual strategies we can use to make this refactor go well. One of my favorites is to use deprecation warnings. This is not using them in the traditional sense of indicating that it is a method, which is deprecated or not, but rather using it to scope out the actual size of the problem. This is really handy, because as soon as I add this deprecation warning to the VeryOldBathroom class, compiler warnings start popping up everywhere. It looks something like this:
"This is deprecated, it's a very old bathroom". Thanks Xcode.
I can start to get an idea of how complicated this refactor is going to be. Is this going to be something I can figure out in 10 minutes? 10 hours? Is this gonna be a two week project that I should talk to my manager about before embarking on?
Deprecation warnings can be really powerful for scoping out the size of the prize, I suppose.
3. Temporary protocols.
The next tool that I've used to help refactor's go well is; temporary protocols. This is not protocol-oriented programming in the traditional sense, because these are being used in a targeted, specific way, to help keep the app compiling. Whatever tests I have green, while the refactor is in progress.
If I try to go about refactoring this, you can see I replaced VeryOldBathroom with DreamBathroom. I start getting all sorts of errors that these two types are incompatible.
What I can do here is I can use a protocol to help bridge the old and the new.
What does that looks like?
First, we define the protocol. It has a single method called turnOnShower.
Then we can extend both the old class and the new class to conform to that protocol.
We can [inaudible] method calls or instances of that old class with references to the protocol instead.
All of a sudden, everything is ok! We don't have any more compiler errors at the bottom.
After doing this, remember, this is not protocol-oriented programming we're talking about. We can totally go back and delete the protocol if we want to, or we can use the same path to slowly back away from the protocol, as we used to get there. We can add deprecation warnings back in for the protocol, then slowly deprecate it.
4. Deprecation warnings.
I keep talking about deprecation warnings, and I'm going to talk about them again, now. But with a different usage than what we talked about before. To indicate code that is actually deprecated and help us clean up after a refactor.
I am not telling you that you should write tests for all of your code, all of the time. I think if you're coming into an existing codebase and you don't have 100% test coverage, it can be really overwhelming to try and add all of that in. But what you can do, is you can use tests in a really targeted way, to help shore up your refactor.
It's a lot like the scaffolding and the tarps, that a home architect or a builder would use. We can dive into what that means.
These are some really bad tests for VeryOldBathroom. The first one will pass very rarely, because we have a non-deterministically behaving shower. The second one will pass eventually, but we don't know how long the tests are going to take to run and we can tell that it's pretty brittle, because we keep trying again and again, until we get the result that we want.
That being said, it's really easy to not write tests to start. If you're starting with code that looks like the VeryOldBathroom.
There are two ways we can use tests to help make the refactor go smoothly:
1. Test the things, which we know we don't want to break during the refactor.
What that means is we have this test, that I said wasn't a great one before, and I still don't think it's a great one. I think it's pretty terrible, but it's useful to keep around during the refactor, to make sure that we're not breaking things in the process.
In the Ruby on Rails community, people are very into test driven development. One of the phrases that you'll hear a lot is called "Red, Green, Refactor". What that means is, assuming you have tests, you write the tests you need if you don't have them already. First they're red, then you make them green. You write the code that makes the tests pass and then you go back and you refactor. I think we can apply that same strategy to some of these tests here as well, which is to say that they're not the best tests, but we can refactor them later, and they can help keep our overall refactor on track while we're moving through it.
2. Write tests for the behavior that we eventually want to see.
The other thing we can do to help our refactor go well is write tests for the behavior that we eventually want to see. Here's an example of what some of those tests might look like:
We want to test that we can turn on the shower. We want to test that it is idempotent, no matter how many times we call it it'll stay on if it's already on. We want to test that it's set to a specific angle for the knob that controls the temperature.
These tests here, we can keep them around if we find them useful, or if we are not test people, we can get rid of them after the refactor. That's okay too, if that's not your thing.
These are all techniques that I've used at work, for real life refactor's, and I think that combined, they're really useful, because if you can keep your app compiling and your tests passing, you can move more quickly through your refactor. You can split up the effort among your different teammates and you can make sure that you're not veering too far from the path that you originally set out on to solve the problems that you are originally trying to solve.
I am actually one minute under time, so thank you so much everyone for listening to my talk. I hope you got something out of it, and I'm looking forward to talking to you folks in the Q&A session after and talking a little bit more with whoever is interested about some of the real life problems that I've used these techniques on.
Thank you everyone.
If you enjoyed this talk, you can find more info: