Craftsmanship

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? 

image01

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.

what-exactly-are-you-trying-to-say-meme

Defining “Intent”

For this article, I define “Capturing Intent” as “why”. Why does this code exist? What is its purpose? Which business need is it covering?

Benefits

You may already be spending a lot of time writing code and shipping products. Why spend even more time trying to capture the intent?

Understanding

One reason is because developers spend most of their time trying to understand code.

Jeff Atwood summarizes:

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

Robert C. Martin writes in Clean 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

image02

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.

Descriptive implementation

With “Descriptive Implementation” I mean structuring code so it’s understandable. Where it communicates its reason to exist in a clear and concise way.

Readable code

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.


public class ShoppingCart
{
public IList<CartItem> Items { get; set; }
public decimal CartTotal { get; set; }
public void Add(CartItem item)
{
if (Items.Any(x => x.StockKeepingUnit == item.StockKeepingUnit))
{
var i = Items.First(x => x.StockKeepingUnit == item.StockKeepingUnit);
item.Qty += i.Qty;
}
else
{
Items.Add(item);
}
// Extra tracking we need to do for customer service and extra debug tracing
Track();
}
private void Track()
{
var t = Items.Sum(x => x.TotaPrice);
CartTotal = t;
CustomerTrackingService.TrackTotalInShoppingCart(t);
}
}


public class ShoppingCart
{
public IList<CartItem> Items { get; set; }
public decimal CartTotal { get { return Items.Sum(item => item.Total); } }
public void AddCartItemAndDoTracking(CartItem item)
{
AddOrIncreaseQuantityFor(item);
TrackForLoggingDebuggingAndCustomerServiceCenter(CustomerAction.CartItemAddedToCart);
}
private void AddOrIncreaseQuantityFor(CartItem addedItem)
{
if (CartContainsStockKeepingUnit(addedItem.StockKeepingUnit))
{
var itemInCart = Items.First(inCart => inCart.StockKeepingUnit == addedItem.StockKeepingUnit);
itemInCart.Quantity += addedItem.Quantity;
}
else
{
Items.Add(item);
}
}
bool CartContainsStockKeepingUnit(StockKeepingUnit sku)
{
return Items.Any(x => x.Sku == sku)
}
void TrackForLoggingDebuggingAndCustomerServiceCenter(Action action)
{
CustomerTrackingService.SignalTotalInShoppingCartUpdated(action, CartTotal);
}
}

There are a few key changes between the Unclear and WithIntent versions.

  • 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.

The method AddCartItemAndDoTracking is still unclear, since it has a side-effect, which is tracking. One way to solve that is with a decorator:


public class ShoppingCartWithCustomerTraceLogging : ShoppingCart
{
public override void AddCartItem(CartItem item)
{
base.AddCartItem(item);
Track(CustomerAction.CartItemAddedToCart);
}
void Track(Action action)
{
CustomerTrackingService.SignalTotalInShoppingCartUpdated(action, CartTotal);
}
}

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).


public class when_adding_a_cart_item
{
Because customer_service_and_our_customer_journey_tracker_needs_trace_info
= () => shoppingCart.AddCartItem(cartItem);
It sends_the_correct_message_to_the_event_aggregator
= () => MessageHandler.Messages.ShouldContain(message =>
message.type == CustomerActiom.CartItemAddedToCart);
}

Here’s another variant written in a more traditional NUnit approach.


public class When_adding_a_cart_item
{
[SetUp]
public void Because_Customer_service_and_our_customer_journey_tracker_needs_trace_info()
{
_shoppingCart.AddCartItem(cartItem);
}
[Test]
public void It_sends_the_correct_message_to_the_event_aggregator
{
var messagePlaced = MessageHandler.Messages.FirstOrDefault(message => message.Type == CustomerActiom.CartItemAddedToCart);
Assert.NotNull(messagePlaced);
Assert.Equal(messagePlaced.Type, CustomerAction.CartItemAddedToCart);
}
}

High-Level Behaviours

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.

  Feature: Add item to shopping cart
    ShoppingCart should have add item if it doesnt exist in cart
    ShoppingCart should increase quantity if item already in cart
    If there is no coffee left then money should be refunded

  Scenario: Add StockKeepingUnit 1234 with quantity 2 to shopping cart
    Given there are 2 items of StockKeepingUnit 1234 already in cart
    When I press the Add to cart button
    Then total number of items should be 4
    And TraceLogging should recieve message CustomerActiom.CartItemAddedToCart

Written Documentation

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.


public class ShoppingCartWithCustomerTraceLogging : ShoppingCart
{
/// <summary>
/// Preferred way to add item to a customers shopping cart
/// Will add the needed extra logging required.
/// </summary>
/// <param name="item"> A CartItem that contains the qty, sku, and item price and item total price</param>
public override void AddCartItem(CartItem item)
{
base.AddCartItem(item);
Track(CustomerAction.CartItemAddedToCart);
}
void Track(Action action)
{
CustomerTrackingService.SignalTotalInShoppingCartUpdated(action, CartTotal);
}
}

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.

Here’s a good article on the topic.

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.


Added extra tracing and logging whenever a customer adds an item to their cart.
This should help our operation team to monitor the system

Issue tracker

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.

Examples of issue trackers are: GitHub Issues, Jira, Team Foundation Server, Trello, Pivotal Tracker.

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.

Developer actions straight on an Issue in Jira.
Developer actions straight on an issue in Jira.

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.

Wiki

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 50 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.

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.

image03

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.

Final thoughts

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.

A special thanks to Kevin O’Shaugnessy from zombiecodekill.com for his help with this article.

Don’t miss out! Subscribe to be notified of new articles that will help you deliver code with empathy.