How to write better reusable code
Andi Rosca
Posted on April 7, 2021
Side-note, the original article contains some interactive elements that are only images here.
Code that is easy to reuse is not very customizable, and code that is very customizable is not as easy to reuse.
Think of the difference between
<custom-button color="red">Cancel</custom-button>
and
<custom-button color="red" async="true" size="big" border="false" type="cancel">
Cancel
</custom-button>
You can do more stuff with more code, but that comes with the cost of spending more time setting things up, i.e. boilerplate.
Just think of all the abstract factory makers you've worked with if you've ever written some Java.
How can we write code that is easy to use by other developers, without sacrificing on the ability to customize?
Abstraction
A good abstraction hides away the details that the developer doesn't care about, and only exposes the relevant bits and pieces.
It's like when you try to explain your code to a non-technical person. You gloss over a lot of the details and years of knowledge you accumulated, and use more simple language and analogies that can convey the main idea.
Think of an HTML element such as the input
.
As a user of the HTML abstraction, you don't care about the inner workings of the browser that make it possible to have an interactive text-box presented to the user.
What you care about is that when you write <input type="text" />
the user can now enter some data.
But if an abstraction hides too many things, then it becomes useless in all but the most basic cases.
Imagine if the input element didn't even let you change the placeholder text.
Soon, a lot of developers would be doing:
<div class="my-input-class" contenteditable="true">
<span>Placeholder text...</span>
</div>
Instead of
<input type="text" placeholder="Placeholder text..." />
If you think that's a stretch you can look into recommended ways of replacing browser checkboxes with custom styled ones like this one. Almost all involve hiding the original box and replacing it with an svg or html/css one you made yourself.
It's about balance ☯
So an abstraction's job is to hide things away from the user, so that they can focus on the task at hand. But also to not hide away too many things, in case the user wants to customize it to suit their needs.
If it sounds like creating a good abstraction is hard to do, that's because it is.
Your job as a developer is to navigate these complexities and walk the fine line between too complex and too simple.
Let's see a few mental models and recipes that can get you started.
Pattern 1: Sane defaults and escape hatches
This pattern is as simple as it sounds.
Imagine you're making a recipe website for the singer Pitbull, who has recently taken up cooking.
He's known as Mr. World-Wide™, so the website has to support all languages of the world.
It's a fair assumption to make that most people visiting your website from Spanish, Mexican, Colombian addresses speak Spanish, so you make your default language for those IPs, well, Spanish.
You also know that there's such a thing as expats in the world, so you provide selection box at the top of your website to change the language.
Mr. World-Wide™ is indeed happy with your services.
Setting the default language to Spanish is a sane default; a good assumption to make on how users will use your product/code/feature. You're now saving ~80% of people time from changing the language themselves.
The language selection box at the top is an escape hatch. For the rest of the users to whom the assumption doesn't apply, you offer a way for them to make changes.
The escape hatch does make some people do more steps to use your website, but it doesn't make it impossible for them to use it.
The same principle applies to writing reusable code. You want to save time for 80% of the developers using your code, but leave a way for the rest of 20% to customize it to suit their needs.
Obviously, most situations won't be as cut and dry as the example I've just provided. The hard part about applying this pattern is that you need to know what the most common use cases are, which requires insight into the users of your code before you've even started writing it.
However, it generally doesn't help if you obsess over what your potential users will try to do.
If it's not obvious what the common use case is from the beginning, try the following things:
Dogfooding 🐶
Dogfooding refers to eating your own dog food, i.e. use your own code yourself, in realistic scenarios.
The more different real-life scenarios you can come up with to test your code, the better of a picture you will have of your code's shortcomings and what you can change to accommodate your future users.
Focus on the escape hatches 🚀
If after dogfooding it's still not super clear which features of your code you should make easy by default, you can try another approach and leave the figuring out for later.
The way to do this and minimize breaking changes is to focus on building your escape hatches and making your code customizable.
The more generically customizable it is, the better the chances of you being able to make modifications in the future without causing breaking changes.
There is however the tradeoff that making things too customizable may make the internal implementation too complicated to maintain.
Example
Let's say you have made a vanilla JavaScript button library that provides the coolest button the world has ever seen:
const btn = new BestestButton("Amazing button");
From dogfooding you learn that it's very common to need to include icons, or loading spinners for buttons triggering async actions, etc.
Now you may not know exactly which case you should support and make easiest for your amazing button, but you can ignore that for now and build in escape hatches that will enable you to consolidate the library later on, without having breaking changes.
In this particular case you could add the following escape hatches for your users (and yourself):
- Make the button constructor accept both strings and HTML elements for the content shown inside
- Accept a second argument which will be a configuration object
const btn = new BestestButton(
// Instead of only strings users can add their own custom html elements
elementContainingIconAndText,
// Generic configuration object that can be extended with
// other accepted properties
{ animateClick: true }
);
The example I've laid out is quite simple and had possibly obvious answers, but the same principles apply to more complex scenarios.
It'll probably take longer to come up with good escape hatches but anythings possible with enough time spent.
Pattern 2: Do one thing well
I named it a pattern for the sake of title consistency but this one's more of a philosophy. You should start thinking about it before any line of code is written.
What "do one thing well" means is that you should very clearly define what your code can do, and what it won't do.
Let's say you've decided to create an HTML Canvas library for making interactive 2D shapes that you can select and drag around. You go ahead and implement a great library that many people use and like.
However, you start noticing that many users report the library rendering very slowly when there are more than 5000 shapes, and they all urge you to also provide a WebGL rendering option, for high-performance needs.
It is up to you now to decide if the one thing that your library does well is either:
- Makes drawing 2D shapes on the Canvas easy
- Makes drawing 2D shapes in the Browser easy
It's your choice what the scope of your library is, but it should be a conscious choice.
Don't just go with the flow 🌊
If you get pressured into implementing version 2, people might start requesting for you to add more functionality. Maybe they want special options for the WebGL. Maybe they want you to add basic 3D shapes as well.
You might wake up in a few years realizing you implemented Blender in the browser, when all you actually wanted to do was to drag some rectangles around.
Stick with what you believe is best
If you stick with your initial scope and purpose, you can spend more time to improve the features that are already there.
You could still implement that WebGL rendering option for performance gains, but this time as part of the goal of the library.
If people start requesting basic 3D shapes, you can simply say that implementing that would defeat the purpose of the library.
You can make multiple things that do one thing well
If you do think a browser based Blender is cooler and want to implement a WebGL 3D editor, there's no reason why you can't create a new library that does that one thing very well, without changing your 2D Canvas code.
Part 2 coming soon
There are more tips I would like to share with you, but I want to keep articles at a manageable length.
Stay tuned for part 2 of this post!
You can subscribe to get email notifications on the original post page (at the bottom): https://godoffrontend.com/posts/terseness-vs-control/
Posted on April 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.