Writing Quality Code, Part 2
Writing quality documentation is more important than writing quality code
Have you read 'Writing Quality Code, part 1' yet?: Writing Quality Code, Part 1
A few months in, Alex is starting to get the hang of the booking application at Etrain. New features can be added quickly, bugs can be solved with certainty, and the team has a general feeling of assuredness when implementing changes. “We know what we’re doing,” is the general consent.
But one day, a request comes in to do some work in one of the older parts of the code. That feeling of assuredness quickly fades. The code quality is even lower than the code in the newer parts, but that’s not the worst of it. Alex just cannot decide how to implement the request. “I have no idea why the current code is the way it is,” Alex mumbles. The choices made seem unorthodox and almost counterproductive. But deliberately so. No developer would build this feature in this manner unless there was some concern or another. But Alex can’t find out why from just looking at the code.
Looking at the history, the code change was part of a big commit named “Several bug fixes (incl. refactoring)”. That’s not helpful. And this is a part of the code where there are no tests (still) and no documentation. Alex needs to adjust this code slightly due to a requested feature, but a rewrite is almost certainly necessary. If Alex rewrites and simplifies this code, removing all the weird choices, will it break something? Maybe the correct approach is playing it safe. Just write around all those weird constructs to make sure nothing is inadvertently changed. But Alex would like to document why those constructs are there.
One of Alex’s colleagues has no idea either. “If the quality of the code was higher, we’d need no documentation. Good code documents itself, right?” Alex’ raises one brow. “That’s… something people say. But how does that help us now? We have neither good code nor documentation. You guys are all the ‘documentation’ that we have, and even you have already forgotten most of the decisions made in the past. To be fair, even though I think we’re doing great stuff since I joined the team, it also feels like we’re reinventing the wheel.”
That night, Alex thinks about that phrase that was used. ‘Good code documents itself.’ It’s basically a chicken-and-egg situation. Would improving the code lead to less need for documentation, or would more extensive documentation lead to the ability to write better code? Alex can't shake the feeling that the team is missing a vital step in their development process.
Reading quality code versus writing quality code
Let’s leave Alex to ponder that question. Before I give my answer, here’s a great question for a Sprint Retrospective. For a software engineer, what’s the more important skill to possess? The ability to write quality code, or the ability to read bad code? Think about that one for a minute.
Done? Alright. I’d argue that the ability to read bad code is slightly more valuable than the ability to write good code. This is mostly because quality code will decay into bad code naturally.
I am not saying that the ability to write quality code is not important. Writing clean and efficient code is a key skill for any developer, as it can reduce the likelihood of bugs, improve maintainability, and enhance overall code quality. Furthermore, being able to write quality code is often a prerequisite for being able to read it effectively.
But reading bad code is more important because, in most software development projects, a significant portion of the work involves understanding and maintaining existing code, rather than writing entirely new code from scratch. Therefore, being able to read and comprehend code efficiently is crucial for success in any software engineering role. When reading code, a developer must be able to understand the code's purpose, how it works, and how it fits into the larger architecture of the system.
Additionally, reading and especially learning to understand code (regardless of quality) can also help a developer improve their coding skills. By studying well-written code, developers can gain insights into best practices, design patterns, and other techniques for writing clean and efficient code.
Don’t throw the Agile Manifesto in my face
So, if I’m advocating that we all learn to read bad code, I guess we don’t need documentation, right? Alex should just level up in awful code reading and the purpose of the code will reveal itself, right? This is where I’d add a </sarcasm> joke of some kind, but I hope that would be unnecessarily on the nose. Reading bad code is important, but it doesn’t fully answer Alex’s question. Rephrased slightly: should we address knowledge gaps by writing better code or by adding documentation? This leads us to the hot take for this blog. The answer is documentation. Documentation is the key to quality.
‘Good code documents itself’ sounds great but is a total sophism. Good code means you invested a lot of time in making good decisions based on the current knowns and unknowns. So why would you ever think that those decisions, and the process that led up to it, are okay to be lost to time? Good code, if anything, needs to be documented even more extensively than bad code. Bad code just needs one line like “not sure if this is the right approach”, “this is junk, apologies”, or even “TODO”. Good code needs proper documentation.
Many developers may place too much emphasis on the quality of their code, assuming that clean, concise, and maintainable code is all that's necessary for a successful project. Efficient and correct documentation is critical – probably the single most important part – for the long-term success of a software application.
You might counter my argument by quoting the Agile Manifesto. “Working software over comprehensive documentation.” To which I say: I agree with that statement as well. What I am arguing here is essentially: working software over comprehensive documentation over code quality.
Permanent perfection does not exist
For the sake of argument, let’s say you’ve not only written good code, you’ve actually written great code. Perfect code. But then, time passes. The context of a software application is constantly changing, with new features being added and old ones being removed. As a result, even the best-written code can become difficult to understand and maintain without proper documentation. This becomes even more apparent when new developers join a project or when existing developers need to revisit code they haven't worked on in a while. Proper and up-to-date documentation can make all the difference between a smooth development process and one fraught with confusion and inefficiency.
Efficient documentation also helps ensure that software applications are designed and developed in a way that can be maintained over time. This includes documenting design decisions, requirements, and technical specifications, as well as providing clear instructions for developers to follow. With proper documentation, developers can quickly understand how the software is supposed to function and how different components fit together, allowing them to make informed decisions when it comes to modifying or adding new features.
The reason I think documentation is so important is that it is essential for achieving clarity. By clarity, I mean purpose, intentionality, and context. I firmly believe that those are crucial for efficient development. While it's great to write quality code, relying solely on the code to communicate your intentions is like having a conversation in the dark. Sure, you might be able to hear each other, but a lot of (nonverbal) information is lost as well. Code can never document the business needs that led up to it, nor can it help answer which arguments and obstacles were considered.
So, how do we create clarity in a software project? It starts with discussing clarity during the refinement process. We need to make sure that everyone involved in the project understands what we're trying to achieve and how we plan to achieve it. This includes discussing things like business requirements, architecture decisions, and code choices. By documenting these decisions, we ensure that everyone is on the same page and can refer back to them as needed.
Of course, I am talking about multi-level documentation here. This includes everything from business context to high-level architecture decisions, and from there to the nitty-gritty details of how different components interact with each other. We create a shared understanding of how the system works, making it easier for developers to work on the project over time.
Another way to create clarity: we need to write code that is easy to debug. Code that is difficult to debug is like asking a question to someone who is speaking a foreign language. You might be able to get the gist of their answer, but you're never going to gain a deep understanding of the subject matter. By writing code that is easy to debug, we can ensure that developers can quickly identify and fix problems as they arise, leading to a more efficient development process overall.
Finally, the code that is tested is part of the required documentation already. When we write tests, we create safety nets. But we’re also essentially documenting how the code is supposed to function. This makes it much easier for developers to understand how different components fit together and how they should interact with each other. So, by writing testable code and thoroughly testing it, we create a more self-documenting codebase that is easier to work with over time. It’s not comprehensive by any means, but it's an efficient start.
How to write documentation
Writing quality code is hard. Writing quality documentation is – well, probably just as hard. Documentation needs to be easy to find, easy to read, and easy to update. It should contain as little superfluous information as possible and writing it should cost the least amount of effort possible.
So, to start, the location of where the documentation can be found is crucial. It's important to store your documentation in places that are easily accessible and searchable by everyone involved in the project. Some good places to store documentation that cannot be part of your repository include a shared drive, a wiki, or a documentation tool like Confluence or Google Docs.
Also, documentation should focus (mostly) on the whys instead of the whats. While the whats can usually be found by searching, the whys will be lost forever without documentation. The reason for a particular choice may change over time, and it's helpful to know why someone made a choice so that you can decide when to disregard it. For example, if there was a particular design decision made for performance reasons, it's important to know that so that you don't change it in a way that negatively impacts performance.
One important form of documentation is git commit messages. Good commit messages are descriptive, concise, and provide context for the changes made. A good example of a commit message might be "Refactor user authentication logic to improve security" while a bad example might be "Updated login stuff." Good commit messages help developers understand what changes were made and why, making it easier to debug and maintain the codebase.
Another important form of documentation is stories and features, which can be documented in tools like Azure DevOps Boards or GitHub Projects. A good user story or feature might include information about the business needs behind the feature, any discussions or decisions made during the planning process, and a clear description of the feature itself. A bad example might be a feature that simply lists the steps required to complete it without providing any context or background information.
Finally, it is criminally easy to follow all the above and still end up with outdated documentation. As I said before, the context in which your application is creating value is constantly changing, and so your documentation must change with it. To do so, make sure you add writing and updating documentation to your Definition of Done.
Also, make sure you have a Definition of Done.
How to write tests
I’ve alluded to what I think is one of the most important and efficient forms of documentation: unit tests. At least when it comes to code-level documentation, I feel there is no other alternative that even comes close. Well-named and well-structured tests can show the intent of the code, making it easier to understand and maintain. For example, a test named TestCalculateTotal is not very descriptive and doesn't tell us much about what the test is doing. We don't know what inputs are being used or what the expected output is. The test GivenMultipleItems_WhenCalculatingTotal_ThenReturnCorrectValue is much more descriptive and tells us exactly what the test is doing. We know that it's testing the calculation of a total for multiple items and that the expected output is a correct value. This name also follows the Given-When-Then form, which makes it easy to read and understand.
From the above, it follows logically that I would be a proponent of Test-Driven Development. But while some engineers swear by it, I believe that it may not be the best approach for every project. Sure, I can see the benefits of TDD. But in the real world, if I can avoid writing my tests first, I will. Not for the reason you think, though.
While TDD can be a useful tool for ensuring that code is functioning as intended, it doesn't go far enough in terms of ensuring that the code being developed is truly meeting the needs of the business and the end users. The focus of TDD is on writing tests and then writing code that passes those tests, but this approach doesn't necessarily lead to a deep understanding of the problem being solved or the potential edge cases that may need to be considered.
Instead of relying solely on TDD, software engineers and business representatives should be working together to think through the whats and whys of a project long before any code is written. This may involve exploring potential edge cases and considering how the software will need to evolve to meet changing requirements. By taking this approach, teams can ensure that they are building software that truly meets the needs of the business and the end users, rather than simply focusing on passing tests. And if this approach is followed, most of the benefits of TDD (thinking about requirements before writing code, catching issues early) will remain, while most of the problems (increased development time, large amounts of small no-value tests) can be eliminated.
In addition, involving business representatives early in the process can help to ensure that the code being developed is aligned with the overall goals and objectives of the organization. This helps to avoid the common problem of developing software that technically works but doesn't deliver value to the business. By taking a more holistic approach to software development, teams ensure that they are building software that is both functional and truly valuable to the organization.
To finish this section, I’d like to repeat something I mentioned in my previous blog: nobody should strive for total code coverage with their tests, as that is needlessly rigid. Focus on testing actual use cases and actual risks, and to ensure that the code is working as expected in real-world scenarios. Determining when something is a theoretical pitfall and when it is a real-world issue is not always easy. It requires experience, judgment, and an understanding of the requirements of the project. In the next installments of this blog, I will provide guidance on how to determine when something is real-world applicable, not just in terms of code quality but also in terms of software quality and even ethical considerations.
Solving the chicken-and-the-egg problem
Alex knows that even quality code may become outdated, gibberish, or obsolete in just a few years. If not documented, it’s impossible to tell which of the above is the case. Instead of rewriting the code base, Alex asks the team to look for documentation in old e-mail chains, feature descriptions, documents, and by talking to colleagues. The documentation is discussed, rewritten, updated, and stored in a documentation tool. If no documentation can be found, new documentation is written. If uncertainty exists, the status quo of the current code base is challenged by debugging sessions and thorough manual testing, by adding feature toggles between old-and-new implementations, and even A/B testing.
Alex takes the time to look at git commit messages and history before making changes to existing code. If no tests exist, Alex adds them before making changes. If tests do exist, Alex makes sure that the goal of the tests is clear. Alex focuses, at first at least, on the clarity of the tests instead of the code quality of the code.
Alex understands that the reasons behind the code's original design may have changed since its creation. To account for this, Alex makes sure that any documentation written is grounded in the present, with a clear understanding of how the code has evolved since its inception. This approach allows the team to work more efficiently and effectively, focusing on the most pressing issues while also accounting for the long-term needs of the project.
After a few more months of progress, Alex receives a call. One of the software engineers is taking parental leave for at least two months. This means their team is looking for someone to help them fill the void. And seeing as Alex is doing such a great job with the booking application…
An overview of what has already been posted and what is still to come, here is a full overview:
As always, I ask you to contact me. Send me your corrections, provide suggestions, ask your questions, deliver some hot takes of your own, or share a personal story. I promise to read it, and if possible, will include it in the series.
Writing Quality Code, Part 2