I was talking to a brilliant engineer friend the other day who mentioned they never get to build anything from the ground up. Their entire career has consisted of maintaining other people's (often quite poor) code.
In a perfect world, we'd all get to write code from scratch, it would work perfectly, and we would put it into a bin in the sky, never to be looked at by anyone again.
We all know that's not how it works. Code need to be maintained.
Kyle Simpson often begins his talks explaining that we spend only 30% of our time writing code, and 70% of our time maintaining it. Being a good coworker and programmer is not just about being a skillful problem solver, but being legible. Great developers produce code with maintenance in mind.
I often joke that I don’t want to hire a code ninja. Ninjas come in the middle of the night and leave a bloody mess.
I want a code janitor. Someone who walks the hallways of code, cleaning up pieces, dusting up neglected parts, shinning up others, tossing unnecessary bits. I prefer this gentler, more accurate analogy. This is the person you want on your team. This is a person you want in your code reviews.
Shoutout to JD Cantrell, who is an awesome code janitor and code reviewer.
Let's think about code maintenance through the lens of two different programming paradigms
In programming there are many tools and procedures available to meet the same end. There is no right answer. Here is a succinct definition by Michael Feather of two very popular programming paradigms:
Object-Oriented Programming makes code understandable by encapsulating moving parts...
Functional Programming makes code understandable by minimizing moving parts.
Let's consider both in terms of not just understandability but also maintenance.
Functional programming includes, but is not limited to:
- It is declarative - we’re writing in a way that can be reused and not telling the computer exactly what you need it to do at every step. This shares some similarities with abstraction.
- It is pure - we’re not modifying or changing things outside of the function’s scope, and for this reason...
- It is immutable - you won’t get into a situation where you feed it the same value and retrieve different results on multiple executions.
I believe functional programming can be extremely useful in terms of maintenance because of the lack of side effects. This is huge. This keeps our code from becoming brittle. People sometimes think the biggest problem is errors in our code. I’d argue that the worst code isn’t code that errors out and fails. We can track down with methodical isolation. The worst code is code that behaves a way that you can’t predict, quietly, all over the place. You run around the code base playing whack-a-mole and sometimes it’s difficult to find the culprit. A functional programming style takes on this problem preemptively, because it’s built from the ground up to keep this from happening (as much).
Here are some resources if you’d like to dig deeper into functional programming:
- Mary Rose Cook: A practical introduction to functional programming
- Kyle Simpson:Functional Lite
As much as I'm in love with functional programming, please be aware that there can still be issues with maintenance. If a few things use the same pure function, and over time you adjust that function for one of its applications, you can get into a problem where you're also altering other things that are hidden dependencies. Good documentation is really helpful here.
At it's best, Object-Oriented approaches tend to think of the highest order of something, then pares down to each type of instance you can have, breaking out what to do in each case. If you're thinking about it like the Linnean System of Classification for animals and how we'd make Morphology Trees (who wouldn't? :)) You'd start by asking is it warm-blooded? Then, does it have fur? Then does it have a snout? etc. I'm really butchering biology here, but it's an example.
At it's worst, Object-Oriented programming gets a lot of pretty necessary criticism because sometimes you're not clearly describing what you think you're describing. You can think of this like: you think something’s a banana but really it’s a peach because the only thing that’s described to you is that you have a fruit. We talked a little bit before about how that kind of code can be a nightmare to maintain, so while that's not always the case, it can certainly come up.
An applicable example in CSS
With both functional and object-oriented approaches in our minds, let's consider how that applies to CSS and the concept of authoring versus maintenance. CSS is written declaratively and purely. You can't mutate one CSS block using another CSS block.
I think a lot of people would anticipate that a purely functional approach would be best for CSS. They might associate that with keeping CSS as pure as possible, which is where ideas like CSS Modules come from. The concept is that if you encapsulate the styles for the thing you're modifying them right where you need them. You're only ever dealing with that instance. No side effects. I don’t disagree. You avoid a few things this way.
- You avoid collisions. This is where object-oriented programming gets a bad rap for sometimes.
- You avoid naming. Naming is hard. You can avoid naming with this approach.
- You avoid dealing with the cascade. I’ll address this in a minute.
For other kinds of (non-CSS) programming, we keep things pure and avoid global scope and we’re golden! So it must apply everywhere, right?
Here's where we need to get into the right tool for the job. If we’re considering maintenance over writing, here is where other approaches shine:
- Companies big and small tend to have redesigns at least every 1-2 years. If your code base is large, with many people, and you need to change the line-height everywhere from 1.1rem to 1.2rem, are you having to go back into every module and change that value? A global or an object that’s extended becomes extraordinarily useful here.
- The cascade can help, if you understand and organize it. This is the same as any sophisticated software design. You can look at what you’re building and make responsible decisions on your build and design. You decide what can be at a top-level and needs to be inherited by other, smaller, pieces. This avoids spaghetti code in CSS and keeps your code DRY.
- CSS is about design. Good design, is by it's nature, successful when it's cohesive. A CSS codebase that aligns itself with the design infrastructure it's built from allows for cleaner code, with the added benefit of better collaboration. CSS can be self-checking for designers as well: "Wait we have another tertiary button? Why?" Leaks in cohesive UI/UX announce themselves well in this model.
- Naming can help with documentation. This point is a little more prickly, and I'm not sure totally necessary, but worth mentioning. Whether you like BEM, or SMACSS/OOCSS, or Atomic, naming when done well can actually give you good information on where a class is used and why.
The paradigms share traits
As you might suspect, though I see value in other paradigms, my preference is a functional style. With that in mind, you might wonder why I would find value in an Object-Oriented approach to CSS.
Despite it's name, OOCSS shares traits with Functional Programming. To correctly work with OOCSS the way it was intended, you're mostly creating mixins that are expressed similarly to functions with parameters (often with defaults) across your system. They are pure, and can be applied to multiple uses.
It still uses Object-Oriented approaches as well. With a very intelligent architecture built with anticipation of how things might be affected by each other, and carefully considering what should be inherited. Considering that CSS is fundamentally a group of objects, this tends to make more sense in a CSS context than in other languages.
Last year I lead a team to completely refactor a giant Front-End component system. It used an OOCSS architecture, I and saw for myself the benefit of this type of model. Designers could ask me to modify something, and we could quickly see it update across the site. They could even change their minds without too much of a hassle. A last minute request from above did not stall our release. I'm definitely not saying it wasn't without it's pain points- but it was surprisingly smooth for it's scale. This has made me look at the way I write CSS for other applications in a whole new light.
People tend to pit these things against each other:
I would argue that you can be maintenance friendly or create a maintenance nightmare either way.
You can absolutely deal with things like line-heights, fonts, and behaviors responsibly in Aphrodite, or React-CXS, or CSS Modules. I quite like some of these approaches. They can handle inheritance, and be written to extend for a DRY-er and more maintainable approach.
But even though you can, I personally haven’t seen enough examples of projects that do. (I'd love to see some examples!)
Mostly I see people pull out a variable or two for the brand color and nothing else. A few variables does not a responsible system architecture make. I think we can do better here, if we think about our code not in terms of what is easier to write, but what's easier to change. Or better yet, in terms of what's easier for others to change.
Please also take care to create good documentation! With either programming paradigm, you have to know about all of your dependencies before you adjust it. Great documentation also means you are more purposeful in your decision-making as well. There are less "oh well"s when you have to explain what a thing is and where it's used to other people.
The issues I’m tackling in this article are issues of collaboration and scale. Not every company or site or even web app is tackling these things. This is an opinion article that comes from experience. Different things work for different projects.
My hope for this article is to encourage developers to think ahead. We’re all in this together, and the best we can do is learn from one another. If you completely disagree with me, that’s totally fine. My hope is that even in taking a stance, you might solidify a direction you head towards with maintenance as the core concept.