PearConf is, in the words of the website:
a gathering of people who pair and collaborate to build software. This includes pair programming, pair design, and all of manner of cross-functional work. It’s also a gathering of people who want their workplaces to level-up on inclusivity and who, themselves, want to work at being better teammates too.
My keynote talk focused on The Technology and Psychology of Refactoring. In an effort to make this talk as accessible as possible, I have included a link to the video
(though we’re not sure how much of it successfully recorded), UPDATE: we know that exactly half of it recorded. I spend the first half of the talk on the technology part of refactoring, and I spend the second half on the psychology part. The whole technology part, minus about 25 seconds, makes it onto the video. Here’s that:
If you want the whole thing, we re-recorded this talk when I delivered it remotely a few months later via Zoom.
In this version, you get to see me talk about refactoring while sitting on my couch with zero makeup on. This is a new level of intimacy for us. Lucky you!
This post also contains the full script interspersed with the relevant slides. The substantive differences between this transcript and the words I said onstage amount to crowd work.
If you’re interested in seeing this talk live, make sure the organizers of your favorite conference know my name. You can send them this link to my speaker page!
Full Draft Transcript:
Hi folks! My name is Chelsea Troy. My pronouns are she and her. They/them is also fine.
Today I’m going to talk about refactoring.
I don’t think this talk requires any content warnings, but if I missed something and it does I’d like to hear from you afterwards. And in general I’d love to hear your feedback on this talk. You can reach me at email@example.com.
My hope is that I can provide something for everyone today. We’ll cover some technical-level details for the software engineers in the crowd and some operational details for the product managers.
So what is refactoring?
Can I have a show of hands—who has done a refactor before?
OK, how many of you love refactoring?
OK, how many of you hate refactoring?
So if we’ve done it and we have opinions on it, we definitely know what it is.
It’s kinda one of those “I know it when I see it” things, right?
The canonical definition says it’s something like:
changing the design of existing code without changing the user-facing features.
I don’t love this definition, because it comes with two assumptions baked into it—specifically into the term “user-facing features.”
1. Who is a user?
Refactoring changes the developer-facing characteristics of a code base, and developers are users.
I can almost hear the designers in the crowd wincing.
It’s true that in many cases, code design is, and should be, second to the software’s functionality. If the only way to write the feature that end users need is with messy code, then we need to write the feature that way, and that’s life. Because If the software doesn’t work for the end user, it doesn’t matter how beautiful the code is.
But it’s also true that if the code is illegible, the software will very quickly not work for the end user: developers will have a hard time understanding it and be afraid to touch it, so updates will take too long or even become impossible.
No designer wants to hear that a small tweak to this animation is going to take 2 weeks, right?
We keep code in a maintainable state to minimize that kind of wait time for designers, developers, end users—everyone.
OK? There’s another assumption baked into the term “user-facing features.”
2. What is a feature?
So we tend to equate “user facing features” to content and navigation features: buttons, tabs, text, pictures, requests…that kind of thing. Sometimes we call these features “functionality.”
That’s one kind of feature, but it represents a very small sliver of the totality of features.
Other kinds of features include:
- scaling: Does the software work when a lot of users are on it? Does the app work when the network connection is heavily taxed, poor, or nonexistent?
- robustness: Does the software respond gracefully to unexpected input from the user or from network calls?
- security: Does the software keep users and data safe when hackers try to compromise it?
- accessibility: Does the software work for users who use screen readers, large font, high contrast UIs, loud and clear sounds? Does it work for users who have compromised motor control or dexterity?
- inclusion: Does your computer vision app work for people with dark skin? Does your software only ask questions about users’ sex and gender if it absolutely must? And if it must, does it ask about those things in the right way? Does it work for all different body shapes and sizes?
All of these are features, just like profile pages, buttons, and news feeds.
And we often refactor to get some of these features, right? Better error handling, better network performance, or better predictive results—but whether we’d call that a new feature or a refactor isn’t clear.
By labeling our work as a feature or as refactoring based on whether users notice a difference makes some assumptions about who is a user,
and we don’t have to make those assumptions.
Instead, let’s imagine that software exists on a continuum toward maintainability, and refactoring is the work that we do to make our code as maintainable as possible.
What does it mean for software to be maintainable?
We’re looking for three things, specifically, and I’d like you to think of them as constant works in progress on a software product. Refactoring is the thing we do to give our software more of these qualities.
How can other technologists maintain this code together?
This is the thing we most commonly associate with the term refactoring, because we want to leave the code clean and organized so that other developers can figure out what it does, and why.
So, who does this well? I have a few examples for you.
The Spring framework makes exemplary use of documentation, rigorous tests, and thoughtful syntax and architecture choices to keep the code legible and thoroughly explained so that their distributed team—and outside contributors—can understand and improve the way it works.
Apple’s programming language, Swift, aims to enforce some collaborability on Swift projects.
Swift is a compiled, typed language. It leans on optionals and includes a lot of features to ensure the safety and legibility of the code written in it.
You can see this commitment to concise, legible code in the source, which is also on Github.
When code isn’t collaborable, we end up with arcane legacy systems that developers are afraid to touch for fear of braking them. If the situation gets dire enough, the company may commission a complete rewrite rather than maintaining the existing system.
We want software to be collaboration friendly. We also want it to be:
2. Dialogue-friendly: How does a user or developer share their expectations about this program?
The feedback cycle is an important part of software development, so if we want to develop maintainable software, that software has to merit maintaining by being used.
In order for software to be used, we need to make sure it keeps up with what users and development teams expect it to do. How do we do that?
When we deploy apps, we might do user testing. Sometimes we use app store ratings to get information about how our product is doing.
Google’s products often provide mechanisms for users to share how their expectations differed from what they got. These dialogues appeared while I was looking up how to do something in gmail. I can choose whether I found this article helpful, and if I did not, then Google shows me a text field to explain what I needed.
Software that fails to open this dialogue end up getting out of touch with user needs, and they become shelfware.
The third aspect of maintainability is…
How does the code react to unexpected or unideal inputs?
A resilient app anticipates the things that a user could do, and it reacts gracefully even when something happens that it didn’t anticipate.
When software isn’t resilient, we end up with failures where the software is clearly not working as intended, or where the development team didn’t anticipate something that could happen.
I once worked on an app for ramp agents working on the tarmac at airports to scan luggage, pets, mail, military equipment, and hazardous materials into the holds of commercial airplanes.
We build this beautiful app. API clients made calls to a server in real time to scan each bag, and they returned updated information about the whole flight calculated on the server with each call.
We sent it to the tarmac for user testing. The product owner came back to report that ramp agents spent 10% of their time with our app scanning bags.
So what were they doing the other 90% of the time?
They were walking around on the tarmac, which, by the way, is where the planes go, holding the phones up in the air.
Why do you think they were doing that?
This was the most likely way to get a network signal, since a network response blocked every single scan.
That wasn’t going to work. So we completely refactored the app, this time with the question in mind “How much stuff can we do without any network connection at all?” We couldn’t coordinate scanning between scanners this way, so we assigned each scanner to a pit in the hold and queued the scanning requests.
At the very end of scanning, the ramp agents needed a network connection to connect to check some security things. They could guarantee this network connection by plugging the phone into a networked holder called a cradle, just once at the end of scanning.
We took an existing app that did not do at all what the users needed, and we worked together to translate that feedback into changes that worked.
This is an extreme example that I chose for clarity, but this happens on a lot of software: requirements change out from under you.
We write maintainable software precisely so that, when the underlying requirements change, our code is ready to change, too.
Writing Maintainable Software…
…means assuming our assumptions will be wrong.
Software has to change when it was originally written under assumptions that are not true, or assumptions that are no longer true.
How do we hedge against the possibility that our assumptions are wrong?
We can do that by noticing and examining our assumptions.
As I was developing this talk, Kenneth Mayer asked on Twitter: When is enough refactoring, enough?
And I think it will be helpful to approach this question with an example.
How many of you have heard of DRY software engineering?
What does DRY stand for?
It stands for “Don’t repeat yourself.” This is an adage developers learn to avoid duplicating stuff in code.
Coraline Ehmke articulates a misunderstanding with this term. She explains that the point of DRY is to not repeat concepts. It’s not referring to individual lines of code.
Sometimes it makes sense to leave similar lines of code in several places. Because when you extract a variable or a class or a method for them, you communicate the perspective that these two things are the same. And sometimes they’re not.
Suppose we have a class representing each view in our app. We have a ListView and a DetailView. In each one, we need to load the view, handle button taps, and load up a navigation bar on the screen.
They both do all of these things, so it makes sense to extract a superclass. Let’s call it View.
Now we’re adding login. So we make a LoginView, and we still need to load the view, handle button presses, et cetera. but no navigation bar here, since there’s nothing to navigate to until the person logs in. What do we do?
Subclass it and no-op it to turn off that functionality?
Pull loading the navigation bar out of the superclass, and add it back to the subclasses that used it?
This is a case where creating a collaborator could be a helpful design pattern,
or creating a taller hierarchy
(sidenote: you see very tall hierarchies in mobile frameworks because of this kind of thing).
but if you need default functionality rather than interface adherence and you’re working with a language that prohibits multiple inheritance, that kind of pattern may not be an option for you.
So we get a new rule: the rule of 3.
What is it? It’s the rule that code should be duplicated in at least 3 places before factoring it out.
Why? What is it about 3? 3 is not a magic number. We could factor out the navigation bar, get 8 views in, and then run into the exceptional class, and have to decide between subclass and no-op or adding the nav bar method to the other 8 classes.
3 is, instead, a rule of thumb, because if we have to do something exactly the same way 3 times, the probability is higher that these things really are the same than if we only have similar functionality twice.
So how do we judge when to start refactoring or when to stop refactoring?
We have refactored enough when we have balanced the probability that this will change times the amount of work to make that change against the amount of work we’re willing or happy to do.
In finance, when we multiply the probability of something happening and the cost if it does happen, we call that the expected cost. So in this case we’re balancing the expected cost of maintenance with the cost we’d be happy (or at least willing) to bear.
This means that we need to ask some questions as we plan how to write our maintainable code.
1. How likely is it that we’ll have to change this?
It’s tough to get a definitive number here, but we can use a few cues to get an idea.
- The more examples we have in the code that support our assumptions, the more likely it is that those assumptions are still useful, and the less likely it is that we’ll have to overhaul this code later. This is the generalized version of the rule of 3 that we talked about before.
- The more customers we have using our app in the way we thought they would use it, the more likely it is that our understanding of their needs is accurate, and the less likely it is that we’ll have to overhaul the software later.
- Regular feedback cycles allow us to catch and correct differences in user expectations and software requirements early on—so it’s less likely that we’ll have to make sweeping changes later.
By asking “how likely is it that we’ll have to change this,” we can estimate the probability part of our expected cost of maintenance. We still need to estimate the amount of work part.
2. How much work is this to change?
Again, it’s tough to get a definitive number, but we can take some things into account as we make a guess.
- Consolidating is usually less work than separating
- It’s less work to consolidate our views to inherit from a superclass with duplicate functionality than it is to break that superclass apart again when we realize it represents a concept that is too general for our use case.
- It’s usually less work to change documented patterns than undocumented patterns
- This is important when deciding whether to go with system patterns or your own bespoke ideas for writing software. The more commonplace the pattern you choose, the more google-able they’ll be if team members run into issues.
- It’s usually less work to change an existing system than to start over
- Refactoring the scanning app, as monumental a project as it was, still involved less work than rewriting the app would have been. Even sweeping refactors can save the team down the line from needing to do a complete rewrite. By the same token, replacing the plumbing in a building may be an expensive and onerous task—but it still beats ripping down the building and starting over.
That’s why it is worthwhile for teams to make these sweeping refactors, and it is worthwhile for technologists to build their skills in making sweeping refactors.
Planning and selling the refactor
In many cases, sweeping refactors require some negotiation. Stakeholders don’t love to hear that we need to sink a bunch of developer time into a major change that won’t result in more navigation or content features. To them, that sounds like a lot of risk for no upside.
As I was developing this talk, Brent Halliburton asked on Twitter: as a product manager, what can I do to help my eng team be successful in appropriate refactoring over the life of a codebase?
Here’s the information that a product manager can collect to make space for the development team to refactor:
1. What future developments does this change affect, and how does it affect them?
People respond to the prospect of losing something. The product manager can figure out, what perceived pain or loss does this change address?
Maybe refactoring an inefficient server pattern will allow your company to save $8k of the $10k it’s currently spending per month on AWS costs.
Maybe this refactor allows multiple engineers to more easily work in the same part of the app, so work streams don’t get blocked and execs don’t pay developers to sit around waiting in line to deliver code.
Finding that pain or loss will help product managers make a case for a sweeping refactor being mission-critical. The tech team might know something about that pain or loss, but the sales team, the product owners, and potential customers can also help uproot these worries that the changes can address.
2. How can we maximize value delivery over the life of making this change?
If we have to make a sweeping change, how can we incrementally perform this change to produce benefits from the very beginning, so the business doesn’t have to wait on results?
Suppose, for example, you have a server that clients can query for the contents of federal laws. That’s a lot of text, so the responses take a long time to serialize and deliver over HTTP. Also, no human can effectively interpret that much text at once.
You’re working on a filtering feature so that this endpoint will now only serve the most relevant pieces of text for what the user is looking for. You never want the call to take as long as it currently does.
How do you deliver user value before the new filtering feature is done? Maybe you can set up a temporary endpoint that delivers the most-read pieces of text and redirect to that while you finish the filtering. Maybe you can pre-populate some suggested search terms and make an interim version of the filter that only serves up results for those terms.
It’s valuable to think creatively about the question: How do I deliver the most value to the user with the smallest amount of work? The best tech teams can leverage this question to sneak through monumental undertakings, piece by piece, delivering value the whole way.
Let’s talk about Allies.
Even with the best sales strategy, sometimes your team might need a little help to get buy-in on a sweeping refactor. Who can you turn to for help? You have a few options that are often overlooked, so I’d like to highlight them.
- The sponsor of the project
- This is not necessarily the end user. If you’re writing an app for a health insurance company’s enrollees, the enrollees are the end user, but that’s not who creates demand for the app. The demand comes from the insurance company’s clients: employers who offer this insurance to their staff. If you can convince these sponsors and the people who speak for them that this refactor is important, you can open doors that otherwise might be closed.
- The peanut gallery
- The peanut gallery is a term with its origins in French Vaudeville performances, and it refers to the top-most gallery of the theater where the cheapest seats were located. People sat there and ate peanuts, and sometimes threw them at the stage. In modern parlance it’s slang for all the people who are removed from the action but nonetheless feel the need to lob their comments around, which usually aren’t that useful.
- It can be annoying when stakeholders have conflicting views of what should go into the software. Conveniently, if your refactor furthers several of these stakeholders’ goals, it might skyrocket to the front of the priority list by virtue of being the only thing multiple stakeholders agree on.
- The person who wrote it the first way
- If you’re refactoring something, that means it was originally written some other way. It can be tempting to denigrate the original choices and even the people who made them.
- I recommend doing the opposite. The people who wrote it the first way might have intimate knowledge of the technological or institutional limitations that forced the original design.
- They are also the ones with the most practical perspective from which to advise on how they would do this feature, if they had it to do again.
- I recommend valuing this person’s work and seeking their counsel. I once worked on a rewrite that would have gotten completely blocked on some idiosyncratic API details known only to the person who wrote the thing we were rewriting. Without his help, the rewrite would have failed. We ended up naming a whole menu in the rewritten application in his honor.
To Wrap Up
We generally think of refactoring as improving the design of existing code without changing user-facing features. That’s a hard line to draw, and I don’t think we have to draw it.
Instead, we can focus on making our code more maintainable: more collaboration-friendly, more dialogue-friendly, and more resilient.
We can do that by making an effort to notice and examine our assumptions, and to consider the probability and the cost of our assumptions being wrong later on. This puts us in a good position to adapt our software to change.
And change, even big change, is often more practical than rewriting software from scratch. But how do we sell the business on big changes, especially if they don’t come with obvious changes to the software’s functionality?
We need to practice the psychology of refactoring. We can identify the pain that our refactors address and sell our ideas that way, then plan our work so that we deliver incremental value the whole way through. And, when it comes to it, we can reach out for help, remembering that sometimes, that help might come from some unlikely places.
If you’re interested in the topic of refactoring, I’d like to recommend some…
Christin Gorman talks about some excellent practical examples in her presentation on how to make your code sustainable, which she gave at JavaZone 3 years ago. I have watched this talk like 9 times, and I have sent it to other developers many more times than that.
Michael Feathers’ book Working Effectively with Legacy Code is a go-to for me. I keep a copy on my desk at all times. Book examples are in Java, but I have used lessons from this book on projects in Swift, .Net, Kotlin, Python, Ruby, Java, and Golang.
I also recommend Cohere’s workshop on the subject of Real-World Refactoring. Code examples in this workshop are in Ruby, but I think the material would be accessible to anyone with some object-oriented app development experience in any language.
End of Draft Transcript
Once again if you want to see me deliver this at a stage near you, send this link to someone who can make it happen.
And if you like reading what I have to say about refactoring, I scraped up a few other posts on the topic that I thought you might enjoy.
Other Times I Wrote About Refactoring:
I watched Michael Feathers’ talk on Edge Free Programming and wrote a watcher’s guide—this speaker is the person who wrote Working Effectively with Legacy Code. I find value in what he has to say.
I gave a full screencast on Avdi’s show, RubyTapas, where I draw architecture diagrams and share code before your very eyes! You can see the screencast right here. It’s about testing. You will love it.
Here’s a case study of a refactor I did on a major data import feature with a high potential cost if things went wrong.