Capturing Intent — Making sense of code
Picture the scenario: You are staring at the computer screen and scanning the lines of code that fill it. You scratch your head and think: Why?
You debug the code, step-by-step and see a method call that sticks out like a sore thumb. It’s doing an out of process call to another subsystem before the code has completed its job in this method.
Why is that out of process call made there? There’s no documentation explaining why. Moving it to the end of the method makes more sense, but then everything breaks. WHY?
As a software developer, you might have experienced situations like this. You’re looking to fix a bug or extend a feature and you stumble over code that just doesn’t make sense.
You can see what it’s doing, even understand how it’s works, but you just can’t understand why.
In this post I’ll show you why making the extra effort to capture the intent of the code is so important.
For this article, I define “Capturing Intent” as “why”. Why does this code exist? What is its purpose? Which business need is it covering?
You may already be spending a lot of time writing code and shipping products. Why spend even more time trying to capture the intent?
One reason is because developers spend most of their time trying to understand code.
If you ask a software developer what they spend their time doing, they’ll tell you that they spend most of their time writing code.
However, if you actually observe what software developers spend their time doing, you’ll find that they spend most of their time trying to understand code
“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.”
So, looking at ways to make our code more understandable seems like the right thing to do.
Breadcrumbs while bug fixing
Codebases that ooze intent are easy to navigate. This is especially useful when bug-fixing, understanding edge-cases and debugging. What this means is you have a narrative you can follow, and hints guiding you through the business logic.
This narrative serves as breadcrumbs to guide the next developer. Quite often that next developer is you!
Future proofing / Saving time
In this context, future proofing isn’t about creating code that covers future scenarios (over-engineering). It is having code that states why it exists.
Allowing a developer to understand implementation and architecture to evolving needs with minimal effort.
Show me some code!
You’ve now seen some of the benefits of writing code with intent. Now let’s look at some concrete examples and different techniques that support this.
With “Descriptive Implementation” I mean structuring code so it’s understandable. Where it communicates its reason to exist in a clear and concise way.
Clean code is a great starting point for writing code with intent. This means writing code with descriptive names, proper scoping and correct abstractions. Dealing with code in bite-sized chunks also aids readability. The examples are quite simple, but imagine them as a replacement.
There are a few key changes between the
- Easier to understand the flow of code.
- Clearer naming.
- Hid the conditional behind a named function
- Focused methods that can be refactored further if needed.
- Removed the need for comment with explicit naming of the tracking function.
AddCartItemAndDoTracking is still unclear, since it has a side-effect, which is tracking. One way to solve that is with a decorator:
Unit tests / specs as describing intent
Sometimes the implementation doesn’t or can’t convey enough intent. In these times it’s good to use code near the implementation to help communicate. This is one of the most useful features of writing good tests / specs.
Unit Tests / Specifications
In this example you’ll see the matching unit test / specification of the above ShoppingCart. The goal is to document the edge-case scenario of extra logging. The spec is written with Machine Specifications (MSpec).
Here’s another variant written in a more traditional NUnit approach.
High-level behaviour tests are another way to document intent and behaviour of a system. They serve as readable specifications which business owners can read, understand and write themselves.
The idea is to allow non-programmers to be able to define requirements up-front. It’s then up to developers to attach code that meets those requirements. A major benefit is that the specs actually describe the system in the language of the business.
These specifications are still code, so they are maintainable and better reflect the implementation. The following example is a SpecFlow spec, which describes a business requirement.
Documentation is a love/hate topic when it comes to software development. Especially when there are so many (forced) requirements within a business context.
Open-source communities have a strong need for good documentation. When a community has this, it’s easier to onboard new users and support existing users.
Written documentation has the added advantage of being free-form, which leads to declarative documentation. However there’s a danger that documentation on this level will get outdated.
Function / Class Documentation
Formal class / function documentation uses formatted comments to create API documentation. This enhances the intent through auto code completion tools such as IntelliSense. You can also expose it to 3rd parties.
This may be practical for certain scenarios for library authors or shared internal tools. On a negative side it’s still free-text, which must match the implementation.
Avoid Code Comments
As a rule of thumb, code that needs inline comments to describe its intent needs improvement. There will be cases where adding a comment makes sense, but this is a last-resort approach. As with any rule, though, it depends on your context.
Source Control Messages
The source control system is another great place to document choices made. When used right, commit messages can serve as a source of the original intent behind a change to the code. The value is seeing the context in which code has changed.
This is especially valuable when working in a long-living, legacy, codebase. Open-source projects also benefit from good commit messages since there are many different contributors.
A common pitfall to avoid is when source control is the primary source of communication.
Read this post for a better understanding on how being more result and user focused will improve your commit messages.
An issue tracker is a system where you can keep track of issues for your product, and usually have an open discussion around a case. It’s where a developer may first look to understand what change is needed in the system.
These issues could be written as a User Story, but can also in many other ways. Issue trackers aren’t a replacement for direct communication. Rather they serve a common goal of documenting the change needed, and the choices involved to make that change.
These systems are completely separate from the code, but it’s possible to integrate them in your workflow.
An example would be to create a branch when moving a story to in progress. Another would be to move a story to done after merging and deployed to production. You can also add the issue id in a commit message, which leaves a comment on the issue itself. This adds yet another layer of context, which can be valuable for understanding the intent of the feature.
Usually the most important part of the issue is the original intent / goal. Looking at the code of an issue after it’s done isn’t always easy. Sometimes the issue will give valuable context of the decisions of the background. Other times it only contains hints of information, and doesn’t capture changes made underway.
At the highest and furthest level away from the implementation is general documentation. Usually documentation resides in a wiki-system with coding best practices, high-level architecture, descriptions of bounded contexts and repositories. This is also where some of the generated documentation may end up. This kind of documentation may serve as a good way to share original intent, but tend to stagnate over time.
Open source communities are often quite reliant on good documentation. When a community has enough contributors, the documentation often accurate, relevant and up to date. Keeping internal documentation at this level, though, is difficult at best.
Be aware of your context
The level of intent you need to include will vary between codebases and teams. Each team will have different needs to capture and share intent.
Are you working on an Minimum Viable Product for a startup with a team of 3? Or on an internal accounting system of a Fortune 50 company with 30 others? Are you the sole contributor, on a tightly knit co-located team, or a distributed team spread across timezones?
Is it a greenfield application with the latest tooling support? Or an ageing application with an equally ageing programming language?
No matter what your context, you are responsible as a developer to convey the intent of why that code exists.
No matter what your context, you are responsible as a developer to convey the intent of why that code exists.
Communication and Empathy
Code is the result of communication between people. It’s this communication that leads to the need to capture intent in your code.
Sometimes this communication leads to not needing to write code at all. It also leads to a shared understanding of the problem that you’re trying to solve and how you want to solve it.
An open channel to stakeholders, developers and users is essential for understanding the actual needs for the system. Being able to empathise with the parties involved can lead to caring more about the outcome of writing the code. This in turn results in cleaner code, with better naming and a natural focus on the end result.
In this article I’ve introduced you to the concept of capturing intent in your code. You’ve learned how to do so at different levels of the software development stack. With this knowledge you effectively communicate with other developers through code.
The closer you communicate your intent to the code implementation, the better. I believe this is the best way to produce maintainable, well-written and self-documenting code.
The further away you focus, the harder it is for the next developer to discover the code’s true purpose.
The ordering of the topics isn’t by accident. The more synchronous the communication, the easier it is for the intent to come across in a meaningful way. Software with intentful and clean code isn’t a replacement for healthy communication practices.
How do you communicate intent of your code with your team? Reach out to me directly if you have any thoughts, questions or criticisms. Or leave a comment below.
Don’t forget to Follow and Like the article.
Originally published on Coding With Empathy