Sitemap
CodeX

Everything connected with Tech & Code. Follow to join our 1M+ monthly readers

Coding backward

8 min readApr 3, 2024

--

Photo by Wim Arys on Unsplash

When working backward, we begin with the end in mind and keep it in sight. We start from the goal (what’s known) and work backward (the unknown). Coding backward means working backward applied to coding. Also known as reverse programming, it makes us think about intent before implementation, making the code more expressive and leaner. Let’s see some examples.

Begin with the test

Test-driven development (TDD) is the perfect example of beginning with the end in mind and moving toward it. It tells you to start with the test (specification) — the reason for the change and the goal — and then jump to the implementation. You do it iteratively, relying on the TDD cycle.

Beware, TDD doesn’t mean “writing the tests first”; it means writing a tiny iteration towards the goal in a test (i.e., the most straightforward assertion to have a failing test) and making it pass; repeat it till you’re happy.

TDD is interface-first, separating interface (what) and implementation (how) design decisions. It forces you to think about what before how, so you’ll hardly produce cumbersome APIs; since the tests are the first users of the code, you get immediate feedback. The executable tests (safety) and tests as documentation (specification by examples) are just byproducts of TDD.

When we write a test, we imagine the perfect interface for our operation. We are telling ourselves a story about how the operation will look from the outside. Test-Driven Development

Begin with the interface

When starting the work on a user story (which should be only a problem statement), it’s common to begin discussing algorithms or the database, but that’s plain wrong. That’s a bottom-up approach, where we build low-level components first, trying to imagine how they’re used upward in the tech stack. Alternatively, we could start with the domain (the center/core) and build outwards — a domain-centric building approach. The problem is that in both cases, we’re not driven by actual needs but by guesses of what will be needed later.

The solution is to start at the top (at the interface, usually a GUI), build the underlying need, and do it recursively until we reach the lower level — a top-down approach. That way, the users’ needs always drive us, and we do solely what’s required rather than trying to guess what’s needed. We define and implement each tech layer's interface (a GUI, a CLI, a REST API, a function/method signature, etc.) and move downward recursively. The interface establishes a contract you must fulfill. Only after implementing it can we move further down the tech stack. For example, in web development (with client-side rendering), design a minimal UI first, try to implement it, and only then think about the needed backend API and use it (repeat this cycle until the database). If you’re API-first, you may want to start with the OpenAPI specifications.

One of the biggest “Aha!” moments in my career is when I watched a senior colleague sketch out roles, responsibilities and collaborations in his design working backwards from user interactions. “This is the outcome we want. This is how the system achieves that outcome.” Simples. ~ Jason Gorman

A top-down approach ensures you don’t waste time building generic overengineered components trying to guess how they’ll be used up the stack; you don’t create utilities and reusable components and hope for the best. When you journey from the user problem to the database, you build exactly what’s needed and no more, layer by layer.

Until the Cabal process got underway, technology was added to Half-Life freely. It was assumed that “if we build it, they will come,” meaning that any new technology would just naturally find a creative use by the content creation folks. A prime example of this fallacy was our “beam” effect, […] It was added to the engine, the parameters were exposed, and an e-mail was sent out explaining it. The result was … nothing. After two months only one level designer had put it in a map. Engineering was baffled. The Cabal: Valve’s Design Process For Creating Half-Life

Another benefit of top-down is that it encourages the use of ubiquitous language in the codebases since concrete needs drive the code building.

Begin with the commit message

Coding backward can be applied to commit messages. This involves preparing a clear message that states the intended outcome before making any changes to the code. Doing so can help us stay focused and avoid diversions or trying to achieve too much simultaneously. It’s beneficial for people who get distracted easily. Writing it before also helps to make commit messages less about the code and more about the added value in business terms.

A small commit is one with minimal scope; it does one “thing.” […] A commit is atomic when it is a stable, independent unit of change. Write Better Commits, Build Better Projects

Begin with the function steps

If you want to write a function, start by writing the steps necessary to achieve its goal without knowing how they’ll be implemented. This promotes thinking about “what” (known), followed by the “how” (unknown). You’re working upfront, driven by the intended usage rather than implementation details — a top-down approach. You use a variable before defining it or knowing how it will be computed. You call non-existent functions (and later ask the editor to generate them) with the inputs and outputs in place. In a nutshell, you define purpose first, also known as wishful thinking programming.

With wishful thinking, you write your ideal code upfront. If you’re test-driven, you start to play with a test. Don’t worry about it if it doesn’t compile or won’t run. Experiment with different variants until the code is as expressive as possible. Then you just have to make it compile by implementing your abstractions. This is often straightforward once you’ve come up with clear roles for your objects and functions. Your Code as a Crime Scene

// the function below was writen without any of the referenced functions
// (when done, you can ask your code editor to generate the needed functions)
func main() {
customers := readCustomersCSV("input_data.csv")
custumers30YearsOld := filterCustomers(customers, "age", 30)
customersWithDiscount := activateDiscount(custumers30YearsOld)
writeCustomersCSV("output_result.csv", customersWithDiscount)
}
// you should apply this mindset to any function (not just main)

Notice that the steps in the example are written almost in plain language. It’s like an index or a recipe: you can tell the whole story from there at a glance. All the steps present the same level of abstraction. It’s self-documenting code.

Steps-first helps us to tackle complexity in multiple ways:

  • Intent-first: You split the mental work of defining ideal function signatures (the purpose) from the implementation effort. Naming things is way easier because you start with the goal in mind.
  • Focus: It’s guaranteed that you will do only what’s required and no more. You avoid side-tracking because you have a clear goal.
  • Dividing to conquer: You split a problem into smaller ones — the functions you’ll create later. You recursively apply this reasoning, starting with the entry point (e.g., main), creating functions (at the same level of abstraction), and doing the same until the problem is solved (you could apply this to similar issues like building a regex).

We can use systems modeling to complement the steps-first technique: activity diagrams, flowcharts, state diagrams, sequence diagrams, pseudocode… All of this helps us before jumping into the actual code.

Begin with the comments

In a comments-first approach, we write comments in plain language to explain what we aim to do before the hands-on code. Since we express our intentions as an upfront plan, we must first structure and articulate our thoughts. Writing the code becomes more straightforward, and we may save some back-and-forth and code rewrites.

def main():
# 1: request the type of shape
# 2: request the dimensions of the shape
# 3: calculate the area of the shape
# 4: print the calculated area
...

The second, and most important, benefit of writing the comments at the beginning is that it improves the system design. […] The simpler the comments, the better I feel about my design. A Philosophy of Software Design

Comments-first may be helpful to clarify thoughts (e.g., for a complicated algorithm) as a precursor to steps-first, as in writing quick thoughts on a napkin. However, more often than not, TDD solves the same problems.

Begin with the function goal

Beginning with the function goal means writing functions where you start coding a function by its goal (its return or side effect). The function goal is what’s known from the start. You start in the last line of code and work your way up (into the unknown). Per line of code, you must ask, “What do I need just above to make this feasible?”. Do this until there’s nothing else to do.

// Iteration 1️⃣ 
func main() {
// step 1: what do I want to achieve?
// to write a CSV of 30yo customers with discount
// now I need to compute that variable...
writeCustomersCSV("output_result.csv", customersWithDiscount)
}

// Iteration 2️⃣
func main() {
// step 2: now I need custumers30YearsOld...
customersWithDiscount := activateDiscount(custumers30YearsOld)
writeCustomersCSV("output_result.csv", customersWithDiscount)
}

// Iteration N
// ... (till you're done)

You can apply this technique to any function, especially higher-level ones. This includes any script that you may need to write.

Does everybody not know the trick of coding backwards? Start coding with “return result”. Then the previous line “result = …”. Then the previous line and so on back to the beginning. Try it! Would make a great TikTok. ~ Kent Beck

Coding backward generates leaner code because at each step, you do solely what needs to be done and no more. Each line of code is driven by its intent.

Begin with the assertion

A notable example of starting a function by its end is when writing a test, you begin with the assertion and move your way up. This means doing the Arrange/Act/Assert (the same as Given/When/Then) in the reverse order. This forces you to have the minimum code possible in that test. A test case should isolate a particular scenario, and starting with the assertion, which represents the goal and the reason for the test, is the best way to ensure it. This approach is more meaningful when doing TDD because you start exactly with your goal, having written no other code.

Write the test assertion first and work backwards to the set-up. TDD ~ Jason Gorman

protip: trying working backwards from the assertions Canon TDD ~ Kent Beck

Iteration 1️⃣
def test_calculate_rectangle_area():
assert rectangle_area == 12
# there's an error because rectangle_area in not defined

Iteration 2️⃣
def test_calculate_rectangle_area():
rectangle_area = calculate_area('rectangle', 4, 3)

assert rectangle_area == 12

Iteration 3️⃣
# auto-generate a dummy calculate_area function
# and see the test fail


Iteration 4️⃣
# (we don't need an Arrange in this test)
# implement calculate_area solely to pass this test

Write the outputs, the assertions and the checks first. Then try to explain how to get to those outputs. […] When tests are written from the outputs towards the inputs and contextual information, people tend to leave out all the incidental detail. Fifty Quick Ideas To Improve Your Tests

Learn more

--

--

CodeX
CodeX

Published in CodeX

Everything connected with Tech & Code. Follow to join our 1M+ monthly readers

Luís Soares
Luís Soares

Written by Luís Soares

I write about automated testing, Lean, TDD, CI/CD, trunk-based dev., user-centric dev, domain-centric arch, ...

Responses (1)