In my first developer job, I worked on the software I had used in a different role at the same company. On my first day, I explained the business need for an extra thousand lines of code to the developer who wrote them.
Learning to maintain and fix a legacy codebase taught me how important it is to instead, practice reading other developer’s code. Knowing the real-world context for the codebase also showed me that career switchers can still benefit from their former career experience. Both of those things showed me how to tell what kind of codebases a developer has worked on just by reading their code.
Developers without much experience fixing and maintaining existing codebases often make short-sighted choices. In this article, I’m going to give you tips to hopefully make your code easier to extend and maintain by questioning every opinion you build into it.
Opinionspermalink
We call code “opinionated” if you have to use it a certain way, like in a game when you are forced to take a certain path. The opinions can be big or small, such as:
- Having to use
className
for CSS classes instead ofclass
in React - Template Syntax in Vue
- Only being able to call hooks in certain places in React
- Almost everything being reactive in Vue
With tools like libraries and frameworks, this is often unavoidable, but even on a smaller scale — like within your components — opinions affect use-cases and scalability. Designs, browsers, architecture, and tools change. The bigger the opinion, the harder it can be to untangle and fix when you need to update. When you get to build a component from scratch, question every opinion you’re building into it.
Extensibilitypermalink
Extensible code allows the addition of new capabilities or functionality. It’s reusable.
For example, you are tasked with building a button. You build it only for your current use case - hard coding the class name and text.
- Code language
- html
<button class="primary-button">Accept</button>
This way, you are only able to use this button when the user is accepting something. What if the client calls you and now wants a button that can be used for cancelling something? You’d probably update the text to be a variable.
- Code language
- jsx
<button className="primary-button">{props.text}</button>
But wait, that hard coded class
/className
means your accept and cancel buttons will look the same! Instead of a class, let’s add a variant
prop to a more generic button
class.
- Code language
- jsx
<button className="button" variant={variant}>{props.text}</button>
Now the parent component can determine the text and styling. The only opinion that you’re forcing the parent component to agree with is the button tag and class.
Maintainabilitypermalink
Maintainability, or ease of maintenance, is your gift for the developer who comes after you. Often, that developer is you in six months. Our button is now extensible, but is it easily maintainable?
With our design, the parent component determines the styling. In a utopia, developers would style every button sensibly without any direction. In reality, there will be dozens of buttons with some copy and pasted styling and some totally unique styling.
For maintainability, we should probably enforce a few types of styling.
- Code language
- tsx
export interface ButtonProps { variant?: "primary" | "secondary" | "tertiary"; text: "string"; // Other props redacted }
- Code language
- tsx
<button className="button" variant={props.variant}>{props.text}</button>
Now we can keep all the styling and variants within the button component. If we need a new use case for the button, we can add another variant name. If we need to update the styling for one or all of the types of buttons, we can update it in one place instead of having to hunt down every single use of the button and change it several times.
For a button component, we’re probably thinking about how we want it to look every time we go to use it. What about a rarer use-case? Let’s say sometimes we need a tooltip. We could add an opt-in prop like addTooltip
that defaults to false
or an opt-out prop like hasTooltip
that defaults to true
. The first would be better if we rarely add a tooltip. The second would be better if we usually add a tooltip.
However, one component can’t cover all of the use cases without sacrificing maintainability. If we notice a pattern, it might be better to turn one component into two, rather than making one component larger and difficult to maintain.
This also applies to naming and structuring your data. If your component has to consume a prop that is sometimes a number and sometimes an array of numbers, you’ll end up repeating yourself a lot as you try to handle both.
If your component consumes two endpoints and compares the data, it’d be a lot easier to write reusable code if the property was always called name
and not sometimes fullName
.
Also, while you can typecast in JavaScript (==
), it’s safer and easier to compare two values of the same type (===
).
Practicepermalink
Start identifying opinions that affect the extensibility and maintainability of your component with tests and documentation. When you list out the things it can do and the things it can’t do, you get an idea of how you’re eliminating choice with your opinions.
I have found that learning concepts from multiple programming languages and paradigms helps train this part of the brain. For example, learning both Functional Programming and Object-oriented Programming makes you think about different ways to evaluate the side effects of code. Similarly, working in a statically-typed environment forces you to evaluate exactly what inputs and outputs a component can have.
Once you have identified all the opinions and side effects, you can ask yourself: how do these choices create problems for future developers?
The best way to practice this mindset is exposing yourself to a variety of codebases. Live code is often very different than the examples you see while learning. You’ll see how two developers can solve one problem very differently. The more you’re exposed to different ways of doing things, the more you realize there aren’t many “right” ways to do things. This often takes pressure off perfectionists. (I say from experience)
Your ignorance should be a joyful experience… If you’re a curious person [legacy code] is a goldmine.
Legacy codebases are the best at teaching you the results of choices made long ago. You’ll see how another developer attempted one small fix or update, and it ballooned into a massive undertaking. Taking a stab at maintaining it and fixing it yourself shows you just how difficult some choices can be to untangle. When you start to evaluate your own choices from the perspective of having to maintain it years from now, you’ll see how difficult it is to anticipate every use case and edge case.
Finally, you’ll need to talk to other developers about the problems they had to solve and the constraints they were under, unless they kept architectural decision records.
I learned this lesson the hard way - complaining about “shitty” code. Luckily, my gracious senior developer explained he knew the people who wrote it. He was impressed that they managed to get anything done in the short deadline they were under. Once you start to approach code this way — taking into account the context in which the decision is made — you’ll probably find that your communication with other developers — especially ones struggling with legacy code — will improve. It will definitely improve your code reviews too.
Wrapping uppermalink
Be nice to future you and the other developers who may have to maintain your components. Learn how to evaluate the side effects of the choices you’re making, test, document, and leave room for other use cases.