It’s often said that software engineers have no code of ethics. This is untrue. For example, no respectable software engineer would ever consent to writing a function called DestroyBaghdad(). Professional ethics would compel them to instead write a function DestroyCity, to which “Baghdad” could be passed as a parameter.
Reusability, like readability, counts.
Although I touched on this in my Write maintainable code the first time article, this topic is so imperative that it deserves its own article.
I often see people arguing that reusable code is a by-product of maintainable code. While I think these programmatic attributes are closely linked, I am of the opinion that one should write with reusability and efficiency in mind instead of maintainability. Why? Because "maintainability" can be arbitrary and ambiguous, espescially within a corporate environment.
The ambiguity of maintainability
Maintenance means different things in different contexts, even within the category of programming. One senior developer may view maintainability as the level of cleanliness within a codebase, while a junior may view it as "how easily I can modfiy it." Or these may be swapped, who knows.
The point is: asking someone to write maintainable code isn't exactly a specific request. It can mean many different things to many different people. Within even my article linked above, I only mention a few characteristics of maintainable code, not what "programmatic maintainability" actually means. After all, the way you'll write maintainable code will differ depending on a variety of factors: your programming language, your area of concern (web, desktop, embedded devices, etc.) and so on. These factors all make "writing maintainable code" an ambiguous venture.
You can view maintainability as a "high-level" concept. It's abstract in nature. It represents and contains a variety of different topics, ranging from modularization to cleanliness. Even the latter being abstract within itself.
Reusability, on the other hand, is more "low-level." It's something we, as programmers, can understand, visualize, and implement without too much direction. In particular, within the highly popular object-oriented approach.
For this reason, I think programmers should instead write code with reusability in mind. For the rest of this article, I'll explain what makes code reusable, and why we should even bother.
Why code should be reusable
Since we speak the same language, I'll let the code do the heavy lifting here. I'll walk through a realistic example that I've encountered a few times as of late. First, here's a very normal-looking HTTP GET request method that returns a user:
Let's take this example apart and understand why you (hopefully) won't see such a specific method like this in a professional codebase.
At first glance, this method may seem just fine. It's properly commented, properly formatted, has
catch cases, etc. etc.. So... what's wrong with this? Not only is this method in and of itself not very reusable, but it uses a variety of operations that can all be extraced to separate methods to later be reused.
For the sake of argument, let's say this method always returns
null. The status is
200, but the user isn't returned. Obviously, we're not passing the Authorization headers!
Bear with me through these examples. It'll be useful.
Let's go ahead and add those headers now:
Perfect! The user is now properly returned.
Some time later, what if we need to write a new method to update the user's information? Well, copy and paste will be our friend here:
Clearly, we've just copied the
headers object for this new method. No biggie. Now, what if we later need a
delete method? Or what if we have another type of resource within out application that also utilizes both GET and PUT requests? What if.. what if...
We're starting to see how reusable code can be usefull (and maintainable!).
Side note: reusable != premature optimization
More often than I'd like to, I often see people on Stack Overflow saying things like, "there's no need to write 8 new methods for this. That's premature optimization. Write just the one or two you need now, and then extract it later if you find a reason to."
Is it just me, or is this bad advice? Of course, the validity of this advice depends upon the context. In some cases, like premature micro-optimization, waiting until you come upon a problem is totally fine, and often rightfully encouraged. The problem is, this fact isn't often mentioned. People simple say, "wait until you have a problem, then refactor."
I believe we may be lost in translation here. Perhaps the adviser isn't considering that they're speaking with new programmers, who don't write maintainable code the first time. Regardless, I'd like to invalidate this common notion of "waiting until there's a problem."
Like I touched on, there is one primary case where this perspective is completely valid. If you later discover that your program isn't performing as good as it should, that's the time to optimize.
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.
Yet we should not pass up our opportunities in that critical 3%.
I won't bother explaining this. It just makes sense. Don't make uneccesary optimizations.
I will, however, explain how the lines between reusability and optimization are often blurred. When I was starting out, this was the case. "But this 10-layer if-statement works just fine, there's no need to optimize it." This is not true.
I blame, in part, some experienced programmers on Stack Overflow and others for this confusion. Many people don't bother to explain what optimization actually is.
make the best or most effective use of (a situation, opportunity, or resource)
Within the context of programming, optimization is talking about the performance of a program. Many people, however, confuse this with both the maintainability and reusability of a program.
I think too many new programmers have this idea that "refactoring" is optimization. And when you've got experienced programmers on Stack Overflow saying things like "don't do premature optimization," it can be confusing.
Anyways... Let's continue.
The 7 (unofficial) principles of reusable code
Before I dive into these 7 principles, let's talk about something most of you probably understand: CSS. One central concept to CSS is classes; in my opinion, these are the epitome of reusability. Let's very quickly go through a simple example.
Both share the common class
button, but the second has the modifier class
secondary. Think about this for a moment.
Reusability does not mean that all uses of these extracted methods need to have the same behavior (or, in this case, style). Instead, it means that these similar behaviors can share similar operations. The secondary button has the same shape, frame, and layout of the normal button, but it differs in color:
Keep this in mind as we go through the principles: reusable code is only an abstraction (and/or extraction) of common behvaiors. It doesn't mandate that you need to change the structure of your program.
1. Keep it simple
Many of us are aware of the KISS principle. The idea itself is quite simple to understand: keep things simple.
Any intelligent fool can make things bigger, more complex, and more violent. It takes a touch of genius — and a lot of courage to move in the opposite direction.
― E.F. Schumacher
Truth is ever to be found in the simplicity, and not in the multiplicity and confusion of things.
― Isaac Newton
Simplcity is a form of art, really. It's easy to say, "make this method simple." In implementation, however, we often blur the lines between complexity and simplicity.
To excercise my own humility, we'll be using examples from two of my open-source projects: Mercury and Hermes. The latter is the newer, more reusable iteration of the former. In fact, I abhor Mercury's code so much, I archived it and nearly deleted it. I don't know why I kept it, actually. Sentiment, probably. Such a strange feeling.
To aid your understanding, here's the self-described purpose of both Mercury and Hermes:
A fluid, general-purpose command-line utility for centralizing automation scripts.
Essentially, they help to centralize a lot of the automation scripts that I write. This was a difficult problem for me initially, since I had to find a way to automatically, programmatically import Python scripts from any arbitrary directory and allow them to work with the Hermes/Mercury framework. Here's a little GIF showing what it does and how it works:
Hermes is essentially a looped
input which accepts arguments. The goal is to parse the arguments and send them off to their proper "handlers." Mercury's approach was disasterous:
(Note: Please keep in mind that this code is many years old and does not accurately reflect my skills today, haha.)
You can see that there were some good ideas here. Primarily, some data structures were used to manage, for instance, the global commands (
quit, etc.). In general, the structure isn't too bad. We can clearly see a separation of certain types of commands:
- global commands, like
- navigation commands beginning with
- normal system commands prefixed with
- app commands which were not explicitly handled (the
Regardless, this is not an elegant approach. It's error-prone, assumes that all non-global commands are for applications, and so on. ("Applications" in the context of Hermes/Mercury are essentially Python extensions).
Additionally, it's repetitive. All of the if-statements handle a similar thing: parsing commands. This leads us to Principle #2. (Don't worry: I'll later show you the method Hermes uses and how it improves upon this fragile design.)
2. Keep it DRY: reduce repetition
Don't Repeat Yourself is a very popular principle in modern software development. It encourages programmers to extract common logic into separate methods to improve both the behavior and readability of a program.
I think, in part, that this isn't taken seriously enough. Some people think this principle is limited to common operations, not the structure that invokes them. What do I mean? Well, let's look at an example.
This is almost a controversial example, considering how common it is to repeat DOM calls in this way. What's the trouble with just calling
document.querySelector('.input') twice? There may be nothing inherently wrong with this approach, nor does it negatively impact the performance of your website.
What it does do, however, is invite you to continue to write repetitive code.
Extracting the DOM call into a variable or it's own function may seem verbose and futile (the latter, in particular). Why add an extra function to the stack when two minimal DOM calls is hardly frowned upon? For the simple reason that it is repetitive. That's really it.
I have a very valuable principle to writing code: I rarely touch the clipboard. Before you hit Command + C, think carefully about what you're copying and why. Doing this consistently reminds you to take care that you don't repeat yourself too often.
As such, a much better approach would be either:
or, if you may need the same DOM node later:
Next time you write a function or use the clipboard, think carefully about what you're doing, and if you're repeating yourself.
This is such an easy thing to forget about. After all, simply copying some logic is often easier and quicker than creating a new method. I am often guilty of these small repititions. Even in Hermes, the Logger class has repetitive file I/O calls:
Don't forget: keep it DRY.
3. Think in the abstract: make it extractable
There are two primary ways to think about this principle: Abstract base classes (ABC) and abstract structures in general. When most people say that "thinking in the abstract" while writing code is often a good thing, they're usually referring to the former. No matter what this prompts you to think of, approaching problems with "can I extract this into an abstract class?" in mind is incredibly useful. Even if your language, framework, area of concern, whatever, doesn't support ABCs directly, the abstract thought of them is useful in all areas.
Let's use React as an example. React, in the last two or so years (since React 16/17, really) has made a gigantic shift away from class-based components. Now, modern React applications make tremendous use of functional components and hooks. I specifically chose React 17 for this example because classes are not use widely anymore (except in legacy codebases). Don't worry, I'll also use legitimate OOP examples in Python afterwards.
With this in mind, consider the following:
Now, you probably understand what needs to be done here. The previous principle, Keep it DRY, explains that we should avoid repetition as much as possible. So... why am I mentioning this here? Shouldn't I have put it in principle #2? I could have, yes. But this is precisely the point. Thinking in the abstract or, more accurately, with "abstraction" in mind, is similar to how you'd go about reducing repetition.
Here's a better example:
All we did here was take out the common attributes and create a new
Button component. This is dead-simple. Are you beginning to see how nearly all of these principles mesh together? By abstracting common logic, we keep things DRY, thereby improving the simplicity. Neat!
The reason I split these principles up is because they are far easier to think about when separated. If I had just said, "write clean code," you'd be left just as, if not more, confused than you were before.
As promised, I'll show an actual OOP example using Python.
Let's pretend you have a MongoDB database. You have two types of resources, Users and Articles. For simplicity, you've extracted the User logic into it's own class,
UserCollection. It looks like:
Nice! Now the
UserCollection class is essentially a more contextual wrapper for the
pymongo library. Great.
Let's see what your ArticleCollection class looks like:
Hmm.. that's nearly identical... Don't repeat yourself, man. The obvious solution is to extract the logic for both of these into a separate
Collection class to which
ArticleCollection can extend or inherit from as-needed.
I won't bother with the example, because it looks exactly the same, just more abstract.
Even though Principle #3 is rather similar to Principle #2, I still needed to add it. Think in the abstract; find ways to restructure your program to fit common patterns, or create them yourself.
4. Code for extension: make it mutable
Let's navigate back to my Hermes/Mercury example for this one.
What I mean by "code for extension" is two-fold:
- Your program, class, method, etc., should handle enough edge-cases and catch enough errors that it's easily extendable and mutable. Similar to what I mentioned in my Write maintainable code the first time article, maintainable code is partly dependent upon how easy it is to change; and
- Your program, etc., should be able to be both easily interpreted and easily modified by outsiders.
This is somewhat similar to Principle #7, but different in many ways. Likewise, this differs from static maintainability in that maintenance doesn't necessarily require adding features or extension in general.
Therefore, we can summarize this to: "write code in such a way that you can easily add new features later." Please note, I'm not saying you need to go half-way through the rabbit-hole of a brand new feature. In fact, I'm saying this rabbit-hole shouldn't be necessary. Adding new features should be simple.
Hermes is a perfect example of this principle. The whole idea behind it is to allow me to easily extend its capabilities by adding new Python extensions. First, let's see how Mercury (horribly) handled this:
As you can see, I simply imported each app individually. This invaraibly caused me to not use Mercury because of how hard it was to extend. Adding a new app took time; first having to write the app itself and then figure out ("remember," more accurately) how to hook it up.
This is probably the best and most accurate example throughout this entire article (maybe). You can see exactly why this code wasn't written with extension in mind. In fairness, the actual solution to this problem isn't a beginner-level implementation. It took time to get it to work (more accurately, time spent understanding how Python's module system works).
So... how do we make this code more extendable? How can we, for instance, simply place a
.py file in an
apps/ directory and just have it work? You're about to see.
It's this single function that made Hermes 1000x more extendable. Simply using a base app framework, you can add files that do anything you want, all while invoking them from a single, friendly interface.
Even though the entire philosophy behind Hermes is to allow native Python extensions, I'm sure you can understand what I'm saying here. Your code should be mutable, easily extendable.
This is really important when starting new projects. You have to get the structure right, or else you'll find yourself starting over.
5. No unnecessary actions: Don't write unneeded code
One of my favorite personal philosophic works is the popular Meditations by Marcus Aurelius. In it, he often alludes to the idea that we should only do what is necessary. Verbatim, he even says, "no unnecessary actions." While I've found great results from applying this in my personal life, I've found just as much within my programs. Don't write unnecessary code.
Without doubt, one of my top 10 favorite quotes from Meditations is this:
Ask yourself at every moment: is this necessary?
I adore that. In fact, I've written it on my person a few times, to remind myself to think about it (humorously, writing it down may itself be unnecessary). Regardless, this is something we need to ask ourselves constantly while writing code.
Although this principle is quite similar to Principle #1, it's more specific in that we can directly know if some piece of code is unneccesary.
Side-node: Don't abstract to the point of vacancy
Take care that, in your simplifications and abstractions, you don't accidentally (or deliberately) reduce your program to nothing. Don't make it so simple that it's unusable or unreadable. As with everything, find the proper balance.
You know me: let's go through an example.
What's inherently bad about this implementation? Not much, actually. It's clean and straightforward. What could be improved? We've got some unnecessary code here. We're accessing items in the array via their index value, when it'd be preferable to access the string directly. Moreover, we're using string concatenation when we could be using template literals.
Unless you're using C or some other low-level, older language, we can do better.
Let's see the improved version:
We've reduced the complexity, increased the simplicity, and we've limited the operation to the bare essentials; there's nothing unecessary here (apart from, maybe, the semi-colons, haha).
I urge you: look through some of your current code. Can you spot some unnecessary lines, variables, and so on? I bet you can. Why not reduce this?
The primary reason to keep things simple like this: the fewer things you have going on, the fewer things that can go wrong.
It doesn't matter if you have unnecessary code in a for-loop: reduce it anyway. Reduce, reduce, reduce. It doesn't matter if "this can't possibly make any difference." It will. When you see the second
forEach example in a codebase, even if it's not yours, you'll know that the programmer knows the principles of reduction. Don't do more when less is more readable and looks better. Don't forget, however, not to abstract to the point of emptiness.
Unless the performance of your application is critical, it's almost always better to write cleaner, slightly less performant code than to write an ugly, bare-metal implementation. Even if you have to write performant code, make it clean—add comments and be thorough about it. Explain why this code is the way it is, and that makes all the difference.
6. Modularize: get the structure right
It's rare to find an in-depth article discussing proper structure of an application or program. Even in the few I do find, they usually discuss the structure of a single file, not an entire project. This is so unbelievably important; it should be discussed far more often.
While changing the structure of an application is arguably easier than changing the behavior, it's still something you'd rather get right the first time. Moreover, the structure contributes to the maintainability of a project as a whole in many ways.
Often, front-end projects are huge and have usually hundreds of not thousands of components. In bigger applications, like Facebook, these numbers can get ridiculous.
For this reason, it's encouraged that you find a structure you or your team likes and stick to it for all of your projects. Consistency is, above most things, absolutely imperative. What if you later decide to change this structure?
Well... good luck. This is why I say it's extremely important to get it right the first time. Imagine having upwards of 50 repositories. Changing the directory structure of all of them would be tedious, and that's putting it mildly.
I recently ran into this issue of structure. I spent an entire day reorganizing the entire codebase and fixing upwards of 150 import issues. Trust me, you don't want this. It's tedious and prevents actual progress. Get it right the first time, then stick to it.
Why am I talking about directory structure in an article discussing the reusability of code? Because the structure of your application as a whole directly affects the code you write. It matters.
More imperative still, is how you choose to structure individual files. Modularization, this is the key.
In one of my previous articles linked twice already, I touch on the importance of the single responsibility principle (SRP). One important factor is that each file, not just each function, should serve only one purpose. I rarely ever have files that are longer than 200 or so lines. Even those aren't a result of the code directly, but of a framework that, in many ways, mandates such verbose files. This is an important fact.
Looking through popular open-source repositories, like Chromium, for example, we can easily find files that are 3,000 lines or longer. While this is obviously maintainable for certain teams, I'd avoid doing this if at all possible. In my opinion, I'd rather have twenty 100-line files than one 2,000-line file. This is a matter of personal preference, clearly. Though, you must keep in mind that if you don't know enough yet, you should stick with what makes your life easier. In other words, keep it simple unless you know you can consistently maintain a large file.
I'm kind-of beating a dead horse here. The main takeaway:
- Keep files logically modularized by context. There's no need to have an
api.tsfile containing both User API methods and article API methods when a
article.tsfile would make more sense.
- Structure your code in such a way that someone reading a block of it will know what resource, context, etc. it applies to. Don't mix and match methods like Build-a-Bear. Keep it logical. Keep it simple.
7. Write for the external: make it outsider-friendly
I rarely ever see this mentioned. Somewhere, someone said (I'm paraphrasing, of course):
Write code like you're writing an open-source library. Like you're writing it so that other programmers can use it.
Why haven't I pushed a commit in 4 months? Because writing code for open-source usage is difficult. It requires me to be careful and to write good, valid documentation. I'm still modifying Jupiter daily. I use it, literally, in all of my front-end projects. This website included.
Even if my project is closed-source, should I still write it as if it were open-source?
Yes! This is the point. It requires you to properly document your code, properly structure your project, be consistent with both naming and commits, and so on and so forth. To be reductive about it, it just makes your project better.
If you're working with a team of developers on any project, open-source or otherwise, I urge you to give this a try. Try writing code and documentation like you're talking to someone who has no idea what your project is about. Obviously, use your judgement here. Don't over-simplify it to the point of complete abstraction.
I think I've made my point here. If you maintain an open-source project, you probably know what I'm talking about in detail. You have to step-up your game.
Writing reusable code means writing clean, maintainable code. Obviously, not everything in your project can be made to be reusable, and it shouldn't be. I would argue, however, that a lot of it can be refactored to be reusable. In doing so, you reach a fine point: a rare and peaceful place, the tip of Mt. Maintainability.
Refactoring a piece of reusable code means less code you have to touch during a refactor. This is imperative.
I hope you've learned something useful.
Thanks for reading.