It does take effort to craft good software, and it is the responsibility of software engineers to not just craft good software, but also encourage and help their whole team to do so.
In the end, software engineering is a team sport.
Thanks to the people who have given me feedback and helped with this article:
First and foremost, quality software is software that delivers value. They satisfy their customers or users.
Anything else is worthless if your software doesn't make your customers happy.
Our mission as software engineers is to solve problems.
That's the first thing to keep in mind, and whatever you do in software engineering should tie back to that mission.
Your software should be well-tested. The test code should have the same standard of quality as the production code.
The main reason we write tests is to get confidence that our software works as it should before we ship it to our customers.
This is important, because in the end, what really matters is customer satisfaction. If our software isn't reliable and consistently introduces new bugs, our customers will likely be frustrated, this can lead to negative things:
- Customer chooses to use one of our competitors' software.
- Customer speaks ill about our software publicly.
Gaining confidence before shipping software is extremely important:
- For every new feature implemented.
- Every time we ship a new feature, we need to make sure the existing features work.
Additional pros for why software should be well-tested:
You can get feedback during development on whether your feature or any of the existing ones work.
Tests themselves can serve as documentation for other team members to understand what the code should be able to do.
You can refactor your code with confidence because the tests will tell you when a behavior of the software isn't doing what it is expected to do.
I recommend following the Test-Driven Development approach for writing code and achieving high coverage of the software's behaviors, I wrote about it here: The truth about Test-Driven Development (TDD).
Decoupled with intentional coupling
Modules and parts of the software should be decoupled from other software. Changing something in one place shouldn't break things in other places.
We can't decouple every piece of a software. When the coupling is done, it should be intentional and visible.
It shouldn't come across as a surprise when changing a part of the system breaks a different part of the system.
A nice further reading: Bounded Contexts.
Code is more read than written.
A quote by Robert C. Martin:
Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. ...[Therefore,] making it easy to read makes it easier to write.
Variables should have meaningful names and should read like a novel. Logic that requires more cognitive effort is better extracted to variables with meaningful names.
Code patterns should be consistent throughout the codebase. Consistency leads to the less cognitive effort, both working with the code and understanding it.
In some situations, comments are necessary to explain why something was done. Avoid using comments to explain what something does, that is rarely needed.
Make sure the language of the code is domain-centric. Reading the code, you should understand what type of product and system you're dealing with, for an instance, if you're developing a Fitness App, it should be visible in the code through the language used.
The sooner readers of the code can understand the code, the sooner they can begin working with it.
DRY but no wrong abstractions
The software should follow the DRY (Don't Repeat Yourself) principle but not have any wrong abstractions.
It is better to duplicate than to have wrong abstractions which you will end up being bound to.
If you aren't sure whether something should be abstracted or not, it is better to leave it duplicated till you discover the abstraction that fits.
The Don't Repeat Yourself principle is about knowledge, not code. If you have two exact same code pieces that represent the same knowledge, they will always change together and hence should be abstracted.
Code that seems duplicated but represents different knowledge shouldn't be abstracted. Such code could be abstracted, but later become a massive pain as you need to change the individual pieces of knowledge.
An example of a bad abstraction is one with too much indirection. You end up jumping between functions and files just to understand a single piece of knowledge represented.
Default to duplicating code. It is fine. Before you abstract any code, understand the knowledge you're abstracting away and don't fall into the trap where you end up causing your software more harm than ever intended.
Easy to change
The software should be easy to change. This is possible when the risk of change is low.
If changing something causes a bunch of other things to break, that's a sign that you need to refactor the code and likely redesign your software. It could mean for instance that the software isn't decoupled enough.
Your team should feel confident to change any part of the code base. There are no no-go zones that no one wants to touch.
If you're looking at blueprints of a library, you may see a grand entrance, reading areas, shelves, etc. That architecture would scream: Library.
Similar to software engineering, the architecture of the software should tell a programmer what system they are dealing with.
Architectures are not (or should not) be about frameworks. Architectures should not be supplied by frameworks. Frameworks are tools to be used, not architectures to be conformed to. If your architecture is based on frameworks, then it cannot be based on your use cases.
The architecture should be driven by the use cases, this way you can safely describe the structures that support those use cases without committing to any technology. It should be easy to change your mind about the decisions of the architect too.
The software should be secure. Vulnerabilities discovered should be handled as soon as possible. It is good to use modern frameworks and tools that have handled most security concerns for you.
You should always validate user inputs on the server, even if you have validated them on the client. The server is where things are closer to the database and other sensitive parts of the software.
Security should be kept in mind from the beginning of development. Design the software with best practices to meet security.
Attacks could cost the company loads of money. In fact, Companies Lose $400 Billion to Hackers Each Year.
The software should handle errors.
It should handle errors, give users feedback, and have an experience that doesn't block the user from accomplishing their goals or confuse them. It should be able to cope with errors during execution and cope with erroneous input.
Frequent deployment with ease
We want to ship as soon as possible in order to get feedback from our customers as soon as possible. The software should be able to be frequently deployed with ease. It shouldn't be a painful process filled with uncertainty every time you try to deploy.
The software should ideally be deployed anytime a change has happened that's been merged to the main branch. Whether that'd be multiple times a day, daily, weekly, etc.
We want to be confident that we can change any part of the code and ship changes as soon as possible without worrying about bugs. If there are areas that we're not confident about, we should investigate and decide on a suitable investment (refactor, deprecate, isolate...).